Browse Source

Merge pull request #147 from Bebo-Maker/document-previewer

Document previewer
Marcin Ziąbek 3 years ago
parent
commit
f0cec8988a

+ 131 - 0
.editorconfig

@@ -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

+ 52 - 0
QuestPDF.Previewer.Examples/Program.cs

@@ -0,0 +1,52 @@
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+using QuestPDF.Previewer;
+
+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();

+ 15 - 0
QuestPDF.Previewer.Examples/QuestPDF.Previewer.Examples.csproj

@@ -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.Previewer\QuestPDF.Previewer.csproj" />
+    <ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
+  </ItemGroup>
+
+</Project>

+ 31 - 0
QuestPDF.Previewer/DocumentPreviewerExtensions.cs

@@ -0,0 +1,31 @@
+using Avalonia;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Previewer
+{
+    /// <summary>
+    /// Extensions for <see cref="IDocument"/> for previewer
+    /// </summary>
+    public static class DocumentPreviewerExtensions
+    {
+        /// <summary>
+        /// Displays the document in a previewer which supports hot reloading.
+        /// </summary>
+        /// <remarks>
+        /// Intended for development only. Not intended for shipping.
+        /// </remarks>
+        /// <param name="document"></param>
+        public static void ShowInPreviewer(this IDocument document)
+        {
+            ArgumentNullException.ThrowIfNull(document);
+
+            AppBuilder
+                .Configure(() => new PreviewerApp()
+                {
+                    Document = document,
+                })
+                .UsePlatformDetect()
+                .StartWithClassicDesktopLifetime(Array.Empty<string>());
+        }
+    }
+}

+ 128 - 0
QuestPDF.Previewer/DocumentRenderer.cs

@@ -0,0 +1,128 @@
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using Avalonia.Threading;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+using SkiaSharp;
+
+namespace QuestPDF.Previewer
+{
+    public record RenderedPageInfo(SKPicture Picture, Size Size);
+
+    internal class DocumentRenderer : INotifyPropertyChanged
+    {
+        public float PageSpacing { get; set; }
+        public Size Bounds { get; private set; }
+        public IDocument? Document { get; private set; }
+
+        public event PropertyChangedEventHandler? PropertyChanged;
+
+        private ObservableCollection<RenderedPageInfo> _pages = new();
+        public ObservableCollection<RenderedPageInfo> Pages
+        {
+            get => _pages;
+            set
+            {
+                if (_pages != value)
+                {
+                    _pages = value;
+                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Pages)));
+                }
+            }
+        }
+
+        private bool _isRendering;
+        public bool IsRendering
+        {
+            get => _isRendering;
+            private set
+            {
+                if (_isRendering != value)
+                {
+                    _isRendering = value;
+                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRendering)));
+                }
+            }
+        }
+
+        public void UpdateDocument(IDocument? document)
+        {
+            Document = document;
+            if (document != null)
+            {
+                try
+                {
+                    IsRendering = true;
+                    RenderDocument(document);
+                }
+                catch (Exception ex)
+                {
+                    RenderDocument(CreateExceptionDocument(ex));
+                }
+                finally
+                {
+                    IsRendering = false;
+                }
+            }
+        }
+
+        private void RenderDocument(IDocument document)
+        {
+            var canvas = new PreviewerCanvas();
+
+            DocumentGenerator.RenderDocument(canvas, new SizeTrackingCanvas(), document, s =>
+            {
+                var width = s.PageSizes.Max(p => p.Width);
+                var height = s.PageSizes.Sum(p => p.Height) + ((s.PageSizes.Count - 1) * PageSpacing);
+                Bounds = new Size(width, height);
+            });
+
+            foreach (var pages in Pages)
+                pages?.Picture?.Dispose();
+            Dispatcher.UIThread.Post(() =>
+            {
+                Pages.Clear();
+                Pages = new ObservableCollection<RenderedPageInfo>(canvas.Pictures);
+            });
+        }
+
+        private static IDocument CreateExceptionDocument(Exception exception)
+        {
+            return Fluent.Document.Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Size(PageSizes.A4);
+                    page.Margin(1, Unit.Inch);
+                    page.PageColor(Colors.Red.Lighten4);
+                    page.DefaultTextStyle(x => x.FontSize(16));
+
+                    page.Header()
+                        .BorderBottom(2)
+                        .BorderColor(Colors.Red.Medium)
+                        .PaddingBottom(5)
+                        .Text("Ooops! Something went wrong...").FontSize(28).FontColor(Colors.Red.Medium).Bold();
+
+                    page.Content().PaddingVertical(20).Column(column =>
+                    {
+                        var currentException = exception;
+
+                        while (currentException != null)
+                        {
+                            column.Item().Text(exception.GetType().Name).FontSize(20).SemiBold();
+                            column.Item().Text(exception.Message).FontSize(14);
+                            column.Item().PaddingTop(10).Text(exception.StackTrace).FontSize(10).Light();
+
+                            currentException = currentException.InnerException;
+
+                            if (currentException != null)
+                                column.Item().PaddingVertical(15).LineHorizontal(2).LineColor(Colors.Red.Medium);
+                        }
+                    });
+                });
+            });
+        }
+    }
+}

+ 46 - 0
QuestPDF.Previewer/FluentWindow.cs

@@ -0,0 +1,46 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Platform;
+using Avalonia.Styling;
+
+namespace QuestPDF.Previewer
+{
+    internal class FluentWindow : Window, IStyleable
+    {
+        Type IStyleable.StyleKey => typeof(Window);
+
+        public FluentWindow()
+        {
+            ExtendClientAreaToDecorationsHint = true;
+            ExtendClientAreaTitleBarHeightHint = -1;
+
+            TransparencyLevelHint = WindowTransparencyLevel.AcrylicBlur;
+
+            this.GetObservable(WindowStateProperty)
+              .Subscribe(x =>
+              {
+                  PseudoClasses.Set(":maximized", x == WindowState.Maximized);
+                  PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
+              });
+
+            this.GetObservable(IsExtendedIntoWindowDecorationsProperty)
+              .Subscribe(x =>
+              {
+                  if (!x)
+                  {
+                      SystemDecorations = SystemDecorations.Full;
+                      TransparencyLevelHint = WindowTransparencyLevel.Blur;
+                  }
+              });
+        }
+
+        protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+        {
+            base.OnApplyTemplate(e);
+            ExtendClientAreaChromeHints =
+              ExtendClientAreaChromeHints.PreferSystemChrome |
+              ExtendClientAreaChromeHints.OSXThickTitleBar;
+        }
+    }
+}

+ 17 - 0
QuestPDF.Previewer/HotReloadManager.cs

@@ -0,0 +1,17 @@
+[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);
+        }
+    }
+}

BIN
QuestPDF.Previewer/Images/Logo.png


+ 7 - 0
QuestPDF.Previewer/PreviewerApp.axaml

@@ -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>

+ 29 - 0
QuestPDF.Previewer/PreviewerApp.axaml.cs

@@ -0,0 +1,29 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Previewer
+{
+    internal class PreviewerApp : Application
+    {
+        public IDocument? Document { get; init; }
+
+        public override void Initialize()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        public override void OnFrameworkInitializationCompleted()
+        {
+            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+            {
+                desktop.MainWindow = new PreviewerWindow()
+                {
+                    Document = Document
+                };
+            }
+            base.OnFrameworkInitializationCompleted();
+        }
+    }
+}

+ 43 - 0
QuestPDF.Previewer/PreviewerCanvas.cs

@@ -0,0 +1,43 @@
+using QuestPDF.Drawing;
+using QuestPDF.Infrastructure;
+using SkiaSharp;
+
+namespace QuestPDF.Previewer
+{
+    internal sealed class PreviewerCanvas : SkiaCanvasBase, IRenderingCanvas
+    {
+        private SKPictureRecorder? _currentRecorder;
+
+        private readonly List<RenderedPageInfo> _pictures = new();
+        public IReadOnlyList<RenderedPageInfo> Pictures => _pictures;
+
+        private Size? _currentSize;
+
+        public override void BeginDocument()
+        {
+            _pictures.Clear();
+        }
+
+        public override void BeginPage(Size size)
+        {
+            _currentSize = size;
+            _currentRecorder = new SKPictureRecorder();
+            Canvas = _currentRecorder.BeginRecording(new SKRect(0, 0, size.Width, size.Height));
+
+            using var paint = new SKPaint() { Color = SKColors.White };
+            Canvas.DrawRect(0, 0, size.Width, size.Height, paint);
+        }
+
+        public override void EndPage()
+        {
+            var picture = _currentRecorder?.EndRecording();
+            if (picture != null && _currentSize.HasValue)
+                _pictures.Add(new RenderedPageInfo(picture, _currentSize.Value));
+
+            _currentRecorder?.Dispose();
+            _currentRecorder = null;
+        }
+
+        public override void EndDocument() { }
+    }
+}

+ 44 - 0
QuestPDF.Previewer/PreviewerPageControl.cs

@@ -0,0 +1,44 @@
+using System.Reactive.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace QuestPDF.Previewer
+{
+    internal class PreviewerPageControl : Control
+    {
+        public static readonly StyledProperty<RenderedPageInfo?> PageProperty =
+            AvaloniaProperty.Register<PreviewerPageControl, RenderedPageInfo?>(nameof(Page));
+
+        public RenderedPageInfo? Page
+        {
+            get => GetValue(PageProperty);
+            set => SetValue(PageProperty, value);
+        }
+
+        public PreviewerPageControl()
+        {
+            PageProperty.Changed.Take(1).Subscribe(p =>
+            {
+                var size = p.NewValue.Value?.Size ?? Infrastructure.Size.Zero;
+                Width = size.Width;
+                Height = size.Height;
+            });
+
+            ClipToBounds = true;
+        }
+
+        public override void Render(DrawingContext context)
+        {
+            base.Render(context);
+
+            //context.DrawRectangle(Brushes.Red, null, new Rect(0, 0, Bounds.Width, Bounds.Height));
+
+            var picture = Page?.Picture;
+            if (picture != null)
+                context.Custom(new SkCustomDrawOperation(
+                    new Rect(0, 0, Bounds.Width, Bounds.Height),
+                    c => c.DrawPicture(picture)));
+        }
+    }
+}

+ 43 - 0
QuestPDF.Previewer/PreviewerUtils.cs

@@ -0,0 +1,43 @@
+using System.Diagnostics;
+using Avalonia.Controls;
+using QuestPDF.Fluent;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Previewer
+{
+    internal static class PreviewerUtils
+    {
+        public static async Task<bool> SavePdfWithDialog(IDocument? document, Window dialogOwner)
+        {
+            if (document == null)
+                return false;
+            
+            var dialog = new SaveFileDialog()
+            {
+                DefaultExtension = ".pdf",
+                InitialFileName = document.GetMetadata().Title ?? "Document",
+                Filters = new List<FileDialogFilter>()
+                {
+                    new FileDialogFilter()
+                    {
+                        Extensions = new List<string>() { "pdf" },
+                    }
+                }
+            };
+
+            var filePath = await dialog.ShowAsync(dialogOwner);
+            if (string.IsNullOrWhiteSpace(filePath))
+                return false;
+
+            var dirPath = Path.GetDirectoryName(filePath);
+
+            if (!Directory.Exists(dirPath))
+                return false;
+
+            //TODO Catch layout exceptions.
+            document.GeneratePdf(filePath);
+            Process.Start("explorer.exe", filePath);
+            return true;
+        }
+    }
+}

+ 60 - 0
QuestPDF.Previewer/PreviewerWindow.axaml

@@ -0,0 +1,60 @@
+<FluentWindow 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"
+							Icon="/Images/Logo.png"
+							UseLayoutRounding="True"
+							Background="{x:Null}"
+							x:Name="Window"
+							Title="QuestPDF Document Preview">
+	<FluentWindow.Styles>
+		<Style Selector="TitleBar:fullscreen">
+			<Setter Property="Background" Value="#7F000000" />
+		</Style>
+	</FluentWindow.Styles>
+
+	<Panel>
+		<ExperimentalAcrylicBorder IsHitTestVisible="False">
+			<ExperimentalAcrylicBorder.Material>
+				<ExperimentalAcrylicMaterial TintColor="Black" MaterialOpacity="0.75" TintOpacity="1" />
+			</ExperimentalAcrylicBorder.Material>
+		</ExperimentalAcrylicBorder>
+
+		<Grid Margin="10,40,10,0">
+			<Grid.RowDefinitions>
+				<RowDefinition Height="40"/>
+				<RowDefinition Height="Auto"/>
+				<RowDefinition Height="*"/>
+			</Grid.RowDefinitions>
+
+			<DockPanel LastChildFill="False" VerticalAlignment="Center" Margin="20,0">
+				<TextBlock VerticalAlignment="Center" Text="QuestPDF Document Preview" DockPanel.Dock="Left" FontWeight="SemiBold" />
+				<Button x:Name="GeneratePdf" Content="Generate PDF" DockPanel.Dock="Right" />
+			</DockPanel>
+
+			<ProgressBar IsVisible="{Binding #Window.DocumentRenderer.IsRendering}"
+									 Margin="20,0"
+									 Grid.Row="1"
+									 HorizontalAlignment="Stretch"									 
+									 Height="5"
+								   IsIndeterminate="True"/>
+
+			<ScrollViewer Grid.Row="2" VerticalAlignment="Top">
+				<ItemsRepeater HorizontalAlignment="Center" Margin="0,10" Grid.Row="1" VerticalAlignment="Top" Items="{Binding #Window.DocumentRenderer.Pages, Mode=OneWay}">
+					<ItemsRepeater.Layout>
+						<UniformGridLayout MaximumRowsOrColumns="1" Orientation="Horizontal" MinRowSpacing="25" MinColumnSpacing="25" />
+					</ItemsRepeater.Layout>
+					<ItemsRepeater.ItemTemplate>
+						<DataTemplate>
+							<previewer:PreviewerPageControl Page="{Binding}" />
+						</DataTemplate>
+					</ItemsRepeater.ItemTemplate>
+				</ItemsRepeater>
+			</ScrollViewer>
+		</Grid>
+	</Panel>
+</FluentWindow>

+ 47 - 0
QuestPDF.Previewer/PreviewerWindow.axaml.cs

@@ -0,0 +1,47 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Previewer
+{
+    internal class PreviewerWindow : FluentWindow
+    {
+        public DocumentRenderer DocumentRenderer { get; } = new();
+
+        public static readonly StyledProperty<IDocument?> DocumentProperty =
+            AvaloniaProperty.Register<PreviewerWindow, IDocument?>(nameof(Document));
+
+        public IDocument? Document
+        {
+            get => GetValue(DocumentProperty);
+            set => SetValue(DocumentProperty, value);
+        }
+
+        public PreviewerWindow()
+        {
+            InitializeComponent();
+
+            this.FindControl<Button>("GeneratePdf")
+                .Click += (_, _) => _ = PreviewerUtils.SavePdfWithDialog(Document, this);
+
+            DocumentProperty.Changed.Subscribe(v => Task.Run(() => DocumentRenderer.UpdateDocument(v.NewValue.Value)));
+            HotReloadManager.UpdateApplicationRequested += (_, _) => InvalidatePreview();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void InvalidatePreview()
+        {
+            Dispatcher.UIThread.Post(() =>
+            {
+                var document = Document;
+                _ = Task.Run(() => DocumentRenderer.UpdateDocument(document));
+            });
+        }
+    }
+}

+ 27 - 0
QuestPDF.Previewer/QuestPDF.Previewer.csproj

@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <AvaloniaResource Include="Images\Logo.png" />
+  </ItemGroup>
+
+	<ItemGroup>
+		<PackageReference Include="Avalonia" Version="0.10.13" />
+		<PackageReference Include="Avalonia.Desktop" Version="0.10.13" />
+		<PackageReference Include="Avalonia.Diagnostics" Version="0.10.13" />
+		<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.13" />
+		<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.13" />
+		<PackageReference Include="ReactiveUI" Version="17.1.50" />
+		<PackageReference Include="System.Reactive" Version="5.0.0" />
+	</ItemGroup>
+
+	<ItemGroup>
+	  <ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
+	</ItemGroup>
+
+</Project>

+ 39 - 0
QuestPDF.Previewer/SkCustomDrawOperation.cs

@@ -0,0 +1,39 @@
+using Avalonia;
+using Avalonia.Platform;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Skia;
+using SkiaSharp;
+
+namespace QuestPDF.Previewer
+{
+    internal sealed class SkCustomDrawOperation : ICustomDrawOperation
+    {
+        private readonly Action<SKCanvas> _renderFunc;
+        public Rect Bounds { get; }
+
+        public SkCustomDrawOperation(Rect bounds, Action<SKCanvas> renderFunc)
+        {
+            Bounds = bounds;
+            _renderFunc = renderFunc;
+        }
+
+        public void Dispose() { }
+        public bool Equals(ICustomDrawOperation? other)
+        {
+            return false;
+        }
+        public bool HitTest(Point p)
+        {
+            return false;
+        }
+
+        public void Render(IDrawingContextImpl context)
+        {
+            var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
+            if (canvas == null)
+                throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}");
+
+            _renderFunc(canvas);
+        }
+    }
+}

+ 17 - 0
QuestPDF.sln

@@ -8,6 +8,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.UnitTests", "Quest
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Examples", "QuestPDF.Examples\QuestPDF.Examples.csproj", "{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Examples", "QuestPDF.Examples\QuestPDF.Examples.csproj", "{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer", "QuestPDF.Previewer\QuestPDF.Previewer.csproj", "{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}"
+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
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		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}.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.ActiveCfg = Release|Any CPU
 		{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.Build.0 = Release|Any CPU
 		{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.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
 	EndGlobalSection
 	EndGlobalSection
 EndGlobal
 EndGlobal

+ 1 - 0
QuestPDF/Assembly.cs

@@ -1,6 +1,7 @@
 using System.Runtime.CompilerServices;
 using System.Runtime.CompilerServices;
 
 
 [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
 [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
+[assembly: InternalsVisibleTo("QuestPDF.Previewer")]
 [assembly: InternalsVisibleTo("QuestPDF.UnitTests")]
 [assembly: InternalsVisibleTo("QuestPDF.UnitTests")]
 [assembly: InternalsVisibleTo("QuestPDF.Examples")]
 [assembly: InternalsVisibleTo("QuestPDF.Examples")]
 [assembly: InternalsVisibleTo("QuestPDF.ReportSample")]
 [assembly: InternalsVisibleTo("QuestPDF.ReportSample")]

+ 10 - 2
QuestPDF/Drawing/DocumentGenerator.cs

@@ -51,8 +51,15 @@ namespace QuestPDF.Drawing
             return canvas.Images;
             return canvas.Images;
         }
         }
 
 
-        private static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
+        internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
             where TCanvas : ICanvas, IRenderingCanvas
             where TCanvas : ICanvas, IRenderingCanvas
+        {
+            RenderDocument(canvas, new FreeCanvas(), document);
+        }
+
+        internal static void RenderDocument<TCanvas, TFreeCanvas>(TCanvas canvas, TFreeCanvas freeCanvas, IDocument document, Action<TFreeCanvas>? afterFirstPass = null)
+            where TCanvas : ICanvas, IRenderingCanvas
+            where TFreeCanvas : ICanvas, IRenderingCanvas
         {
         {
             var container = new DocumentContainer();
             var container = new DocumentContainer();
             document.Compose(container);
             document.Compose(container);
@@ -67,7 +74,8 @@ namespace QuestPDF.Drawing
             if (metadata.ApplyCaching)
             if (metadata.ApplyCaching)
                 ApplyCaching(content);
                 ApplyCaching(content);
 
 
-            RenderPass(pageContext, new FreeCanvas(), content, metadata, debuggingState);
+            RenderPass(pageContext, freeCanvas, content, metadata, debuggingState);
+            afterFirstPass?.Invoke(freeCanvas);
             RenderPass(pageContext, canvas, content, metadata, debuggingState);
             RenderPass(pageContext, canvas, content, metadata, debuggingState);
         }
         }
         
         

+ 2 - 1
QuestPDF/Drawing/FontManager.cs

@@ -67,7 +67,8 @@ namespace QuestPDF.Drawing
                 {
                 {
                     Color = SKColor.Parse(style.Color),
                     Color = SKColor.Parse(style.Color),
                     Typeface = GetTypeface(style),
                     Typeface = GetTypeface(style),
-                    TextSize = style.Size ?? 12
+                    TextSize = style.Size ?? 12,
+                    IsAntialias = true,
                 };
                 };
             }
             }
 
 

+ 1 - 1
QuestPDF/Drawing/FreeCanvas.cs

@@ -17,7 +17,7 @@ namespace QuestPDF.Drawing
             
             
         }
         }
 
 
-        public void BeginPage(Size size)
+        public virtual void BeginPage(Size size)
         {
         {
             
             
         }
         }

+ 16 - 0
QuestPDF/Drawing/SizeTrackingCanvas.cs

@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Drawing
+{
+    internal class SizeTrackingCanvas : FreeCanvas
+    {
+        private readonly List<Size> _pageSizes = new();
+        public IReadOnlyList<Size> PageSizes => _pageSizes;
+
+        public override void BeginPage(Size size)
+        {
+            _pageSizes.Add(size);
+        }
+    }
+}