Compare commits

...

68 Commits

Author SHA1 Message Date
Marcin Ziąbek 8cc2fb8696 Merge pull request #169 from fredericoregateiro/AvaloniaCompiledBindings
Enable compiled bindings on Previewer
2022-04-08 22:17:41 +02:00
MarcinZiabek 8e7b528cd9 Updated install command in readme 2022-04-08 22:15:12 +02:00
MarcinZiabek 26193313b8 Previewer: hide "Show in default browser" button when no document available 2022-04-08 22:14:19 +02:00
MarcinZiabek 555825e8be Previewer: throwing exception when cannot connect with frontend 2022-04-08 22:14:19 +02:00
Frederico Regateiro ddde8e7517 enable compiled bindings on PreviewerWindow 2022-04-06 14:08:53 +01:00
Marcin Ziąbek d2c82b5988 Update readme.md 2022-04-05 14:54:33 +02:00
Marcin Ziąbek d82aae95b5 Update readme.md 2022-04-05 14:34:00 +02:00
Marcin Ziąbek 5c11a2ab6a Update readme.md 2022-04-05 14:30:51 +02:00
MarcinZiabek 03e3a80fd9 Fixed logo resource for QuestPDF.Previwer 2022-04-04 23:26:16 +02:00
MarcinZiabek 4692c5ce62 Fixed QuestPDF.Previewer build 2022-04-04 23:24:02 +02:00
MarcinZiabek 919f916219 2022.4 release 2022-04-04 23:20:22 +02:00
MarcinZiabek 7a32f774a1 Fixed spelling 2022-04-04 00:38:58 +02:00
MarcinZiabek fba71e6392 Previewer: stop debugging session when previewer is closed 2022-04-04 00:19:36 +02:00
MarcinZiabek e27585a504 Fixed previewer: first document refresh sometimes does not work 2022-04-02 00:23:58 +02:00
MarcinZiabek 9be497ee41 Fixed automatically starting previewer application on custom port 2022-04-01 19:26:54 +02:00
MarcinZiabek 0fc0db55d2 Better button style path in previewer 2022-03-31 23:14:00 +02:00
MarcinZiabek 9e7a1ed49d Previewer improvements 2022-03-31 23:11:34 +02:00
MarcinZiabek c137bcfcde Improved look and feel 2022-03-31 18:33:34 +02:00
MarcinZiabek b80b3414d8 Added http-based previewer 2022-03-31 15:44:28 +02:00
MarcinZiabek 5f7cee3420 Removed old previewer implementation 2022-03-30 23:38:51 +02:00
MarcinZiabek 435c42ab9e Changed previewer communication layer to http 2022-03-30 23:37:37 +02:00
MarcinZiabek 7b4923b45d Merge remote-tracking branch 'origin/previewer' 2022-03-27 20:15:02 +02:00
MarcinZiabek 8732b7e50b Renaming 2022-03-27 20:12:22 +02:00
MarcinZiabek 6b31322f92 Fixed preview button labels 2022-03-27 17:29:50 +02:00
MarcinZiabek 6155d21207 Previewer: added buttons for documentation and project sponsoring 2022-03-27 14:49:50 +02:00
Marcin Ziąbek a0909eb057 Create FUNDING.yml 2022-03-26 21:06:44 +01:00
MarcinZiabek 11ff53946d Introduced new line breaking algorithm: break anywhere 2022-03-26 03:34:31 +01:00
MarcinZiabek 0711bca1e1 Improved respecting spaces at the beginning and end of text block, fixed breaking long words that do not fit in line 2022-03-26 02:58:55 +01:00
MarcinZiabek 297ca48c70 Code cleanup 2022-03-24 23:09:09 +01:00
Marcin Ziąbek d2601e9e5a Merge pull request #157 from Bebo-Maker/previewer
Previewer - ViewModel for PreviewerWindow
2022-03-24 22:20:28 +01:00
Bebo-Maker 421a926780 Fix vertical scroll bar visibility. 2022-03-24 22:05:49 +01:00
Bebo-Maker 307759d1b3 Remove StyledProperties and rework bindings. 2022-03-24 22:01:10 +01:00
Bebo-Maker 00bcb2c25b Add ViewModel for PreviewerWindow. 2022-03-24 21:59:55 +01:00
MarcinZiabek e430484e85 Fixed zooming out 2022-03-24 13:54:38 +01:00
MarcinZiabek d24d887a3a Previewer: simplified translate operations 2022-03-24 13:35:02 +01:00
MarcinZiabek 404914ea7e Previewer: added button to show generated PDF 2022-03-24 01:02:57 +01:00
Marcin Ziąbek 545b76fd40 Merge pull request #155 from Bebo-Maker/previewer
Previewer - Fix multiple small issues
2022-03-24 00:04:34 +01:00
Marcin Ziąbek c801894ec4 Merge branch 'previewer' into previewer 2022-03-24 00:04:28 +01:00
MarcinZiabek c21744e7de Fixed scrolling math 2022-03-23 23:31:27 +01:00
Bebo-Maker 734d116bfe Fix scroll viewer visibility. 2022-03-23 12:58:58 +01:00
Bebo-Maker 0abb67b748 Unregister HotReload handler when window is closed 2022-03-23 09:22:46 +01:00
Bebo-Maker 828bb86737 Fix MaxWidth calculation when pages are empty. 2022-03-23 08:35:53 +01:00
Bebo-Maker 040c47e017 Fix issue when showing previewer multiple times.
Previously when calling ShowPreviewer  in the same process multiple times lead to an Exception inside Avalonia.
2022-03-23 08:35:28 +01:00
MarcinZiabek ff95fe287a Added scrollbar interactions 2022-03-23 01:10:15 +01:00
MarcinZiabek 47e9a49fe5 Fixed scroll, more code refactoring 2022-03-21 01:33:33 +01:00
MarcinZiabek b2826a06b8 Code refactorization + cleanup 2022-03-21 01:16:26 +01:00
MarcinZiabek 321cba32ce Implemented basic canvas interactions 2022-03-21 01:02:53 +01:00
MarcinZiabek 26fdbb714d Simplified rendering operation 2022-03-20 01:53:51 +01:00
Marcin Ziąbek f0cec8988a Merge pull request #147 from Bebo-Maker/document-previewer
Document previewer
2022-03-20 00:48:50 +01:00
Bebo-Maker ea2102fe4c WIP Implemented virtualization of pages.
Render pages into seperate SKPictures
2022-03-19 17:26:58 +01:00
Bebo-Maker 6cca3ed057 Refactor custom drawing operation. 2022-03-17 22:32:37 +01:00
Bebo-Maker 9afe4ec104 Add button for generating pdf in previewer. 2022-03-17 20:03:20 +01:00
Bebo-Maker 846679007f Enable dark mode for previewer and cleanup UI. 2022-03-17 19:35:46 +01:00
Bebo-Maker 0946607154 Add table to example. 2022-03-17 12:05:27 +01:00
Bebo-Maker d5f86e0d70 Fixed rendering issue where content was clipped. 2022-03-17 11:44:40 +01:00
Bebo-Maker 346513ee85 Optimize document rendering.
We dont need to render the document two times anymore.
2022-03-17 10:34:00 +01:00
Bebo-Maker d18ee5f282 Allow intersection after first RenderPass. 2022-03-17 10:32:41 +01:00
Bebo-Maker a85bc0abc1 Add example for using document previewer. 2022-03-16 22:54:03 +01:00
Bebo-Maker 82e39df572 Enable SKPaint.IsAntialias in FontManager. 2022-03-16 22:48:30 +01:00
Bebo-Maker 1311b6db32 Add PreviewerApp and provide extensions for displaying previewer. 2022-03-16 22:48:07 +01:00
Bebo-Maker b5645bfbd5 Implement document preview. 2022-03-16 22:47:05 +01:00
Bebo-Maker 8217b3b54a Implement HotReloadManager. 2022-03-16 22:46:21 +01:00
Bebo-Maker 726759f47a Add size tracking canvas. 2022-03-16 22:45:57 +01:00
Bebo-Maker bbd9c3379e Change access modifier of RenderDocument to internal. 2022-03-16 22:45:39 +01:00
Bebo-Maker 697c5e238d Add InternalsVisibleTo for QuestPDF.Previewer in QuestPDF AssemblyInfo. 2022-03-16 22:45:13 +01:00
Bebo-Maker 21a92be87e Add .editorconfig . 2022-03-16 22:44:45 +01:00
Bebo-Maker b0cca1ff48 Add QuestPDF.Previewer and QuestPDF.Previewer.Examples 2022-03-16 22:42:16 +01:00
Marcin Ziąbek 78c5049eda Update readme.md 2022-03-15 23:23:18 +01:00
42 changed files with 1587 additions and 63 deletions
+131
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
github: QuestPDF
+32
View File
@@ -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();
});
});
});
});
}
}
}
+61
View File
@@ -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();
}
}
+23
View File
@@ -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();
}
}
+202
View File
@@ -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
}
+7
View File
@@ -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>
+27
View File
@@ -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();
}
}
}
+6
View File
@@ -0,0 +1,6 @@
using SkiaSharp;
namespace QuestPDF.Previewer
{
record PreviewPage(SKPicture Picture, float Width, float Height);
}
+118
View File
@@ -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; }
}
}
+97
View File
@@ -0,0 +1,97 @@
<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"
x:DataType="previewer:PreviewerWindowViewModel"
x:CompileBindings="True"
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}"
IsVisible="{Binding !!Pages.Count}"
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();
}
}
}
+38
View File
@@ -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.1</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

+23
View File
@@ -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");
+17
View File
@@ -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
-6
View File
@@ -1,6 +0,0 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("QuestPDF.UnitTests")]
[assembly: InternalsVisibleTo("QuestPDF.Examples")]
[assembly: InternalsVisibleTo("QuestPDF.ReportSample")]
+8 -1
View File
@@ -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();
+4 -2
View File
@@ -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,
};
}
+55
View File
@@ -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;
}
}
+31 -31
View File
@@ -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)
+3 -1
View File
@@ -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
+5
View File
@@ -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
+5 -1
View File
@@ -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()
+108
View File
@@ -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;
}
});
});
}
}
}
+23
View File
@@ -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
+66
View File
@@ -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
+160
View File
@@ -0,0 +1,160 @@
#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;
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()
{
var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
var cancellationToken = cancellationTokenSource.Token;
while (true)
{
await Task.Delay(TimeSpan.FromMilliseconds(250));
if (cancellationToken.IsCancellationRequested)
throw new Exception($"Cannot connect to the QuestPDF Previewer tool. Please make sure that your Operating System does not block HTTP connections on port {Port}.");
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
+10 -2
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.3.1</Version>
<Version>2022.4.1</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>
+1 -3
View File
@@ -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
+6 -10
View File
@@ -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.
+9 -4
View File
@@ -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
@@ -53,7 +53,7 @@ Install-Package QuestPDF
dotnet add package QuestPDF
// Package reference in .csproj file
<PackageReference Include="QuestPDF" Version="2022.3.1" />
<PackageReference Include="QuestPDF" Version="2022.4.1" />
```
[![Nuget version](https://img.shields.io/badge/package%20details-QuestPDF-blue?logo=nuget)](https://www.nuget.org/packages/QuestPDF/)
@@ -68,11 +68,16 @@ A short and easy to follow tutorial showing how to design an invoice document un
A detailed description of behavior of all available components and how to use them with C# Fluent API.
[![Patterns and Practices](https://img.shields.io/badge/%F0%9F%94%8D%20read-patterns%20and%20practices-blue)](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)
[![Patterns and Practices](https://img.shields.io/badge/%E2%9C%A8%20read-patterns%20and%20practices-blue)](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)
Everything that may help you designing great reports and create reusable code that is easy to maintain.
## QuestPDF Previewer
<!-- 🚀 📖 ️-->
The QuestPDF Previewer is a tool designed to simplify and speed up your development lifecycle. First, it shows a preview of your document. But the real magic starts with the hot-reload capability! It observes your code and updates the preview every time you change the implementation. Get real-time results without the need of code recompilation. Save time and enjoy the task!
[![Learn more](https://img.shields.io/badge/%F0%9F%93%96%20Previewer-learn%20more-blue)](https://www.questpdf.com/documentation/document-previewer.html)
<img src="https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/images/previewer/animation.gif" width="100%">
## Simplicity is the key