Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03e3a80fd9 | |||
| 4692c5ce62 | |||
| 919f916219 | |||
| 7a32f774a1 | |||
| fba71e6392 | |||
| e27585a504 | |||
| 9be497ee41 | |||
| 0fc0db55d2 | |||
| 9e7a1ed49d | |||
| c137bcfcde | |||
| b80b3414d8 | |||
| 5f7cee3420 | |||
| 435c42ab9e | |||
| 7b4923b45d | |||
| 8732b7e50b | |||
| 6b31322f92 | |||
| 6155d21207 | |||
| a0909eb057 | |||
| 11ff53946d | |||
| 0711bca1e1 | |||
| 297ca48c70 | |||
| d2601e9e5a | |||
| 421a926780 | |||
| 307759d1b3 | |||
| 00bcb2c25b | |||
| e430484e85 | |||
| d24d887a3a | |||
| 404914ea7e | |||
| 545b76fd40 | |||
| c801894ec4 | |||
| c21744e7de | |||
| 734d116bfe | |||
| 0abb67b748 | |||
| 828bb86737 | |||
| 040c47e017 | |||
| ff95fe287a | |||
| 47e9a49fe5 | |||
| b2826a06b8 | |||
| 321cba32ce | |||
| 26fdbb714d | |||
| f0cec8988a | |||
| ea2102fe4c | |||
| 6cca3ed057 | |||
| 9afe4ec104 | |||
| 846679007f | |||
| 0946607154 | |||
| d5f86e0d70 | |||
| 346513ee85 | |||
| d18ee5f282 | |||
| a85bc0abc1 | |||
| 82e39df572 | |||
| 1311b6db32 | |||
| b5645bfbd5 | |||
| 8217b3b54a | |||
| 726759f47a | |||
| bbd9c3379e | |||
| 697c5e238d | |||
| 21a92be87e | |||
| b0cca1ff48 | |||
| 78c5049eda |
+131
@@ -0,0 +1,131 @@
|
||||
# Rules in this file were initially inferred by Visual Studio IntelliCode from the D:\GithubRepos\QuestPDF codebase based on best match to current usage at 16.03.2022
|
||||
# You can modify the rules from these initially generated values to suit your own policies
|
||||
# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
|
||||
[*.cs]
|
||||
|
||||
|
||||
#Core editorconfig formatting - indentation
|
||||
|
||||
#use soft tabs (spaces) for indentation
|
||||
indent_style = space
|
||||
|
||||
#Formatting - new line options
|
||||
|
||||
#place catch statements on a new line
|
||||
csharp_new_line_before_catch = true
|
||||
#place else statements on a new line
|
||||
csharp_new_line_before_else = true
|
||||
#require members of anonymous types to be on separate lines
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
#require members of object intializers to be on separate lines
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
#require braces to be on a new line for object_collection_array_initializers, methods, anonymous_types, control_blocks, types, and lambdas (also known as "Allman" style)
|
||||
csharp_new_line_before_open_brace =all
|
||||
|
||||
#Formatting - organize using options
|
||||
|
||||
#sort System.* using directives alphabetically, and place them before other usings
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
#Formatting - spacing options
|
||||
|
||||
#require NO space between a cast and the value
|
||||
csharp_space_after_cast = false
|
||||
#require a space before the colon for bases or interfaces in a type declaration
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
#require a space after a keyword in a control flow statement such as a for loop
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
#require a space before the colon for bases or interfaces in a type declaration
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
#remove space within empty argument list parentheses
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
#remove space between method call name and opening parenthesis
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
#remove space within empty parameter list parentheses for a method declaration
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list.
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
|
||||
#Formatting - wrapping options
|
||||
|
||||
#leave code block on single line
|
||||
csharp_preserve_single_line_blocks = true
|
||||
|
||||
#Style - Code block preferences
|
||||
|
||||
#prefer no curly braces if allowed
|
||||
csharp_prefer_braces = false:suggestion
|
||||
|
||||
#Style - expression bodied member options
|
||||
|
||||
#prefer block bodies for constructors
|
||||
csharp_style_expression_bodied_constructors = false:suggestion
|
||||
#prefer block bodies for methods
|
||||
csharp_style_expression_bodied_methods = false:suggestion
|
||||
#prefer expression-bodied members for properties
|
||||
csharp_style_expression_bodied_properties = true:suggestion
|
||||
|
||||
#Style - expression level options
|
||||
|
||||
#prefer out variables to be declared inline in the argument list of a method call when possible
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
#Style - Expression-level preferences
|
||||
|
||||
#prefer objects to be initialized using object initializers when possible
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
#prefer inferred anonymous type member names
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion
|
||||
#prefer inferred tuple element names
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
|
||||
#Style - implicit and explicit types
|
||||
|
||||
#prefer var over explicit type in all cases, unless overridden by another code style rule
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
#prefer var is used to declare variables with built-in system types such as int
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
#prefer var when the type is already mentioned on the right-hand side of a declaration expression
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
|
||||
#Style - language keyword and framework type options
|
||||
|
||||
#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
|
||||
#Style - Miscellaneous preferences
|
||||
|
||||
#prefer local functions over anonymous functions
|
||||
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||
|
||||
#Style - modifier options
|
||||
|
||||
#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods.
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||
|
||||
#Style - Modifier preferences
|
||||
|
||||
#when this rule is set to a list of modifiers, prefer the specified ordering.
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
|
||||
|
||||
#Style - Pattern matching
|
||||
|
||||
#prefer pattern matching instead of is expression with type casts
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
|
||||
#Style - qualification options
|
||||
|
||||
#prefer fields not to be prefaced with this. or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
#prefer methods not to be prefaced with this. or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
#prefer properties not to be prefaced with this. or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
|
||||
[*.{cs,vb}]
|
||||
tab_width=4
|
||||
indent_size=4
|
||||
@@ -0,0 +1 @@
|
||||
github: QuestPDF
|
||||
@@ -371,5 +371,37 @@ namespace QuestPDF.Examples
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BreakingLongWord()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.RenderDocument(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Margin(50);
|
||||
page.PageColor(Colors.White);
|
||||
|
||||
page.Size(PageSizes.A4);
|
||||
|
||||
page.Content().Column(column =>
|
||||
{
|
||||
column.Item().Text(null);
|
||||
|
||||
column.Item().Text(text =>
|
||||
{
|
||||
text.DefaultTextStyle(x => x.BackgroundColor(Colors.Red.Lighten3).FontSize(24));
|
||||
|
||||
text.Span(" " + Placeholders.LoremIpsum());
|
||||
text.Span(" 0123456789012345678901234567890123456789012345678901234567890123456789 ").WrapAnywhere();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.Previewer;
|
||||
using QuestPDF.ReportSample.Layouts;
|
||||
|
||||
//ImagePlaceholder.Solid = true;
|
||||
|
||||
// var model = DataSource.GetReport();
|
||||
// var report = new StandardReport(model);
|
||||
// report.ShowInPreviewer().Wait();
|
||||
//
|
||||
// return;
|
||||
|
||||
Document
|
||||
.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(2, Unit.Centimetre);
|
||||
page.PageColor(Colors.White);
|
||||
page.DefaultTextStyle(x => x.FontSize(20));
|
||||
|
||||
page.Header()
|
||||
.Text("Hot Reload!")
|
||||
.SemiBold().FontSize(36).FontColor(Colors.Blue.Darken2);
|
||||
|
||||
page.Content()
|
||||
.PaddingVertical(1, Unit.Centimetre)
|
||||
.Column(x =>
|
||||
{
|
||||
x.Spacing(20);
|
||||
|
||||
x.Item().Table(t =>
|
||||
{
|
||||
t.ColumnsDefinition(c =>
|
||||
{
|
||||
c.RelativeColumn();
|
||||
c.RelativeColumn(3);
|
||||
});
|
||||
|
||||
t.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Visual Studio");
|
||||
t.Cell().Border(1).Padding(5).Text("Start in debug mode with 'Hot Reload on Save' enabled.");
|
||||
t.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Command line");
|
||||
t.Cell().Border(1).Padding(5).Text("Run 'dotnet watch'.");
|
||||
});
|
||||
|
||||
x.Item().Text("Modify this line and the preview should show your changes instantly.");
|
||||
});
|
||||
|
||||
page.Footer()
|
||||
.AlignCenter()
|
||||
.Text(x =>
|
||||
{
|
||||
x.Span("Page ");
|
||||
x.CurrentPageNumber();
|
||||
});
|
||||
});
|
||||
})
|
||||
.ShowInPreviewer();
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QuestPDF.ReportSample\QuestPDF.ReportSample.csproj" />
|
||||
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
class CommunicationService
|
||||
{
|
||||
public static CommunicationService Instance { get; } = new ();
|
||||
|
||||
public event Action<ICollection<PreviewPage>>? OnDocumentRefreshed;
|
||||
|
||||
private WebApplication? Application { get; set; }
|
||||
|
||||
private readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private CommunicationService()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Task Start(int port)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging(x => x.ClearProviders());
|
||||
Application = builder.Build();
|
||||
|
||||
Application.MapGet("ping", HandlePing);
|
||||
Application.MapGet("version", HandleVersion);
|
||||
Application.MapPost("update/preview", HandleUpdatePreview);
|
||||
|
||||
return Application.RunAsync($"http://localhost:{port}/");
|
||||
}
|
||||
|
||||
public async Task Stop()
|
||||
{
|
||||
await Application.StopAsync();
|
||||
await Application.DisposeAsync();
|
||||
}
|
||||
|
||||
private async Task<IResult> HandlePing()
|
||||
{
|
||||
return OnDocumentRefreshed == null
|
||||
? Results.StatusCode(StatusCodes.Status503ServiceUnavailable)
|
||||
: Results.Ok();
|
||||
}
|
||||
|
||||
private async Task<IResult> HandleVersion()
|
||||
{
|
||||
return Results.Json(GetType().Assembly.GetName().Version);
|
||||
}
|
||||
|
||||
private async Task<IResult> HandleUpdatePreview(HttpRequest request)
|
||||
{
|
||||
var command = JsonSerializer.Deserialize<DocumentSnapshot>(request.Form["command"], JsonSerializerOptions);
|
||||
|
||||
var pages = command
|
||||
.Pages
|
||||
.Select(page =>
|
||||
{
|
||||
using var stream = request.Form.Files[page.Id].OpenReadStream();
|
||||
var picture = SKPicture.Deserialize(stream);
|
||||
|
||||
return new PreviewPage(picture, page.Width, page.Height);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
Task.Run(() => OnDocumentRefreshed(pages));
|
||||
return Results.Ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
class Helpers
|
||||
{
|
||||
public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<PreviewPage> pages)
|
||||
{
|
||||
using var stream = File.Create(filePath);
|
||||
|
||||
var document = SKDocument.CreatePdf(stream);
|
||||
|
||||
foreach (var page in pages)
|
||||
{
|
||||
using var canvas = document.BeginPage(page.Width, page.Height);
|
||||
canvas.DrawPicture(page.Picture);
|
||||
document.EndPage();
|
||||
canvas.Dispose();
|
||||
}
|
||||
|
||||
document.Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Rendering.SceneGraph;
|
||||
using Avalonia.Skia;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
class InteractiveCanvas : ICustomDrawOperation
|
||||
{
|
||||
public Rect Bounds { get; set; }
|
||||
public ICollection<PreviewPage> Pages { get; set; }
|
||||
|
||||
private float Width => (float)Bounds.Width;
|
||||
private float Height => (float)Bounds.Height;
|
||||
|
||||
public float Scale { get; private set; } = 1;
|
||||
public float TranslateX { get; set; }
|
||||
public float TranslateY { get; set; }
|
||||
|
||||
private const float MinScale = 0.1f;
|
||||
private const float MaxScale = 10f;
|
||||
|
||||
private const float PageSpacing = 25f;
|
||||
private const float SafeZone = 25f;
|
||||
|
||||
public float TotalPagesHeight => Pages.Sum(x => x.Height) + (Pages.Count - 1) * PageSpacing;
|
||||
public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
|
||||
public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0;
|
||||
|
||||
public float MaxTranslateY => TotalHeight - Height / Scale;
|
||||
|
||||
public float ScrollPercentY
|
||||
{
|
||||
get
|
||||
{
|
||||
return TranslateY / MaxTranslateY;
|
||||
}
|
||||
set
|
||||
{
|
||||
TranslateY = value * MaxTranslateY;
|
||||
}
|
||||
}
|
||||
|
||||
public float ScrollViewportSizeY
|
||||
{
|
||||
get
|
||||
{
|
||||
var viewPortSize = Height / Scale / TotalHeight;
|
||||
return Math.Clamp(viewPortSize, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#region transformations
|
||||
|
||||
private void LimitScale()
|
||||
{
|
||||
Scale = Math.Max(Scale, MinScale);
|
||||
Scale = Math.Min(Scale, MaxScale);
|
||||
}
|
||||
|
||||
private void LimitTranslate()
|
||||
{
|
||||
if (TotalPagesHeight > Height / Scale)
|
||||
{
|
||||
TranslateY = Math.Min(TranslateY, MaxTranslateY);
|
||||
TranslateY = Math.Max(TranslateY, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
TranslateY = (TotalPagesHeight - Height / Scale) / 2;
|
||||
}
|
||||
|
||||
if (Width / Scale < MaxWidth)
|
||||
{
|
||||
var maxTranslateX = (Width / 2 - SafeZone) / Scale - MaxWidth / 2;
|
||||
|
||||
TranslateX = Math.Min(TranslateX, -maxTranslateX);
|
||||
TranslateX = Math.Max(TranslateX, maxTranslateX);
|
||||
}
|
||||
else
|
||||
{
|
||||
TranslateX = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void TranslateWithCurrentScale(float x, float y)
|
||||
{
|
||||
TranslateX += x / Scale;
|
||||
TranslateY += y / Scale;
|
||||
|
||||
LimitTranslate();
|
||||
}
|
||||
|
||||
public void ZoomToPoint(float x, float y, float factor)
|
||||
{
|
||||
var oldScale = Scale;
|
||||
Scale *= factor;
|
||||
|
||||
LimitScale();
|
||||
|
||||
TranslateX -= x / Scale - x / oldScale;
|
||||
TranslateY -= y / Scale - y / oldScale;
|
||||
|
||||
LimitTranslate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region rendering
|
||||
|
||||
public void Render(IDrawingContextImpl context)
|
||||
{
|
||||
if (Pages.Count <= 0)
|
||||
return;
|
||||
|
||||
LimitScale();
|
||||
LimitTranslate();
|
||||
|
||||
var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
|
||||
|
||||
if (canvas == null)
|
||||
throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}");
|
||||
|
||||
var originalMatrix = canvas.TotalMatrix;
|
||||
|
||||
canvas.Translate(Width / 2, 0);
|
||||
|
||||
canvas.Scale(Scale);
|
||||
canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale);
|
||||
|
||||
foreach (var page in Pages)
|
||||
{
|
||||
canvas.Translate(-page.Width / 2f, 0);
|
||||
DrawBlankPage(canvas, page.Width, page.Height);
|
||||
canvas.DrawPicture(page.Picture);
|
||||
canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
|
||||
}
|
||||
|
||||
canvas.SetMatrix(originalMatrix);
|
||||
DrawInnerGradient(canvas);
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
public bool Equals(ICustomDrawOperation? other) => false;
|
||||
public bool HitTest(Point p) => true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region blank page
|
||||
|
||||
private static SKPaint BlankPagePaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.White
|
||||
};
|
||||
|
||||
private static SKPaint BlankPageShadowPaint = new SKPaint
|
||||
{
|
||||
ImageFilter = SKImageFilter.CreateBlendMode(
|
||||
SKBlendMode.Overlay,
|
||||
SKImageFilter.CreateDropShadowOnly(0, 6, 6, 6, SKColors.Black.WithAlpha(64)),
|
||||
SKImageFilter.CreateDropShadowOnly(0, 10, 14, 14, SKColors.Black.WithAlpha(32)))
|
||||
};
|
||||
|
||||
private void DrawBlankPage(SKCanvas canvas, float width, float height)
|
||||
{
|
||||
canvas.DrawRect(0, 0, width, height, BlankPageShadowPaint);
|
||||
canvas.DrawRect(0, 0, width, height, BlankPagePaint);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region inner viewport gradient
|
||||
|
||||
private const int InnerGradientSize = (int)SafeZone;
|
||||
private static readonly SKColor InnerGradientColor = SKColor.Parse("#666");
|
||||
|
||||
private void DrawInnerGradient(SKCanvas canvas)
|
||||
{
|
||||
// gamma correction
|
||||
var colors = Enumerable
|
||||
.Range(0, InnerGradientSize)
|
||||
.Select(x => 1f - x / (float) InnerGradientSize)
|
||||
.Select(x => Math.Pow(x, 2f))
|
||||
.Select(x => (byte)(x * 255))
|
||||
.Select(x => InnerGradientColor.WithAlpha(x))
|
||||
.ToArray();
|
||||
|
||||
using var fogPaint = new SKPaint
|
||||
{
|
||||
Shader = SKShader.CreateLinearGradient(
|
||||
new SKPoint(0, 0),
|
||||
new SKPoint(0, InnerGradientSize),
|
||||
colors,
|
||||
SKShaderTileMode.Clamp)
|
||||
};
|
||||
|
||||
canvas.DrawRect(0, 0, Width, InnerGradientSize, fogPaint);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<Application x:Class="QuestPDF.Previewer.PreviewerApp"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Application.Styles>
|
||||
<FluentTheme Mode="Dark" />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -0,0 +1,27 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
internal class PreviewerApp : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new PreviewerWindow()
|
||||
{
|
||||
DataContext = new PreviewerWindowViewModel()
|
||||
};
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
record PreviewPage(SKPicture Picture, float Width, float Height);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
class PreviewerControl : Control
|
||||
{
|
||||
private InteractiveCanvas InteractiveCanvas { get; set; } = new ();
|
||||
|
||||
public static readonly StyledProperty<ObservableCollection<PreviewPage>> PagesProperty =
|
||||
AvaloniaProperty.Register<PreviewerControl, ObservableCollection<PreviewPage>>(nameof(Pages));
|
||||
|
||||
public ObservableCollection<PreviewPage>? Pages
|
||||
{
|
||||
get => GetValue(PagesProperty);
|
||||
set => SetValue(PagesProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<float> CurrentScrollProperty = AvaloniaProperty.Register<PreviewerControl, float>(nameof(CurrentScroll));
|
||||
|
||||
public float CurrentScroll
|
||||
{
|
||||
get => GetValue(CurrentScrollProperty);
|
||||
set => SetValue(CurrentScrollProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<float> ScrollViewportSizeProperty = AvaloniaProperty.Register<PreviewerControl, float>(nameof(ScrollViewportSize));
|
||||
|
||||
public float ScrollViewportSize
|
||||
{
|
||||
get => GetValue(ScrollViewportSizeProperty);
|
||||
set => SetValue(ScrollViewportSizeProperty, value);
|
||||
}
|
||||
|
||||
public PreviewerControl()
|
||||
{
|
||||
PagesProperty.Changed.Subscribe(x =>
|
||||
{
|
||||
InteractiveCanvas.Pages = x.NewValue.Value;
|
||||
InvalidateVisual();
|
||||
});
|
||||
|
||||
CurrentScrollProperty.Changed.Subscribe(x =>
|
||||
{
|
||||
InteractiveCanvas.ScrollPercentY = x.NewValue.Value;
|
||||
InvalidateVisual();
|
||||
});
|
||||
|
||||
ClipToBounds = true;
|
||||
}
|
||||
|
||||
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
|
||||
{
|
||||
base.OnPointerWheelChanged(e);
|
||||
|
||||
if ((e.KeyModifiers & KeyModifiers.Control) != 0)
|
||||
{
|
||||
var scaleFactor = 1 + e.Delta.Y / 10f;
|
||||
var point = new Point(Bounds.Center.X, Bounds.Top) - e.GetPosition(this);
|
||||
|
||||
InteractiveCanvas.ZoomToPoint((float)point.X, -(float)point.Y, (float)scaleFactor);
|
||||
}
|
||||
|
||||
if (e.KeyModifiers == KeyModifiers.None)
|
||||
{
|
||||
var translation = (float)e.Delta.Y * 25;
|
||||
InteractiveCanvas.TranslateWithCurrentScale(0, -translation);
|
||||
}
|
||||
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
private bool IsMousePressed { get; set; }
|
||||
private Vector MousePosition { get; set; }
|
||||
|
||||
protected override void OnPointerMoved(PointerEventArgs e)
|
||||
{
|
||||
base.OnPointerMoved(e);
|
||||
|
||||
if (IsMousePressed)
|
||||
{
|
||||
var currentPosition = e.GetPosition(this);
|
||||
var translation = currentPosition - MousePosition;
|
||||
InteractiveCanvas.TranslateWithCurrentScale((float)translation.X, -(float)translation.Y);
|
||||
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
MousePosition = e.GetPosition(this);
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
IsMousePressed = true;
|
||||
}
|
||||
|
||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||
{
|
||||
base.OnPointerReleased(e);
|
||||
IsMousePressed = false;
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
CurrentScroll = InteractiveCanvas.ScrollPercentY;
|
||||
ScrollViewportSize = InteractiveCanvas.ScrollViewportSizeY;
|
||||
|
||||
InteractiveCanvas.Bounds = new Rect(0, 0, Bounds.Width, Bounds.Height);
|
||||
|
||||
context.Custom(InteractiveCanvas);
|
||||
base.Render(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
internal class DocumentSnapshot
|
||||
{
|
||||
public ICollection<PageSnapshot> Pages { get; set; }
|
||||
|
||||
public class PageSnapshot
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public float Width { get; set; }
|
||||
public float Height { get; set; }
|
||||
|
||||
public SKPicture Picture { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:previewer="clr-namespace:QuestPDF.Previewer"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="QuestPDF.Previewer.PreviewerWindow"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ExtendClientAreaToDecorationsHint="true"
|
||||
ExtendClientAreaTitleBarHeightHint="-1"
|
||||
Background="#666"
|
||||
Icon="/Resources/Logo.png"
|
||||
UseLayoutRounding="True"
|
||||
Title="QuestPDF Document Preview">
|
||||
|
||||
<Window.Styles>
|
||||
<Style Selector="Button.actions">
|
||||
<Setter Property="VerticalAlignment" Value="Bottom"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||
<Setter Property="Padding" Value="10"/>
|
||||
<Setter Property="CornerRadius" Value="100"/>
|
||||
<Setter Property="Background" Value="#888"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="#999"/>
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
|
||||
<Panel>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="32" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||
TextAlignment="Center" Text="QuestPDF Previewer" FontSize="14" Foreground="#DFFF" FontWeight="Regular" IsHitTestVisible="False" />
|
||||
|
||||
<previewer:PreviewerControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
|
||||
ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
|
||||
Pages="{Binding Pages, Mode=OneWay}" />
|
||||
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32">
|
||||
<Button Classes="actions"
|
||||
Command="{Binding ShowPdfCommand, Mode=OneTime}"
|
||||
ToolTip.Tip="Generates PDF file and shows it in the default browser. Useful for testing compatibility and interactive links.">
|
||||
<Viewbox Width="24" Height="24">
|
||||
<Canvas Width="24" Height="24">
|
||||
<Path Fill="White" Data="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H13C12.59,21.75 12.2,21.44 11.86,21.1C11.53,20.77 11.25,20.4 11,20H6V4H13V9H18V10.18C18.71,10.34 19.39,10.61 20,11V8L14,2M20.31,18.9C21.64,16.79 21,14 18.91,12.68C16.8,11.35 14,12 12.69,14.08C11.35,16.19 12,18.97 14.09,20.3C15.55,21.23 17.41,21.23 18.88,20.32L22,23.39L23.39,22L20.31,18.9M16.5,19A2.5,2.5 0 0,1 14,16.5A2.5,2.5 0 0,1 16.5,14A2.5,2.5 0 0,1 19,16.5A2.5,2.5 0 0,1 16.5,19Z" />
|
||||
</Canvas>
|
||||
</Viewbox>
|
||||
</Button>
|
||||
|
||||
<Button Classes="actions"
|
||||
Command="{Binding ShowDocumentationCommand, Mode=OneTime}"
|
||||
ToolTip.Tip="Opens official QuestPDF documentation">
|
||||
<Viewbox Width="24" Height="24">
|
||||
<Canvas Width="24" Height="24">
|
||||
<Path Fill="White" Data="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z" />
|
||||
</Canvas>
|
||||
</Viewbox>
|
||||
</Button>
|
||||
|
||||
<Button Classes="actions"
|
||||
Command="{Binding SponsorProjectCommand, Mode=OneTime}"
|
||||
ToolTip.Tip="Do you like QuestPDF? Please consider sponsoring the project. It really helps!">
|
||||
<Viewbox Width="24" Height="24">
|
||||
<Canvas Width="24" Height="24">
|
||||
<Path Fill="White" Data="M12,21.1L10.5,22.4C3.9,16.5 0.5,13.4 0.5,9.6C0.5,8.4 0.9,7.3 1.5,6.4C1.5,6.6 1.5,6.8 1.5,7C1.5,11.7 5.4,15.2 12,21.1M13.6,17C18.3,12.7 21.5,9.9 21.6,7C21.6,5 20.1,3.5 18.1,3.5C16.5,3.5 15,4.5 14.5,5.9H12.6C12,4.5 10.5,3.5 9,3.5C7,3.5 5.5,5 5.5,7C5.5,9.9 8.6,12.7 13.4,17L13.5,17.1M18,1.5C21.1,1.5 23.5,3.9 23.5,7C23.5,10.7 20.1,13.8 13.5,19.8C6.9,13.9 3.5,10.8 3.5,7C3.5,3.9 5.9,1.5 9,1.5C10.7,1.5 12.4,2.3 13.5,3.6C14.6,2.3 16.3,1.5 18,1.5Z" />
|
||||
</Canvas>
|
||||
</Viewbox>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollBar Grid.Row="1" Grid.Column="1"
|
||||
Orientation="Vertical"
|
||||
AllowAutoHide="False"
|
||||
Minimum="0" Maximum="1"
|
||||
IsVisible="{Binding VerticalScrollbarVisible, Mode=OneWay}"
|
||||
Value="{Binding CurrentScroll, Mode=TwoWay}"
|
||||
ViewportSize="{Binding ScrollViewportSize, Mode=OneWay}"></ScrollBar>
|
||||
</Grid>
|
||||
</Panel>
|
||||
</Window>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
class PreviewerWindow : Window
|
||||
{
|
||||
public PreviewerWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using ReactiveUI;
|
||||
using Unit = System.Reactive.Unit;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
internal class PreviewerWindowViewModel : ReactiveObject
|
||||
{
|
||||
private ObservableCollection<PreviewPage> _pages = new();
|
||||
public ObservableCollection<PreviewPage> Pages
|
||||
{
|
||||
get => _pages;
|
||||
set => this.RaiseAndSetIfChanged(ref _pages, value);
|
||||
}
|
||||
|
||||
private float _currentScroll;
|
||||
public float CurrentScroll
|
||||
{
|
||||
get => _currentScroll;
|
||||
set => this.RaiseAndSetIfChanged(ref _currentScroll, value);
|
||||
}
|
||||
|
||||
private float _scrollViewportSize;
|
||||
public float ScrollViewportSize
|
||||
{
|
||||
get => _scrollViewportSize;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _scrollViewportSize, value);
|
||||
VerticalScrollbarVisible = value < 1;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _verticalScrollbarVisible;
|
||||
public bool VerticalScrollbarVisible
|
||||
{
|
||||
get => _verticalScrollbarVisible;
|
||||
private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _verticalScrollbarVisible, value));
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, Unit> ShowPdfCommand { get; }
|
||||
public ReactiveCommand<Unit, Unit> ShowDocumentationCommand { get; }
|
||||
public ReactiveCommand<Unit, Unit> SponsorProjectCommand { get; }
|
||||
|
||||
public PreviewerWindowViewModel()
|
||||
{
|
||||
CommunicationService.Instance.OnDocumentRefreshed += HandleUpdatePreview;
|
||||
|
||||
ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
|
||||
ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/documentation/api-reference.html"));
|
||||
SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
|
||||
}
|
||||
|
||||
private void ShowPdf()
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.pdf");
|
||||
Helpers.GeneratePdfFromDocumentSnapshots(filePath, Pages);
|
||||
|
||||
OpenLink(filePath);
|
||||
}
|
||||
|
||||
private void OpenLink(string path)
|
||||
{
|
||||
var openBrowserProcess = new Process
|
||||
{
|
||||
StartInfo = new()
|
||||
{
|
||||
UseShellExecute = true,
|
||||
FileName = path
|
||||
}
|
||||
};
|
||||
|
||||
openBrowserProcess.Start();
|
||||
}
|
||||
|
||||
private void HandleUpdatePreview(ICollection<PreviewPage> pages)
|
||||
{
|
||||
var oldPages = Pages;
|
||||
|
||||
Pages.Clear();
|
||||
Pages = new ObservableCollection<PreviewPage>(pages);
|
||||
|
||||
foreach (var page in oldPages)
|
||||
page.Picture.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.ReactiveUI;
|
||||
using QuestPDF.Previewer;
|
||||
|
||||
var applicationPort = GetCommunicationPort();
|
||||
CommunicationService.Instance.Start(applicationPort);
|
||||
|
||||
if(Application.Current?.ApplicationLifetime is ClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new PreviewerWindow()
|
||||
{
|
||||
DataContext = new PreviewerWindowViewModel()
|
||||
};
|
||||
|
||||
desktop.MainWindow.Show();
|
||||
desktop.Start(Array.Empty<string>());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
AppBuilder
|
||||
.Configure(() => new PreviewerApp())
|
||||
.UsePlatformDetect()
|
||||
.UseReactiveUI()
|
||||
.StartWithClassicDesktopLifetime(Array.Empty<string>());
|
||||
|
||||
static int GetCommunicationPort()
|
||||
{
|
||||
const int defaultApplicationPort = 12500;
|
||||
|
||||
var arguments = Environment.GetCommandLineArgs();
|
||||
|
||||
if (arguments.Length < 2)
|
||||
return defaultApplicationPort;
|
||||
|
||||
return int.TryParse(arguments[1], out var port) ? port : defaultApplicationPort;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Authors>MarcinZiabek</Authors>
|
||||
<Company>CodeFlint</Company>
|
||||
<PackageId>QuestPDF.Previewer</PackageId>
|
||||
<Version>2022.4.0</Version>
|
||||
<PackAsTool>true</PackAsTool>
|
||||
<ToolCommandName>questpdf-previewer</ToolCommandName>
|
||||
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
|
||||
<PackageReleaseNotes>Initial release.</PackageReleaseNotes>
|
||||
<LangVersion>10</LangVersion>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageIcon>Logo.png</PackageIcon>
|
||||
<PackageIconUrl>https://www.questpdf.com/images/package-logo.png</PackageIconUrl>
|
||||
<PackageProjectUrl>https://www.questpdf.com/</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/QuestPDF/library.git</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<Copyright>Marcin Ziąbek, QuestPDF contributors</Copyright>
|
||||
<PackageTags>pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core previewer</PackageTags>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<OutputType>exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Resources\Logo.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\Logo.png" />
|
||||
<None Include="Resources\Logo.png">
|
||||
<Pack>true</Pack>
|
||||
<Visible>false</Visible>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Avalonia" Version="0.10.10" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.10" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" />
|
||||
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.10" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.10" />
|
||||
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,23 @@
|
||||
Install nuget locally (in directory where nupkg file is located)
|
||||
|
||||
```
|
||||
dotnet tool install --global --add-source . QuestPDF.Previewer --global
|
||||
```
|
||||
|
||||
Run on default port
|
||||
|
||||
```
|
||||
questpdf-previewer
|
||||
```
|
||||
|
||||
Run on custom port
|
||||
|
||||
```
|
||||
questpdf-previewer 12500
|
||||
```
|
||||
|
||||
Remove nuget locally
|
||||
|
||||
```
|
||||
dotnet tool uninstall QuestPDF.Previewer --global
|
||||
```
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
@@ -47,7 +48,7 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
container.Border(0.75f).BorderColor(Colors.Grey.Medium).Grid(grid =>
|
||||
{
|
||||
grid.Columns(6);
|
||||
|
||||
|
||||
grid.Item().LabelCell().Text("Date");
|
||||
grid.Item(2).ValueCell().Text(Model.Date?.ToString("g") ?? string.Empty);
|
||||
grid.Item().LabelCell().Text("Location");
|
||||
|
||||
@@ -8,6 +8,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.UnitTests", "Quest
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Examples", "QuestPDF.Examples\QuestPDF.Examples.csproj", "{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer.Examples", "QuestPDF.Previewer.Examples\QuestPDF.Previewer.Examples.csproj", "{CA413A39-038F-4A9F-B56F-0E5413B6B158}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configuration", "{73123649-216A-47B4-BFB2-DADF13FBE75D}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer", "QuestPDF.Previewer\QuestPDF.Previewer.csproj", "{B2FF6003-3A45-4A78-A85D-B86C7F01D054}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -30,5 +39,13 @@ Global
|
||||
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
[assembly: InternalsVisibleTo("QuestPDF.UnitTests")]
|
||||
[assembly: InternalsVisibleTo("QuestPDF.Examples")]
|
||||
[assembly: InternalsVisibleTo("QuestPDF.ReportSample")]
|
||||
@@ -51,7 +51,14 @@ namespace QuestPDF.Drawing
|
||||
return canvas.Images;
|
||||
}
|
||||
|
||||
private static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
|
||||
internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
|
||||
{
|
||||
var canvas = new SkiaPictureCanvas();
|
||||
RenderDocument(canvas, document);
|
||||
return canvas.Pictures;
|
||||
}
|
||||
|
||||
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
|
||||
where TCanvas : ICanvas, IRenderingCanvas
|
||||
{
|
||||
var container = new DocumentContainer();
|
||||
|
||||
@@ -52,7 +52,8 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
return new SKPaint
|
||||
{
|
||||
Color = SKColor.Parse(color)
|
||||
Color = SKColor.Parse(color),
|
||||
IsAntialias = true
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -67,7 +68,8 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
Color = SKColor.Parse(style.Color),
|
||||
Typeface = GetTypeface(style),
|
||||
TextSize = style.Size ?? 12
|
||||
TextSize = style.Size ?? 12,
|
||||
IsAntialias = true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
public class PreviewerPicture
|
||||
{
|
||||
public SKPicture Picture { get; set; }
|
||||
public Size Size { get; set; }
|
||||
|
||||
public PreviewerPicture(SKPicture picture, Size size)
|
||||
{
|
||||
Picture = picture;
|
||||
Size = size;
|
||||
}
|
||||
}
|
||||
|
||||
internal class SkiaPictureCanvas : SkiaCanvasBase
|
||||
{
|
||||
private SKPictureRecorder? PictureRecorder { get; set; }
|
||||
private Size? CurrentPageSize { get; set; }
|
||||
|
||||
public ICollection<PreviewerPicture> Pictures { get; } = new List<PreviewerPicture>();
|
||||
|
||||
public override void BeginDocument()
|
||||
{
|
||||
Pictures.Clear();
|
||||
}
|
||||
|
||||
public override void BeginPage(Size size)
|
||||
{
|
||||
CurrentPageSize = size;
|
||||
PictureRecorder = new SKPictureRecorder();
|
||||
|
||||
Canvas = PictureRecorder.BeginRecording(new SKRect(0, 0, size.Width, size.Height));
|
||||
}
|
||||
|
||||
public override void EndPage()
|
||||
{
|
||||
var picture = PictureRecorder?.EndRecording();
|
||||
|
||||
if (picture != null && CurrentPageSize.HasValue)
|
||||
Pictures.Add(new PreviewerPicture(picture, CurrentPageSize.Value));
|
||||
|
||||
PictureRecorder?.Dispose();
|
||||
PictureRecorder = null;
|
||||
}
|
||||
|
||||
public override void EndDocument() { }
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ namespace QuestPDF.Elements.Text.Calculation
|
||||
|
||||
public int StartIndex { get; set; }
|
||||
public float AvailableWidth { get; set; }
|
||||
|
||||
public bool IsFirstElementInBlock { get; set; }
|
||||
public bool IsFirstElementInLine { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,6 @@ namespace QuestPDF.Elements.Text.Calculation
|
||||
public int NextIndex { get; set; }
|
||||
public int TotalIndex { get; set; }
|
||||
|
||||
public bool IsLast => NextIndex == EndIndex;
|
||||
public bool IsLast => EndIndex == TotalIndex;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ namespace QuestPDF.Elements.Text.Items
|
||||
{
|
||||
internal class TextBlockSpan : ITextBlockItem
|
||||
{
|
||||
private const char Space = ' ';
|
||||
|
||||
public string Text { get; set; }
|
||||
public TextStyle Style { get; set; } = new TextStyle();
|
||||
|
||||
@@ -26,8 +28,6 @@ namespace QuestPDF.Elements.Text.Items
|
||||
|
||||
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
|
||||
{
|
||||
const char space = ' ';
|
||||
|
||||
var paint = Style.ToPaint();
|
||||
var fontMetrics = Style.ToFontMetrics();
|
||||
|
||||
@@ -35,9 +35,9 @@ namespace QuestPDF.Elements.Text.Items
|
||||
|
||||
// if the element is the first one within the line,
|
||||
// ignore leading spaces
|
||||
if (request.IsFirstElementInLine)
|
||||
if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
|
||||
{
|
||||
while (startIndex < Text.Length && Text[startIndex] == space)
|
||||
while (startIndex < Text.Length && Text[startIndex] == Space)
|
||||
startIndex++;
|
||||
}
|
||||
|
||||
@@ -67,17 +67,12 @@ namespace QuestPDF.Elements.Text.Items
|
||||
if (wrappedTextLength == null)
|
||||
return null;
|
||||
|
||||
textLength = wrappedTextLength.Value;
|
||||
textLength = wrappedTextLength.Value.fragmentLength;
|
||||
|
||||
text = text.Slice(0, textLength);
|
||||
|
||||
var endIndex = startIndex + textLength;
|
||||
var nextIndex = endIndex;
|
||||
|
||||
// when breaking text, omit spaces at the end of the line
|
||||
while (nextIndex < Text.Length && Text[nextIndex] == space)
|
||||
nextIndex++;
|
||||
|
||||
// measure final text
|
||||
var width = paint.MeasureText(text);
|
||||
|
||||
@@ -92,34 +87,39 @@ namespace QuestPDF.Elements.Text.Items
|
||||
|
||||
StartIndex = startIndex,
|
||||
EndIndex = endIndex,
|
||||
NextIndex = nextIndex,
|
||||
NextIndex = startIndex + wrappedTextLength.Value.nextIndex,
|
||||
TotalIndex = Text.Length
|
||||
};
|
||||
|
||||
static int? WrapText(ReadOnlySpan<char> text, int textLength, bool isFirstElementInLine)
|
||||
{
|
||||
// textLength - length of the part of the text that fits in available width (creating a line)
|
||||
}
|
||||
|
||||
// TODO: consider introduce text wrapping abstraction (basic, english-like, asian-like)
|
||||
private (int fragmentLength, int nextIndex)? WrapText(ReadOnlySpan<char> text, int textLength, bool isFirstElementInLine)
|
||||
{
|
||||
// textLength - length of the part of the text that fits in available width (creating a line)
|
||||
|
||||
// entire text fits, no need to wrap
|
||||
if (textLength == text.Length)
|
||||
return textLength;
|
||||
// entire text fits, no need to wrap
|
||||
if (textLength == text.Length)
|
||||
return (textLength, textLength + 1);
|
||||
|
||||
// current line ends at word, next character is space, perfect place to wrap
|
||||
if (text[textLength - 1] != space && text[textLength] == space)
|
||||
return textLength;
|
||||
// breaking anywhere
|
||||
if (Style.WrapAnywhere ?? false)
|
||||
return (textLength, textLength);
|
||||
|
||||
// find last space within the available text to wrap
|
||||
var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(space);
|
||||
// current line ends at word, next character is space, perfect place to wrap
|
||||
if (text[textLength - 1] != Space && text[textLength] == Space)
|
||||
return (textLength, textLength + 1);
|
||||
|
||||
// find last space within the available text to wrap
|
||||
var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(Space);
|
||||
|
||||
// text contains space that can be used to wrap
|
||||
if (lastSpaceIndex > 0)
|
||||
return lastSpaceIndex;
|
||||
// text contains space that can be used to wrap
|
||||
if (lastSpaceIndex > 0)
|
||||
return (lastSpaceIndex, lastSpaceIndex + 1);
|
||||
|
||||
// there is no available space to wrap text
|
||||
// if the item is first within the line, perform safe mode and chop the word
|
||||
// otherwise, move the item into the next line
|
||||
return isFirstElementInLine ? textLength : null;
|
||||
}
|
||||
// there is no available space to wrap text
|
||||
// if the item is first within the line, perform safe mode and chop the word
|
||||
// otherwise, move the item into the next line
|
||||
return isFirstElementInLine ? (textLength, textLength + 1) : null;
|
||||
}
|
||||
|
||||
public virtual void Draw(TextDrawingRequest request)
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace QuestPDF.Elements.Text
|
||||
.ForEach(x => RenderingQueue.Dequeue());
|
||||
|
||||
var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
|
||||
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
|
||||
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.EndIndex;
|
||||
|
||||
if (!RenderingQueue.Any())
|
||||
ResetState();
|
||||
@@ -170,6 +170,8 @@ namespace QuestPDF.Elements.Text
|
||||
|
||||
StartIndex = currentItemIndex,
|
||||
AvailableWidth = availableWidth - currentWidth,
|
||||
|
||||
IsFirstElementInBlock = currentElement == Items.First(),
|
||||
IsFirstElementInLine = !currentLineElements.Any()
|
||||
};
|
||||
|
||||
|
||||
@@ -63,6 +63,12 @@ namespace QuestPDF.Fluent
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T WrapAnywhere<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.WrapAnywhere = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
#region Weight
|
||||
|
||||
public static T Weight<T>(this T descriptor, FontWeight weight) where T : TextSpanDescriptor
|
||||
|
||||
@@ -71,6 +71,11 @@ namespace QuestPDF.Fluent
|
||||
{
|
||||
return style.Mutate(x => x.HasUnderline = value);
|
||||
}
|
||||
|
||||
public static TextStyle WrapAnywhere(this TextStyle style, bool value = true)
|
||||
{
|
||||
return style.Mutate(x => x.WrapAnywhere = value);
|
||||
}
|
||||
|
||||
#region Weight
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace QuestPDF.Infrastructure
|
||||
internal bool? IsItalic { get; set; }
|
||||
internal bool? HasStrikethrough { get; set; }
|
||||
internal bool? HasUnderline { get; set; }
|
||||
internal bool? WrapAnywhere { get; set; }
|
||||
|
||||
internal object PaintKey { get; private set; }
|
||||
internal object FontMetricsKey { get; private set; }
|
||||
@@ -30,7 +31,8 @@ namespace QuestPDF.Infrastructure
|
||||
FontWeight = Infrastructure.FontWeight.Normal,
|
||||
IsItalic = false,
|
||||
HasStrikethrough = false,
|
||||
HasUnderline = false
|
||||
HasUnderline = false,
|
||||
WrapAnywhere = false
|
||||
};
|
||||
|
||||
public static TextStyle Default => new TextStyle();
|
||||
@@ -58,6 +60,7 @@ namespace QuestPDF.Infrastructure
|
||||
IsItalic ??= parentStyle.IsItalic;
|
||||
HasStrikethrough ??= parentStyle.HasStrikethrough;
|
||||
HasUnderline ??= parentStyle.HasUnderline;
|
||||
WrapAnywhere ??= parentStyle.WrapAnywhere;
|
||||
}
|
||||
|
||||
internal void OverrideStyle(TextStyle parentStyle)
|
||||
@@ -71,6 +74,7 @@ namespace QuestPDF.Infrastructure
|
||||
IsItalic = parentStyle.IsItalic ?? IsItalic;
|
||||
HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
|
||||
HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
|
||||
WrapAnywhere = parentStyle.WrapAnywhere ?? WrapAnywhere;
|
||||
}
|
||||
|
||||
internal TextStyle Clone()
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
public class ExceptionDocument : IDocument
|
||||
{
|
||||
private Exception Exception { get; }
|
||||
|
||||
public ExceptionDocument(Exception exception)
|
||||
{
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
public DocumentMetadata GetMetadata()
|
||||
{
|
||||
return DocumentMetadata.Default;
|
||||
}
|
||||
|
||||
public void Compose(IDocumentContainer document)
|
||||
{
|
||||
document.Page(page =>
|
||||
{
|
||||
page.ContinuousSize(PageSizes.A4.Width);
|
||||
page.Margin(50);
|
||||
page.DefaultTextStyle(x => x.FontSize(16));
|
||||
|
||||
page.Foreground().PaddingTop(5).Border(10).BorderColor(Colors.Red.Medium);
|
||||
|
||||
page.Header()
|
||||
.ShowOnce()
|
||||
.PaddingBottom(5)
|
||||
.Row(row =>
|
||||
{
|
||||
row.Spacing(15);
|
||||
|
||||
row.AutoItem()
|
||||
.PaddingTop(15)
|
||||
.Width(48)
|
||||
.AspectRatio(1)
|
||||
.Canvas((canvas, size) =>
|
||||
{
|
||||
const float iconSize = 24;
|
||||
using var iconPath = SKPath.ParseSvgPathData("M23,12L20.56,14.78L20.9,18.46L17.29,19.28L15.4,22.46L12,21L8.6,22.47L6.71,19.29L3.1,18.47L3.44,14.78L1,12L3.44,9.21L3.1,5.53L6.71,4.72L8.6,1.54L12,3L15.4,1.54L17.29,4.72L20.9,5.54L20.56,9.22L23,12M20.33,12L18.5,9.89L18.74,7.1L16,6.5L14.58,4.07L12,5.18L9.42,4.07L8,6.5L5.26,7.09L5.5,9.88L3.67,12L5.5,14.1L5.26,16.9L8,17.5L9.42,19.93L12,18.81L14.58,19.92L16,17.5L18.74,16.89L18.5,14.1L20.33,12M11,15H13V17H11V15M11,7H13V13H11V7");
|
||||
|
||||
using var paint = new SKPaint()
|
||||
{
|
||||
Color = SKColors.Red,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
canvas.Scale(size.Width / iconSize);
|
||||
canvas.DrawPath(iconPath, paint);
|
||||
});
|
||||
|
||||
row.RelativeItem()
|
||||
.Column(column =>
|
||||
{
|
||||
column.Item().Text("Exception").FontSize(36).FontColor(Colors.Red.Medium).Bold();
|
||||
column.Item().PaddingTop(-10).Text("Don't panic! Just analyze what's happened...").FontSize(18).FontColor(Colors.Red.Medium).Bold();
|
||||
});
|
||||
});
|
||||
|
||||
page.Content().PaddingVertical(20).Column(column =>
|
||||
{
|
||||
var currentException = Exception;
|
||||
|
||||
while (currentException != null)
|
||||
{
|
||||
column.Item()
|
||||
.PaddingTop(25)
|
||||
.PaddingBottom(15)
|
||||
|
||||
.Padding(-10)
|
||||
.Background(Colors.Grey.Lighten3)
|
||||
.Padding(10)
|
||||
|
||||
.Text(text =>
|
||||
{
|
||||
text.DefaultTextStyle(x => x.FontSize(16));
|
||||
|
||||
text.Span(currentException.GetType().Name + ": ").Bold();
|
||||
text.Span(currentException.Message);
|
||||
});
|
||||
|
||||
foreach (var trace in currentException.StackTrace.Split('\n'))
|
||||
{
|
||||
column
|
||||
.Item()
|
||||
.ShowEntire()
|
||||
.BorderBottom(1)
|
||||
.BorderColor(Colors.Grey.Lighten2)
|
||||
.PaddingVertical(5)
|
||||
.Text(trace.Trim())
|
||||
.FontSize(12);
|
||||
}
|
||||
|
||||
currentException = currentException.InnerException;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#if NET6_0_OR_GREATER
|
||||
|
||||
using System;
|
||||
|
||||
[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(QuestPDF.Previewer.HotReloadManager))]
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper for subscribing to hot reload notifications.
|
||||
/// </summary>
|
||||
internal static class HotReloadManager
|
||||
{
|
||||
public static event EventHandler? UpdateApplicationRequested;
|
||||
|
||||
public static void UpdateApplication(Type[]? _)
|
||||
{
|
||||
UpdateApplicationRequested?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,66 @@
|
||||
#if NET6_0_OR_GREATER
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static void ShowInPreviewer(this IDocument document, int port = 12500)
|
||||
{
|
||||
document.ShowInPreviewerAsync(port).ConfigureAwait(true).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static async Task ShowInPreviewerAsync(this IDocument document, int port = 12500)
|
||||
{
|
||||
var previewerService = new PreviewerService(port);
|
||||
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
|
||||
|
||||
await previewerService.Connect();
|
||||
await RefreshPreview();
|
||||
|
||||
HotReloadManager.UpdateApplicationRequested += (_, _) => RefreshPreview();
|
||||
|
||||
await WaitForPreviewerExit(cancellationTokenSource.Token);
|
||||
|
||||
Task RefreshPreview()
|
||||
{
|
||||
var pictures = GetPictures();
|
||||
return previewerService.RefreshPreview(pictures);
|
||||
|
||||
ICollection<PreviewerPicture> GetPictures()
|
||||
{
|
||||
try
|
||||
{
|
||||
return DocumentGenerator.GeneratePreviewerPictures(document);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
var exceptionDocument = new ExceptionDocument(exception);
|
||||
return DocumentGenerator.GeneratePreviewerPictures(exceptionDocument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task WaitForPreviewerExit(CancellationToken cancellationToken)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
internal class PreviewerRefreshCommand
|
||||
{
|
||||
public ICollection<Page> Pages { get; set; }
|
||||
|
||||
public class Page
|
||||
{
|
||||
public string Id { get; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
public float Width { get; init; }
|
||||
public float Height { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,151 @@
|
||||
#if NET6_0_OR_GREATER
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
using QuestPDF.Drawing;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
internal class PreviewerService
|
||||
{
|
||||
private int Port { get; }
|
||||
private HttpClient HttpClient { get; }
|
||||
|
||||
public event Action? OnPreviewerStopped;
|
||||
|
||||
public PreviewerService(int port)
|
||||
{
|
||||
Port = port;
|
||||
HttpClient = new()
|
||||
{
|
||||
BaseAddress = new Uri($"http://localhost:{port}/"),
|
||||
Timeout = TimeSpan.FromMilliseconds(250)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task Connect()
|
||||
{
|
||||
var isAvailable = await IsPreviewerAvailable();
|
||||
|
||||
if (!isAvailable)
|
||||
{
|
||||
StartPreviewer();
|
||||
await WaitForConnection();
|
||||
}
|
||||
|
||||
var previewerVersion = await GetPreviewerVersion();
|
||||
CheckVersionCompatibility(previewerVersion);
|
||||
}
|
||||
|
||||
private async Task<bool> IsPreviewerAvailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await HttpClient.GetAsync("/ping");
|
||||
return result.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Version> GetPreviewerVersion()
|
||||
{
|
||||
var result = await HttpClient.GetAsync("/version");
|
||||
return await result.Content.ReadFromJsonAsync<Version>();
|
||||
}
|
||||
|
||||
private void StartPreviewer()
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new()
|
||||
{
|
||||
FileName = "questpdf-previewer",
|
||||
Arguments = $"{Port}",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await process.WaitForExitAsync();
|
||||
OnPreviewerStopped?.Invoke();
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new Exception("Cannot start the QuestPDF Previewer tool. " +
|
||||
"Please install it by executing in terminal the following command: 'dotnet tool install --global QuestPDF.Previewer'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckVersionCompatibility(Version version)
|
||||
{
|
||||
if (version.Major == 2022 && version.Minor == 4)
|
||||
return;
|
||||
|
||||
throw new Exception($"Previewer version is not compatible. Possible solutions: " +
|
||||
$"1) Update the QuestPDF library to newer version. " +
|
||||
$"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version 2022.4'");
|
||||
}
|
||||
|
||||
private async Task WaitForConnection()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(250));
|
||||
|
||||
var isConnected = await IsPreviewerAvailable();
|
||||
|
||||
if (isConnected)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshPreview(ICollection<PreviewerPicture> pictures)
|
||||
{
|
||||
var multipartContent = new MultipartFormDataContent();
|
||||
|
||||
var pages = new List<PreviewerRefreshCommand.Page>();
|
||||
|
||||
foreach (var picture in pictures)
|
||||
{
|
||||
var page = new PreviewerRefreshCommand.Page
|
||||
{
|
||||
Width = picture.Size.Width,
|
||||
Height = picture.Size.Height
|
||||
};
|
||||
|
||||
pages.Add(page);
|
||||
|
||||
var pictureStream = picture.Picture.Serialize().AsStream();
|
||||
multipartContent.Add(new StreamContent(pictureStream), page.Id, page.Id);
|
||||
}
|
||||
|
||||
var command = new PreviewerRefreshCommand
|
||||
{
|
||||
Pages = pages
|
||||
};
|
||||
|
||||
multipartContent.Add(JsonContent.Create(command), "command");
|
||||
|
||||
await HttpClient.PostAsync("/update/preview", multipartContent);
|
||||
|
||||
foreach (var picture in pictures)
|
||||
picture.Picture.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -4,7 +4,7 @@
|
||||
<Authors>MarcinZiabek</Authors>
|
||||
<Company>CodeFlint</Company>
|
||||
<PackageId>QuestPDF</PackageId>
|
||||
<Version>2022.3.1</Version>
|
||||
<Version>2022.4.0</Version>
|
||||
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
|
||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||
<LangVersion>9</LangVersion>
|
||||
@@ -24,7 +24,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.80.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
<InternalsVisibleTo Include="QuestPDF.ReportSample" />
|
||||
<InternalsVisibleTo Include="QuestPDF.UnitTests" />
|
||||
<InternalsVisibleTo Include="QuestPDF.Examples" />
|
||||
<InternalsVisibleTo Include="QuestPDF.Previewer" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
QuestPDF is an open-source .NET library for PDF documents generation.
|
||||
|
||||
It offers a layouting engine designed with a full paging support in mind. The document consists of many simple elements (e.g. border, background, image, text, padding, table, grid etc.) that are composed together to create more complex structures. This way, as a developer, you can understand the behavior of every element and use them with full confidence. Additionally, the document and all its elements support paging functionality. For example, an element can be moved to the next page (if there is not enough space) or even be split between pages like table's rows.
|
||||
|
||||
Unlike other libraries, it does not rely on the HTML-to-PDF conversion which in many cases is not reliable. Instead, it implements its own layouting engine that is optimized to cover all paging-related requirements.
|
||||
It offers a layout engine designed with a full paging support in mind. The document consists of many simple elements (e.g. border, background, image, text, padding, table, grid etc.) that are composed together to create more complex structures. This way, as a developer, you can understand the behavior of every element and use them with full confidence. Additionally, the document and all its elements support paging functionality. For example, an element can be moved to the next page (if there is not enough space) or even be split between pages like table's rows. All of it done with predictable and discoverable Fluent API.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
- Added minimal API to help you quickly start with QuestPDF development and speed up prototyping,
|
||||
- Improved exception message when desired font type cannot be found (instead of loading default font on Windows and failing with wrong characters on Linux),
|
||||
- Improved support for custom font types: loading all type faces from a file, respecting true font family, using CSS-like algorithm to find best style match,
|
||||
- Added support for custom page number formats in the `Text` element, e.g. you can implement roman literal style if required,
|
||||
- Improved text API to make it more concise,
|
||||
- Extended support for the `Section` element (previously the `Location` element) by tracking: beginning page number, end page number, page length, page number within location,
|
||||
- Renaming: the `ExternalLink` element was renamed to the `Hyperlink` element,
|
||||
- Renaming: the `Location` element was renamed to the `Section` element,
|
||||
- Renaming: the `InternalLink` element was renamed to the `SectionLink` element,
|
||||
- Updated homepage and GitHub pages content.
|
||||
Release theme:
|
||||
Introduced the QuestPDF Previewer tool - a hot-reload powered, cross-platform program that visualizes your PDF document and updates its preview every time you make a code change. You don't need to recompile your code after every small adjustment. Save time and enjoy the design process! (available only for dotnet 6 and beyond)
|
||||
|
||||
Other changes:
|
||||
- Improved default word-wrapping algorithm to better handle words which do not fit on the available width,
|
||||
- Introduced new word-wrapping option 'WrapAnywhere' that wraps word at the last possible character instead of moving it into new line.
|
||||
|
||||
@@ -21,7 +21,7 @@ Unlike other libraries, it does not rely on the HTML-to-PDF conversion which in
|
||||
|
||||
Choosing a project dependency could be difficult. We need to ensure stability and maintainability of our projects. Surveys show that GitHub stars count play an important factor when assessing library quality.
|
||||
|
||||
⭐ Please give this repository a star. It takes seconds and help thousands of developers!
|
||||
⭐ Please give this repository a star. It takes seconds and help thousands of developers! ⭐
|
||||
|
||||
## Please share with the community
|
||||
|
||||
|
||||
Reference in New Issue
Block a user