Browse Source

2024.3.0 release (removed SkiaSharp dependency, library based on a custom QuestPDF-specific Skia build) (#837)

Version 2024.3.0

The primary theme of this release is the removal of the SkiaSharp dependency and the introduction of a custom native layer built on top of Skia M124.


This change was necessary to provide much higher flexibility and enable several new long-awaited features:
- Font subsetting - this feature includes only necessary font glyphs in the output document, greatly reducing the file size, especially when using glyph-rich fonts supporting multiple languages,
- Improved text-related capabilities, including but not limited to text justification, bi-directionality, enhanced line breaking algorithms, more styles for text decoration, word spacing, and more,
- Introduced additional document compression to further reduce file size,
- Integrated native SVG support,
- In future releases: enhanced accessibility through support for PDF tags.


Other changes:
- Removed the SkiaSharp dependency, thereby making QuestPDF a standalone library,
- Updated the Lato font to the latest version (2.015),
- Included licenses of the third-party dependencies in the NuGet package,
- Included a list of Contributors in the NuGet package to acknowledge the valuable input from our Community in improving QuestPDF.


Unfortunately, this release also introduces breaking changes:
- Removed support for injecting SkiaSharp content directly into the document. It is still possible to integrate SkiaSharp through vector and raster graphics; please consult the documentation for more details,
- Removed support for platforms: Android, iOS, UWP, WASM, and Linux-Alpine. We are currently investigating reintroducing support for WASM and Linux-Alpine in future releases.

We would like to thank the SkiaSharp project, its maintainers, and contributors, for creating a fantastic graphics library. It was a fantastic foundation for QuestPDF for over 3 years.
Marcin Ziąbek 1 year ago
parent
commit
1b9c81bb4e
100 changed files with 1480 additions and 2231 deletions
  1. 3 4
      Source/QuestPDF.Examples/CanvasExamples.cs
  2. 4 4
      Source/QuestPDF.Examples/ChartExamples.cs
  3. 35 21
      Source/QuestPDF.Examples/ContentDirectionExamples.cs
  4. 1 0
      Source/QuestPDF.Examples/ContinousPage.cs
  5. 1 1
      Source/QuestPDF.Examples/DynamicFibonacci.cs
  6. 17 16
      Source/QuestPDF.Examples/ElementExamples.cs
  7. 16 4
      Source/QuestPDF.Examples/Engine/RenderingTest.cs
  8. 4 4
      Source/QuestPDF.Examples/Engine/SimpleDocument.cs
  9. 1 1
      Source/QuestPDF.Examples/FrameExample.cs
  10. 63 61
      Source/QuestPDF.Examples/GenerationBenchmark.cs
  11. 5 5
      Source/QuestPDF.Examples/ImageExamples.cs
  12. 2 1
      Source/QuestPDF.Examples/LoremPicsumExample.cs
  13. 22 21
      Source/QuestPDF.Examples/Padding.cs
  14. 1 1
      Source/QuestPDF.Examples/PageBackgroundForegroundExample.cs
  15. 2 2
      Source/QuestPDF.Examples/PlaceholderExamples.cs
  16. 3 3
      Source/QuestPDF.Examples/QuestPDF.Examples.csproj
  17. 25 0
      Source/QuestPDF.Examples/SkiaSharpHelpers.cs
  18. 1 1
      Source/QuestPDF.Examples/StopPaging.cs
  19. 1 17
      Source/QuestPDF.Examples/SvgImageExample.cs
  20. 2 1
      Source/QuestPDF.Examples/TableExamples.cs
  21. 1 1
      Source/QuestPDF.Examples/TextBenchmark.cs
  22. 94 72
      Source/QuestPDF.Examples/TextExamples.cs
  23. 2 1
      Source/QuestPDF.LayoutTests/QuestPDF.LayoutTests.csproj
  24. 2 2
      Source/QuestPDF.LayoutTests/TestEngine/AnnotateInvalidAreaHelper.cs
  25. 19 14
      Source/QuestPDF.LayoutTests/TestEngine/LayoutTestOutputVisualization.cs
  26. 3 3
      Source/QuestPDF.LayoutTests/TestEngine/MockChild.cs
  27. 32 10
      Source/QuestPDF.Previewer/CommunicationService.cs
  28. 0 19
      Source/QuestPDF.Previewer/DocumentSnapshot.cs
  29. 0 20
      Source/QuestPDF.Previewer/Helpers.cs
  30. 111 20
      Source/QuestPDF.Previewer/InteractiveCanvas.cs
  31. 28 0
      Source/QuestPDF.Previewer/Models.cs
  32. 15 15
      Source/QuestPDF.Previewer/PreviewerControl.cs
  33. 2 36
      Source/QuestPDF.Previewer/PreviewerWindow.axaml
  34. 1 37
      Source/QuestPDF.Previewer/PreviewerWindowViewModel.cs
  35. 8 9
      Source/QuestPDF.Previewer/QuestPDF.Previewer.csproj
  36. 35 1
      Source/QuestPDF.ReportSample/Helpers.cs
  37. 1 0
      Source/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs
  38. 2 0
      Source/QuestPDF.ReportSample/Layouts/StandardReport.cs
  39. 15 9
      Source/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs
  40. 2 2
      Source/QuestPDF.ReportSample/QuestPDF.ReportSample.csproj
  41. 82 0
      Source/QuestPDF.UnitTests/DocumentCompressionTests.cs
  42. 7 7
      Source/QuestPDF.UnitTests/DynamicImageTests.cs
  43. 0 146
      Source/QuestPDF.UnitTests/FontStyleSetTests.cs
  44. 2 2
      Source/QuestPDF.UnitTests/ImageTests.cs
  45. 2 2
      Source/QuestPDF.UnitTests/QuestPDF.UnitTests.csproj
  46. 19 6
      Source/QuestPDF.UnitTests/TestEngine/MockCanvas.cs
  47. 17 5
      Source/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs
  48. 2 2
      Source/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawRectangleOperation.cs
  49. 2 2
      Source/QuestPDF.UnitTests/TestEngine/TestPlan.cs
  50. 9 20
      Source/QuestPDF.UnitTests/TextStyleTests.cs
  51. 14 0
      Source/QuestPDF/Build/net4/QuestPDF.targets
  52. 3 16
      Source/QuestPDF/Drawing/DocumentGenerator.cs
  53. 5 0
      Source/QuestPDF/Drawing/Exceptions/InitializationException.cs
  54. 14 234
      Source/QuestPDF/Drawing/FontManager.cs
  55. 0 132
      Source/QuestPDF/Drawing/FontStyleSet.cs
  56. 50 4
      Source/QuestPDF/Drawing/FreeCanvas.cs
  57. 26 14
      Source/QuestPDF/Drawing/ImageCanvas.cs
  58. 32 24
      Source/QuestPDF/Drawing/PdfCanvas.cs
  59. 19 7
      Source/QuestPDF/Drawing/PreviewerCanvas.cs
  60. 67 23
      Source/QuestPDF/Drawing/SkiaCanvasBase.cs
  61. 3 3
      Source/QuestPDF/Drawing/SkiaDocumentCanvasBase.cs
  62. 1 2
      Source/QuestPDF/Drawing/SpacePlan.cs
  63. 0 224
      Source/QuestPDF/Drawing/TextShaper.cs
  64. 4 5
      Source/QuestPDF/Drawing/XpsCanvas.cs
  65. 1 1
      Source/QuestPDF/Elements/AspectRatio.cs
  66. 2 2
      Source/QuestPDF/Elements/Background.cs
  67. 5 5
      Source/QuestPDF/Elements/Border.cs
  68. 0 38
      Source/QuestPDF/Elements/Canvas.cs
  69. 3 3
      Source/QuestPDF/Elements/DebugArea.cs
  70. 27 10
      Source/QuestPDF/Elements/DynamicImage.cs
  71. 30 0
      Source/QuestPDF/Elements/DynamicSvgImage.cs
  72. 3 3
      Source/QuestPDF/Elements/Image.cs
  73. 10 79
      Source/QuestPDF/Elements/LayoutOverflowVisualization.cs
  74. 3 3
      Source/QuestPDF/Elements/Line.cs
  75. 1 1
      Source/QuestPDF/Elements/Page.cs
  76. 4 7
      Source/QuestPDF/Elements/Placeholder.cs
  77. 0 1
      Source/QuestPDF/Elements/Shrink.cs
  78. 22 0
      Source/QuestPDF/Elements/SvgImage.cs
  79. 24 0
      Source/QuestPDF/Elements/SvgPath.cs
  80. 0 1
      Source/QuestPDF/Elements/Table/TableLayoutPlanner.cs
  81. 0 3
      Source/QuestPDF/Elements/Table/TableLayoutValidator.cs
  82. 0 16
      Source/QuestPDF/Elements/Text/Calculation/TextDrawingRequest.cs
  83. 0 45
      Source/QuestPDF/Elements/Text/Calculation/TextLine.cs
  84. 0 10
      Source/QuestPDF/Elements/Text/Calculation/TextLineElement.cs
  85. 0 16
      Source/QuestPDF/Elements/Text/Calculation/TextMeasurementRequest.cs
  86. 0 22
      Source/QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs
  87. 0 165
      Source/QuestPDF/Elements/Text/FontFallback.cs
  88. 2 6
      Source/QuestPDF/Elements/Text/Items/ITextBlockItem.cs
  89. 8 31
      Source/QuestPDF/Elements/Text/Items/TextBlockElement.cs
  90. 2 18
      Source/QuestPDF/Elements/Text/Items/TextBlockHyperlink.cs
  91. 2 16
      Source/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs
  92. 2 19
      Source/QuestPDF/Elements/Text/Items/TextBlockSectionLink.cs
  93. 1 190
      Source/QuestPDF/Elements/Text/Items/TextBlockSpan.cs
  94. 34 0
      Source/QuestPDF/Elements/Text/SkParagraphBuilderPoolManager.cs
  95. 310 169
      Source/QuestPDF/Elements/Text/TextBlock.cs
  96. 1 2
      Source/QuestPDF/Fluent/AlignmentExtensions.cs
  97. 2 5
      Source/QuestPDF/Fluent/BorderExtensions.cs
  98. 1 2
      Source/QuestPDF/Fluent/ConstrainedExtensions.cs
  99. 2 4
      Source/QuestPDF/Fluent/DebugExtensions.cs
  100. 16 19
      Source/QuestPDF/Fluent/ElementExtensions.cs

+ 3 - 4
Source/QuestPDF.Examples/CanvasExamples.cs

@@ -1,4 +1,3 @@
-using Microcharts;
 using NUnit.Framework;
 using NUnit.Framework;
 using QuestPDF.Examples.Engine;
 using QuestPDF.Examples.Engine;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
@@ -26,16 +25,16 @@ namespace QuestPDF.Examples
                         .MinimalBox()
                         .MinimalBox()
                         .Layers(layers =>
                         .Layers(layers =>
                         {
                         {
-                            layers.Layer().Canvas((canvas, size) =>
+                            layers.Layer().SkiaSharpCanvas((canvas, size) =>
                             {
                             {
                                 DrawRoundedRectangle(Colors.White, false);
                                 DrawRoundedRectangle(Colors.White, false);
                                 DrawRoundedRectangle(Colors.Blue.Darken2, true);
                                 DrawRoundedRectangle(Colors.Blue.Darken2, true);
 
 
-                                void DrawRoundedRectangle(string color, bool isStroke)
+                                void DrawRoundedRectangle(Color color, bool isStroke)
                                 {
                                 {
                                     using var paint = new SKPaint
                                     using var paint = new SKPaint
                                     {
                                     {
-                                        Color = SKColor.Parse(color),
+                                        Color = new SKColor(color.Hex),
                                         IsStroke = isStroke,
                                         IsStroke = isStroke,
                                         StrokeWidth = 2,
                                         StrokeWidth = 2,
                                         IsAntialias = true
                                         IsAntialias = true

+ 4 - 4
Source/QuestPDF.Examples/ChartExamples.cs

@@ -70,7 +70,7 @@ namespace QuestPDF.Examples
                                 .Border(1)
                                 .Border(1)
                                 .ExtendHorizontal()
                                 .ExtendHorizontal()
                                 .Height(300)
                                 .Height(300)
-                                .Canvas((canvas, size) =>
+                                .SkiaSharpCanvas((canvas, size) =>
                                 {
                                 {
                                     var chart = new BarChart
                                     var chart = new BarChart
                                     {
                                     {
@@ -79,7 +79,7 @@ namespace QuestPDF.Examples
                                         LabelOrientation = Orientation.Horizontal,
                                         LabelOrientation = Orientation.Horizontal,
                                         ValueLabelOrientation = Orientation.Horizontal,
                                         ValueLabelOrientation = Orientation.Horizontal,
                                         
                                         
-                                        IsAnimated = false,
+                                        IsAnimated = false
                                     };
                                     };
                                     
                                     
                                     chart.DrawContent(canvas, (int)size.Width, (int)size.Height);
                                     chart.DrawContent(canvas, (int)size.Width, (int)size.Height);
@@ -101,7 +101,7 @@ namespace QuestPDF.Examples
                     container
                     container
                         .Background(Colors.White)
                         .Background(Colors.White)
                         .Padding(25)
                         .Padding(25)
-                        .Canvas((canvas, availableSpace) =>
+                        .SkiaSharpCanvas((canvas, availableSpace) =>
                         {
                         {
                             var points = Enumerable
                             var points = Enumerable
                                 .Range(0, 100)
                                 .Range(0, 100)
@@ -109,7 +109,7 @@ namespace QuestPDF.Examples
                                 .ToArray();
                                 .ToArray();
                             
                             
                             using var plot = new Plot();
                             using var plot = new Plot();
-                            plot.Add.Scatter(points, Color.FromHex(Colors.Teal.Medium));
+                            plot.Add.Scatter(points, ScottPlot.Color.FromHex(Colors.Teal.Medium));
                             
                             
                             canvas.ClipRect(new SKRect(0, 0, availableSpace.Width, availableSpace.Height));
                             canvas.ClipRect(new SKRect(0, 0, availableSpace.Width, availableSpace.Height));
                             plot.Render(canvas, (int)availableSpace.Width, (int)availableSpace.Height);
                             plot.Render(canvas, (int)availableSpace.Width, (int)availableSpace.Height);

+ 35 - 21
Source/QuestPDF.Examples/ContentDirectionExamples.cs

@@ -263,32 +263,38 @@ namespace QuestPDF.Examples
                     column.Spacing(10);
                     column.Spacing(10);
 
 
                     column.Item().Text("Default alignment").FontSize(14).SemiBold();
                     column.Item().Text("Default alignment").FontSize(14).SemiBold();
-                    column.Item().Element(ContentWithAlignment(null));
+                    column.Item().Element(ContentWithAlignment(x => { }));
                     
                     
                     column.Item().Text("Left alignment").FontSize(14).SemiBold();
                     column.Item().Text("Left alignment").FontSize(14).SemiBold();
-                    column.Item().Element(ContentWithAlignment(InlinedAlignment.Left));
+                    column.Item().Element(ContentWithAlignment(x => x.AlignLeft()));
                     
                     
                     column.Item().Text("Center alignment").FontSize(14).SemiBold();
                     column.Item().Text("Center alignment").FontSize(14).SemiBold();
-                    column.Item().Element(ContentWithAlignment(InlinedAlignment.Center));
+                    column.Item().Element(ContentWithAlignment(x => x.AlignCenter()));
                     
                     
                     column.Item().Text("Right alignment").FontSize(14).SemiBold();
                     column.Item().Text("Right alignment").FontSize(14).SemiBold();
-                    column.Item().Element(ContentWithAlignment(InlinedAlignment.Right));
+                    column.Item().Element(ContentWithAlignment(x => x.AlignRight()));
+                    
+                    column.Item().Text("Justify alignment").FontSize(14).SemiBold();
+                    column.Item().Element(ContentWithAlignment(x => x.AlignJustify()));
+                    
+                    column.Item().Text("Space around alignment").FontSize(14).SemiBold();
+                    column.Item().Element(ContentWithAlignment(x => x.AlignSpaceAround()));
                 });
                 });
                 
                 
-                static Action<IContainer> ContentWithAlignment(InlinedAlignment? alignment)
+                static Action<IContainer> ContentWithAlignment(Action<InlinedDescriptor> configure)
                 {
                 {
                     return container =>
                     return container =>
                     {
                     {
                         container.Inlined(inlined =>
                         container.Inlined(inlined =>
                         {
                         {
                             inlined.Spacing(5);
                             inlined.Spacing(5);
-                            
-                            inlined.Alignment(alignment);
+
+                            configure(inlined);
                     
                     
-                            inlined.Item().Height(50).Width(50).Background(Colors.Red.Lighten1);
-                            inlined.Item().Height(50).Width(75).Background(Colors.Green.Lighten1);
-                            inlined.Item().Height(50).Width(100).Background(Colors.Blue.Lighten1);
-                            inlined.Item().Height(50).Width(125).Background(Colors.Orange.Lighten1);
+                            inlined.Item().Height(40).Width(50).Background(Colors.Red.Lighten1);
+                            inlined.Item().Height(40).Width(75).Background(Colors.Green.Lighten1);
+                            inlined.Item().Height(40).Width(100).Background(Colors.Blue.Lighten1);
+                            inlined.Item().Height(40).Width(125).Background(Colors.Orange.Lighten1);
                         });
                         });
                     };
                     };
                 }
                 }
@@ -313,29 +319,37 @@ namespace QuestPDF.Examples
                     column.Spacing(10);
                     column.Spacing(10);
 
 
                     column.Item().Text("Default alignment").FontSize(14).SemiBold();
                     column.Item().Text("Default alignment").FontSize(14).SemiBold();
-                    column.Item().Element(ContentWithAlignment(null));
+                    column.Item().Element(ContentWithAlignment(x => { }));
                     
                     
                     column.Item().Text("Left alignment").FontSize(14).SemiBold();
                     column.Item().Text("Left alignment").FontSize(14).SemiBold();
-                    column.Item().Element(ContentWithAlignment(HorizontalAlignment.Left));
+                    column.Item().Element(ContentWithAlignment(x => x.AlignLeft()));
                     
                     
                     column.Item().Text("Center alignment").FontSize(14).SemiBold();
                     column.Item().Text("Center alignment").FontSize(14).SemiBold();
-                    column.Item().Element(ContentWithAlignment(HorizontalAlignment.Center));
+                    column.Item().Element(ContentWithAlignment(x => x.AlignCenter()));
                     
                     
                     column.Item().Text("Right alignment").FontSize(14).SemiBold();
                     column.Item().Text("Right alignment").FontSize(14).SemiBold();
-                    column.Item().Element(ContentWithAlignment(HorizontalAlignment.Right));
+                    column.Item().Element(ContentWithAlignment(x => x.AlignRight()));
+                    
+                    column.Item().Text("Justify alignment").FontSize(14).SemiBold();
+                    column.Item().Element(ContentWithAlignment(x => x.Justify()));
+                    
+                    column.Item().Text("Start alignment").FontSize(14).SemiBold();
+                    column.Item().Element(ContentWithAlignment(x => x.AlignStart()));
+                    
+                    column.Item().Text("End alignment").FontSize(14).SemiBold();
+                    column.Item().Element(ContentWithAlignment(x => x.AlignEnd()));
                 });
                 });
 
 
-                static Action<IContainer> ContentWithAlignment(HorizontalAlignment? alignment)
+                static Action<IContainer> ContentWithAlignment(Action<TextDescriptor> configure)
                 {
                 {
                     return container =>
                     return container =>
                     {
                     {
                         container.Text(text =>
                         container.Text(text =>
                         {
                         {
-                            text.Alignment = alignment; // internal API
-                    
-                            text.Span("Lorem ipsum").Bold().FontColor(Colors.Red.Medium);
-                            text.Element().Width(5);
-                            text.Span(Placeholders.LoremIpsum());
+                            configure(text);
+
+                            text.Span("Dotnet").Bold().FontColor(Colors.Red.Medium);
+                            text.Span(" is an open-source platform for building desktop, web, and mobile applications that can run natively on any operating system.");
                         });
                         });
                     };
                     };
                 }
                 }

+ 1 - 0
Source/QuestPDF.Examples/ContinousPage.cs

@@ -12,6 +12,7 @@ namespace QuestPDF.Examples
     public class ContinuousPageDocument : IDocument
     public class ContinuousPageDocument : IDocument
     {
     {
         public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
         public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
+        public DocumentSettings GetSettings() => DocumentSettings.Default;
 
 
         public void Compose(IDocumentContainer container)
         public void Compose(IDocumentContainer container)
         {
         {

+ 1 - 1
Source/QuestPDF.Examples/DynamicFibonacci.cs

@@ -19,7 +19,7 @@ namespace QuestPDF.Examples
     {
     {
         public FibonacciHeaderState State { get; set; }
         public FibonacciHeaderState State { get; set; }
         
         
-        public static readonly string[] ColorsTable =
+        public static readonly Color[] ColorsTable =
         {
         {
             Colors.Red.Lighten2,
             Colors.Red.Lighten2,
             Colors.Orange.Lighten2,
             Colors.Orange.Lighten2,

+ 17 - 16
Source/QuestPDF.Examples/ElementExamples.cs

@@ -20,7 +20,7 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FFF")
+                        .Background(Colors.White)
                         .Padding(25)
                         .Padding(25)
                         .Placeholder();
                         .Placeholder();
                 });
                 });
@@ -35,7 +35,7 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FFF")
+                        .Background(Colors.White)
                         .Padding(25)
                         .Padding(25)
                         .Decoration(decoration =>
                         .Decoration(decoration =>
                         {
                         {
@@ -45,7 +45,7 @@ namespace QuestPDF.Examples
                                 .Padding(10)
                                 .Padding(10)
                                 .Text("Notes")
                                 .Text("Notes")
                                 .FontSize(16)
                                 .FontSize(16)
-                                .FontColor("#FFF");
+                                .FontColor(Colors.White);
                     
                     
                             decoration
                             decoration
                                 .Content()
                                 .Content()
@@ -63,10 +63,11 @@ namespace QuestPDF.Examples
             RenderingTest
             RenderingTest
                 .Create()
                 .Create()
                 .PageSize(740, 200)
                 .PageSize(740, 200)
+                .ShowResults()
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FFF")
+                        .Background(Colors.White)
                         .Padding(20)
                         .Padding(20)
                         .Column(column =>
                         .Column(column =>
                         {
                         {
@@ -106,7 +107,7 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FFF")
+                        .Background(Colors.White)
                         .Padding(20)
                         .Padding(20)
                         .Row(row =>
                         .Row(row =>
                         {
                         {
@@ -127,7 +128,7 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FFF")
+                        .Background(Colors.White)
                         .Padding(15)
                         .Padding(15)
                         .Column(column =>
                         .Column(column =>
                         {
                         {
@@ -177,7 +178,7 @@ namespace QuestPDF.Examples
                         .Element(x =>
                         .Element(x =>
                         {
                         {
                             if (string.IsNullOrWhiteSpace(text))
                             if (string.IsNullOrWhiteSpace(text))
-                                x.Height(10).Width(50).Background("#DDD");
+                                x.Height(10).Width(50).Background(Colors.Grey.Medium);
                             else
                             else
                                 x.Text(text);
                                 x.Text(text);
                         });
                         });
@@ -227,9 +228,9 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FFF")
+                        .Background(Colors.White)
                         .Padding(25)
                         .Padding(25)
-                        .Canvas((canvas, size) =>
+                        .SkiaSharpCanvas((canvas, size) =>
                         {
                         {
                             using var paint = new SKPaint
                             using var paint = new SKPaint
                             {
                             {
@@ -438,7 +439,7 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FFF")
+                        .Background(Colors.White)
                         .Padding(25)
                         .Padding(25)
                         .Layers(layers =>
                         .Layers(layers =>
                         {
                         {
@@ -450,7 +451,7 @@ namespace QuestPDF.Examples
                                 column.Item().PaddingTop(40).Text("Text 2");
                                 column.Item().PaddingTop(40).Text("Text 2");
                             });
                             });
                     
                     
-                            layers.Layer().Canvas((canvas, size) =>
+                            layers.Layer().SkiaSharpCanvas((canvas, size) =>
                             {
                             {
                                 using var paint = new SKPaint
                                 using var paint = new SKPaint
                                 {
                                 {
@@ -462,7 +463,7 @@ namespace QuestPDF.Examples
                                 canvas.DrawCircle(0, 0, 50, paint);
                                 canvas.DrawCircle(0, 0, 50, paint);
                             });
                             });
                     
                     
-                            layers.Layer().Background("#8F00").Extend();
+                            layers.Layer().Background(Colors.Red.Medium.WithAlpha(128)).Extend();
                             layers.Layer().PaddingTop(40).Text("It works!").FontSize(24);
                             layers.Layer().PaddingTop(40).Text("It works!").FontSize(24);
                         });
                         });
                 });
                 });
@@ -477,7 +478,7 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FFF")
+                        .Background(Colors.White)
                         .Padding(15)
                         .Padding(15)
                         .Border(4)
                         .Border(4)
                         .BorderColor(Colors.Blue.Medium)
                         .BorderColor(Colors.Blue.Medium)
@@ -503,8 +504,8 @@ namespace QuestPDF.Examples
                         {
                         {
                             var headerFontStyle = TextStyle
                             var headerFontStyle = TextStyle
                                 .Default
                                 .Default
-                                .Size(20)
-                                .Color(Colors.Blue.Darken2)
+                                .FontSize(20)
+                                .FontColor(Colors.Blue.Darken2)
                                 .SemiBold();
                                 .SemiBold();
     
     
                             decoration
                             decoration
@@ -550,7 +551,7 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FFF")
+                        .Background(Colors.White)
                         .MinimalBox()
                         .MinimalBox()
                         
                         
                         .Padding(25)
                         .Padding(25)

+ 16 - 4
Source/QuestPDF.Examples/Engine/RenderingTest.cs

@@ -103,9 +103,7 @@ namespace QuestPDF.Examples.Engine
 
 
         public void RenderDocument(Action<IDocumentContainer> content)
         public void RenderDocument(Action<IDocumentContainer> content)
         {
         {
-            MaxPagesThreshold ??= ResultType == RenderingTestResult.Pdf ? 1000 : 10;
-            var document = new SimpleDocument(content, MaxPagesThreshold.Value, ApplyCaching, ApplyDebugging);
-
+            var document = new SimpleDocument(content, ApplyCaching, ApplyDebugging);
             Render(document);
             Render(document);
         }
         }
         
         
@@ -121,7 +119,7 @@ namespace QuestPDF.Examples.Engine
                 if (ShowResult && ShowingResultsEnabled)
                 if (ShowResult && ShowingResultsEnabled)
                 {
                 {
                     var firstImagePath = fileNameSchema(0);
                     var firstImagePath = fileNameSchema(0);
-                    Helpers.Helpers.OpenFileUsingDefaultProgram(firstImagePath);
+                    OpenFileUsingDefaultProgram(firstImagePath);
                 }
                 }
             }
             }
 
 
@@ -130,5 +128,19 @@ namespace QuestPDF.Examples.Engine
                 document.GeneratePdfAndShow();
                 document.GeneratePdfAndShow();
             }
             }
         }
         }
+        
+        static void OpenFileUsingDefaultProgram(string filePath)
+        {
+            var process = new Process
+            {
+                StartInfo = new ProcessStartInfo(filePath)
+                {
+                    UseShellExecute = true
+                }
+            };
+
+            process.Start();
+            process.WaitForExit();
+        }
     }
     }
 }
 }

+ 4 - 4
Source/QuestPDF.Examples/Engine/SimpleDocument.cs

@@ -5,17 +5,17 @@ namespace QuestPDF.Examples.Engine
 {
 {
     public class SimpleDocument : IDocument
     public class SimpleDocument : IDocument
     {
     {
+        public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
+        public DocumentSettings GetSettings() => DocumentSettings.Default;
+        
         private Action<IDocumentContainer> Content { get; }
         private Action<IDocumentContainer> Content { get; }
-        private int MaxPages { get; }
 
 
-        public SimpleDocument(Action<IDocumentContainer> content, int maxPages, bool applyCaching, bool applyDebugging)
+        public SimpleDocument(Action<IDocumentContainer> content, bool applyCaching, bool applyDebugging)
         {
         {
             Content = content;
             Content = content;
-            MaxPages = maxPages;
 
 
             QuestPDF.Settings.EnableCaching = applyCaching;
             QuestPDF.Settings.EnableCaching = applyCaching;
             QuestPDF.Settings.EnableDebugging = applyDebugging;
             QuestPDF.Settings.EnableDebugging = applyDebugging;
-            QuestPDF.Settings.DocumentLayoutExceptionThreshold = MaxPages;
         }
         }
 
 
         public void Compose(IDocumentContainer container)
         public void Compose(IDocumentContainer container)

+ 1 - 1
Source/QuestPDF.Examples/FrameExample.cs

@@ -34,7 +34,7 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FFF")
+                        .Background(Colors.White)
                         .Padding(25)
                         .Padding(25)
                         .Column(column =>
                         .Column(column =>
                         {
                         {

+ 63 - 61
Source/QuestPDF.Examples/GenerationBenchmark.cs

@@ -65,75 +65,77 @@ namespace QuestPDF.Examples
         
         
         static ProcessRunningTime GenerateAndCollect(int attemptNumber)
         static ProcessRunningTime GenerateAndCollect(int attemptNumber)
         {
         {
-            var stopwatch = new Stopwatch();
-            stopwatch.Start();
-            
-            var container = new Container();
-            
-            container
-                .Padding(10)
-                .MinimalBox()
-                .Border(1)
-                .Column(column =>
+            var fluentTime = TimeSpan.Zero;
+
+            var document = Document.Create(document =>
+            {
+                document.Page(page =>
                 {
                 {
-                    column.Item().Text($"Attempts {attemptNumber}");
-                    
-                    const int numberOfRows = 100;
-                    const int numberOfColumns = 10;
-
-                    for (var y = 0; y < numberOfRows; y++)
-                    {
-                        column.Item().Row(row =>
+                    page.Content()
+                        .Padding(10)
+                        .Shrink()
+                        .Border(1)
+                        .Column(column =>
                         {
                         {
-                            for (var x = 0; x < numberOfColumns; x++)
+                            var fluentTimeStopwatch = new Stopwatch();
+                            fluentTimeStopwatch.Start();
+                            
+                            column.Item().Text($"Attempts {attemptNumber}");
+
+                            const int numberOfRows = 100;
+                            const int numberOfColumns = 10;
+
+                            for (var y = 0; y < numberOfRows; y++)
                             {
                             {
-                                row.RelativeItem()
-                                    
-                                    .Background(Colors.Red.Lighten5)
-                                    .Padding(3)
-                                    
-                                    .Background(Colors.Red.Lighten4)
-                                    .Padding(3)
-                                    
-                                    .Background(Colors.Red.Lighten3)
-                                    .Padding(3)
-                                    
-                                    .Background(Colors.Red.Lighten2)
-                                    .Padding(3)
-                                    
-                                    .Background(Colors.Red.Lighten1)
-                                    .Padding(3)
-                                    
-                                    .Background(Colors.Red.Medium)
-                                    .Padding(3)
-                                    
-                                    .Background(Colors.Red.Darken1)
-                                    .Padding(3)
-                                    
-                                    .Background(Colors.Red.Darken2)
-                                    .Padding(3)
-                                    
-                                    .Background(Colors.Red.Darken3)
-                                    .Padding(3)
-                                    
-                                    .Background(Colors.Red.Darken4)
-                                    .Height(3);
+                                column.Item().Row(row =>
+                                {
+                                    for (var x = 0; x < numberOfColumns; x++)
+                                    {
+                                        row.RelativeItem()
+
+                                            .Background(Colors.Red.Lighten5)
+                                            .Padding(3)
+
+                                            .Background(Colors.Red.Lighten4)
+                                            .Padding(3)
+
+                                            .Background(Colors.Red.Lighten3)
+                                            .Padding(3)
+
+                                            .Background(Colors.Red.Lighten2)
+                                            .Padding(3)
+
+                                            .Background(Colors.Red.Lighten1)
+                                            .Padding(3)
+
+                                            .Background(Colors.Red.Medium)
+                                            .Padding(3)
+
+                                            .Background(Colors.Red.Darken1)
+                                            .Padding(3)
+
+                                            .Background(Colors.Red.Darken2)
+                                            .Padding(3)
+
+                                            .Background(Colors.Red.Darken3)
+                                            .Padding(3)
+
+                                            .Background(Colors.Red.Darken4)
+                                            .Height(3);
+                                    }
+                                });
                             }
                             }
+                            
+                            fluentTime = fluentTimeStopwatch.Elapsed;
                         });
                         });
-                    }  
                 });
                 });
+            });
 
 
-            var fluentTime = stopwatch.Elapsed;
+            var generationTimeStopWatch = new Stopwatch();
             
             
-            stopwatch.Reset();
-            stopwatch.Start();
-
-            var size = Document
-                .Create(x => x.Page(page => page.Content().Element(container)))
-                .GeneratePdf()
-                .Length;
-
-            var generationTime = stopwatch.Elapsed;
+            generationTimeStopWatch.Start();
+            var size = document.GeneratePdf().Length;
+            var generationTime = generationTimeStopWatch.Elapsed;
             
             
             return new ProcessRunningTime
             return new ProcessRunningTime
             {
             {

+ 5 - 5
Source/QuestPDF.Examples/ImageExamples.cs

@@ -77,7 +77,7 @@ namespace QuestPDF.Examples
             RenderingTest
             RenderingTest
                 .Create()
                 .Create()
                 .PageSize(400, 600)
                 .PageSize(400, 600)
-                .ProduceImages()
+                .ProducePdf()
                 .ShowResults()
                 .ShowResults()
                 .Render(page =>
                 .Render(page =>
                 {
                 {
@@ -85,7 +85,7 @@ namespace QuestPDF.Examples
                     {
                     {
                         column.Spacing(15);
                         column.Spacing(15);
                         
                         
-                        column.Item().Image("photo.jpg").WithRasterDpi(16).FitUnproportionally();
+                        column.Item().Image("photo.jpg").WithRasterDpi(16);
                         column.Item().Image("photo.jpg").WithRasterDpi(72);
                         column.Item().Image("photo.jpg").WithRasterDpi(72);
                     });
                     });
                 });
                 });
@@ -97,7 +97,7 @@ namespace QuestPDF.Examples
             RenderingTest
             RenderingTest
                 .Create()
                 .Create()
                 .PageSize(400, 600)
                 .PageSize(400, 600)
-                .ProduceImages()
+                .ProducePdf()
                 .ShowResults()
                 .ShowResults()
                 .Render(page =>
                 .Render(page =>
                 {
                 {
@@ -106,7 +106,7 @@ namespace QuestPDF.Examples
                         column.Spacing(15);
                         column.Spacing(15);
                         
                         
                         column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.VeryLow).WithRasterDpi(72);
                         column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.VeryLow).WithRasterDpi(72);
-                        column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.High).WithRasterDpi(72);
+                        column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.VeryHigh).WithRasterDpi(72);
                     });
                     });
                 });
                 });
         }
         }
@@ -160,7 +160,7 @@ namespace QuestPDF.Examples
         [Test]
         [Test]
         public void Exception()
         public void Exception()
         {
         {
-            Assert.Throws<DocumentComposeException>(() =>
+            Assert.Throws<Exception>(() =>
             {
             {
                 RenderingTest
                 RenderingTest
                     .Create()
                     .Create()

+ 2 - 1
Source/QuestPDF.Examples/LoremPicsumExample.cs

@@ -2,6 +2,7 @@ using System.Net;
 using NUnit.Framework;
 using NUnit.Framework;
 using QuestPDF.Examples.Engine;
 using QuestPDF.Examples.Engine;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Examples
 namespace QuestPDF.Examples
@@ -43,7 +44,7 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FFF")
+                        .Background(Colors.White)
                         .Padding(25)
                         .Padding(25)
                         .Column(column =>
                         .Column(column =>
                         {
                         {

+ 22 - 21
Source/QuestPDF.Examples/Padding.cs

@@ -1,6 +1,7 @@
 using NUnit.Framework;
 using NUnit.Framework;
 using QuestPDF.Examples.Engine;
 using QuestPDF.Examples.Engine;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Examples
 namespace QuestPDF.Examples
@@ -17,16 +18,16 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#FDD")
+                        .Background(Colors.Red.Lighten2)
                         .Padding(50)
                         .Padding(50)
 
 
-                        .Background("#AFA")
+                        .Background(Colors.Green.Lighten2)
                         .PaddingVertical(50)
                         .PaddingVertical(50)
 
 
-                        .Background("#77F")
+                        .Background(Colors.Blue.Lighten2)
                         .PaddingHorizontal(50)
                         .PaddingHorizontal(50)
 
 
-                        .Background("#444");
+                        .Background(Colors.Grey.Darken2);
                 });
                 });
         }
         }
         
         
@@ -39,15 +40,15 @@ namespace QuestPDF.Examples
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
-                        .Background("#EEE")
+                        .Background(Colors.Grey.Lighten3)
                         .Padding(25)
                         .Padding(25)
 
 
                         .AlignBottom()
                         .AlignBottom()
                         .AlignCenter()
                         .AlignCenter()
                         .BorderBottom(2)
                         .BorderBottom(2)
-                        .BorderColor("#000")
+                        .BorderColor(Colors.Black)
                 
                 
-                        .Background("FFF")
+                        .Background(Colors.White)
                         .Padding(5)
                         .Padding(5)
                         .AlignCenter()
                         .AlignCenter()
                         .Text("Sample text")
                         .Text("Sample text")
@@ -69,38 +70,38 @@ namespace QuestPDF.Examples
                             column
                             column
                                 .Item()
                                 .Item()
                                 .Height(100)
                                 .Height(100)
-                                .Background("#FFF")
+                                .Background(Colors.White)
                         
                         
                                 .AlignLeft()
                                 .AlignLeft()
                                 .AlignMiddle()
                                 .AlignMiddle()
 
 
                                 .Width(50)
                                 .Width(50)
                                 .Height(50)
                                 .Height(50)
-                                .Background("#444");
+                                .Background(Colors.Grey.Darken2);
                     
                     
                             column
                             column
                                 .Item()
                                 .Item()
                                 .Height(100)
                                 .Height(100)
-                                .Background("#DDD")
+                                .Background(Colors.Grey.Lighten4)
                         
                         
                                 .AlignCenter()
                                 .AlignCenter()
                                 .AlignMiddle()
                                 .AlignMiddle()
 
 
                                 .Width(50)
                                 .Width(50)
                                 .Height(50)
                                 .Height(50)
-                                .Background("#222");
+                                .Background(Colors.Grey.Darken3);
                     
                     
                             column
                             column
                                 .Item()
                                 .Item()
                                 .Height(100)
                                 .Height(100)
-                                .Background("#BBB")
+                                .Background(Colors.Grey.Lighten3)
                         
                         
                                 .AlignRight()
                                 .AlignRight()
                                 .AlignMiddle()
                                 .AlignMiddle()
 
 
                                 .Width(50)
                                 .Width(50)
                                 .Height(50)
                                 .Height(50)
-                                .Background("#000");
+                                .Background(Colors.Black);
                         });
                         });
                 });
                 });
         }
         }
@@ -123,19 +124,19 @@ namespace QuestPDF.Examples
                                 {
                                 {
                                     row.RelativeItem()
                                     row.RelativeItem()
                                         .Extend()
                                         .Extend()
-                                        .Background("FFF")
+                                        .Background(Colors.White)
 
 
                                         .Height(50)
                                         .Height(50)
                                         .Width(50)
                                         .Width(50)
-                                        .Background("444");
+                                        .Background(Colors.Grey.Darken2);
                             
                             
                                     row.RelativeItem()
                                     row.RelativeItem()
                                         .Extend()
                                         .Extend()
-                                        .Background("BBB")
+                                        .Background(Colors.Grey.Lighten3)
 
 
                                         .Height(50)
                                         .Height(50)
                                         .ExtendHorizontal()
                                         .ExtendHorizontal()
-                                        .Background("444");
+                                        .Background(Colors.Grey.Darken2);
                                 });
                                 });
                     
                     
                             column
                             column
@@ -145,19 +146,19 @@ namespace QuestPDF.Examples
                                 {
                                 {
                                     row.RelativeItem()
                                     row.RelativeItem()
                                         .Extend()
                                         .Extend()
-                                        .Background("BBB")
+                                        .Background(Colors.Grey.Lighten3)
 
 
                                         .ExtendVertical()
                                         .ExtendVertical()
                                         .Width(50)
                                         .Width(50)
-                                        .Background("444");
+                                        .Background(Colors.Grey.Darken2);
                             
                             
                                     row.RelativeItem()
                                     row.RelativeItem()
                                         .Extend()
                                         .Extend()
-                                        .Background("BBB")
+                                        .Background(Colors.Grey.Lighten3)
 
 
                                         .ExtendVertical()
                                         .ExtendVertical()
                                         .ExtendHorizontal()
                                         .ExtendHorizontal()
-                                        .Background("444");
+                                        .Background(Colors.Grey.Darken2);
                                 });
                                 });
                         });
                         });
                 });
                 });

+ 1 - 1
Source/QuestPDF.Examples/PageBackgroundForegroundExample.cs

@@ -27,7 +27,7 @@ namespace QuestPDF.Examples
                         page.DefaultTextStyle(TextStyle.Default.FontSize(16));
                         page.DefaultTextStyle(TextStyle.Default.FontSize(16));
                         page.PageColor(Colors.White);
                         page.PageColor(Colors.White);
 
 
-                        const string transparentBlue = "#662196f3";
+                        var transparentBlue = Colors.LightBlue.Medium.WithAlpha(64);
 
 
                         page.Background()
                         page.Background()
                             .AlignTop()
                             .AlignTop()

+ 2 - 2
Source/QuestPDF.Examples/PlaceholderExamples.cs

@@ -106,13 +106,13 @@ public class PlaceholderExamples
     [Test]
     [Test]
     public void BackgroundColor()
     public void BackgroundColor()
     {
     {
-        PrintRandomValues(Placeholders.BackgroundColor);
+        PrintRandomValues(() => Placeholders.BackgroundColor());
     }
     }
     
     
     [Test]
     [Test]
     public void Color()
     public void Color()
     {
     {
-        PrintRandomValues(Placeholders.Color);
+        PrintRandomValues(() => Placeholders.Color());
     }
     }
 }
 }
     
     

+ 3 - 3
Source/QuestPDF.Examples/QuestPDF.Examples.csproj

@@ -11,10 +11,10 @@
         <PackageReference Include="microcharts" Version="0.9.5.9" />
         <PackageReference Include="microcharts" Version="0.9.5.9" />
         <PackageReference Include="nunit" Version="4.0.1" />
         <PackageReference Include="nunit" Version="4.0.1" />
         <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
         <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
-        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
-        <PackageReference Include="ScottPlot" Version="5.0.21" />
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
+        <PackageReference Include="ScottPlot" Version="5.0.25" />
         <PackageReference Include="SkiaSharp" Version="2.88.7" />
         <PackageReference Include="SkiaSharp" Version="2.88.7" />
-        <PackageReference Include="Svg.Skia" Version="1.0.0.10" />
+        <PackageReference Include="Svg.Skia" Version="1.0.0.16" />
     </ItemGroup>
     </ItemGroup>
 
 
     <ItemGroup>
     <ItemGroup>

+ 25 - 0
Source/QuestPDF.Examples/SkiaSharpHelpers.cs

@@ -0,0 +1,25 @@
+using System;
+using System.IO;
+using System.Text;
+using QuestPDF.Fluent;
+using QuestPDF.Infrastructure;
+using SkiaSharp;
+
+namespace QuestPDF.Examples;
+
+public static class SkiaSharpHelpers
+{
+    public static void SkiaSharpCanvas(this IContainer container, Action<SKCanvas, Size> drawOnCanvas)
+    {
+        container.Svg(size =>
+        {
+            using var stream = new MemoryStream();
+
+            using (var canvas = SKSvgCanvas.Create(new SKRect(0, 0, size.Width, size.Height), stream))
+                drawOnCanvas(canvas, size);
+            
+            var svgData = stream.ToArray();
+            return Encoding.UTF8.GetString(svgData);
+        });
+    }
+}

+ 1 - 1
Source/QuestPDF.Examples/StopPaging.cs

@@ -27,7 +27,7 @@ namespace QuestPDF.Examples
                                 .Before()
                                 .Before()
                                 .Text(text =>
                                 .Text(text =>
                                 {
                                 {
-                                    text.DefaultTextStyle(TextStyle.Default.SemiBold().Color(Colors.Blue.Medium));
+                                    text.DefaultTextStyle(TextStyle.Default.SemiBold().FontColor(Colors.Blue.Medium));
                                     
                                     
                                     text.Span("Page ");
                                     text.Span("Page ");
                                     text.CurrentPageNumber();
                                     text.CurrentPageNumber();

+ 1 - 17
Source/QuestPDF.Examples/SvgImageExample.cs

@@ -23,24 +23,8 @@ namespace QuestPDF.Examples
                 .ShowResults()
                 .ShowResults()
                 .Render(container =>
                 .Render(container =>
                 {
                 {
-                    container
-                        .Padding(25)
-                        .Svg(svg);
+                    container.Svg(SvgImage.FromFile("pdf-icon.svg")).FitArea();
                 });
                 });
         }
         }
     }
     }
-    
-    public static class SvgExtensions
-    {
-        public static void Svg(this IContainer container, SKSvg svg)
-        {
-            container
-                .AlignCenter()
-                .AlignMiddle()
-                .ScaleToFit()
-                .Width(svg.Picture.CullRect.Width)
-                .Height(svg.Picture.CullRect.Height)
-                .Canvas((canvas, space) => canvas.DrawPicture(svg.Picture));
-        }
-    }
 }
 }

+ 2 - 1
Source/QuestPDF.Examples/TableExamples.cs

@@ -11,6 +11,7 @@ using QuestPDF.Examples.Engine;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using Color = QuestPDF.Infrastructure.Color;
 using IContainer = QuestPDF.Infrastructure.IContainer;
 using IContainer = QuestPDF.Infrastructure.IContainer;
 
 
 namespace QuestPDF.Examples
 namespace QuestPDF.Examples
@@ -398,7 +399,7 @@ namespace QuestPDF.Examples
                         .Border(1)
                         .Border(1)
                         .Table(table =>
                         .Table(table =>
                         {
                         {
-                            IContainer DefaultCellStyle(IContainer container, string backgroundColor)
+                            IContainer DefaultCellStyle(IContainer container, Color backgroundColor)
                             {
                             {
                                 return container
                                 return container
                                     .Border(1)
                                     .Border(1)

+ 1 - 1
Source/QuestPDF.Examples/TextBenchmark.cs

@@ -104,7 +104,7 @@ namespace QuestPDF.Examples
         
         
         private void ComposeBook(IDocumentContainer container, ICollection<BookChapter> chapters)
         private void ComposeBook(IDocumentContainer container, ICollection<BookChapter> chapters)
         {
         {
-            var subtitleStyle = TextStyle.Default.Size(24).SemiBold().Color(Colors.Blue.Medium);
+            var subtitleStyle = TextStyle.Default.Size(24).SemiBold().FontColor(Colors.Blue.Medium);
             var normalStyle = TextStyle.Default.Size(14);
             var normalStyle = TextStyle.Default.Size(14);
 
 
             container.Page(page =>
             container.Page(page =>

+ 94 - 72
Source/QuestPDF.Examples/TextExamples.cs

@@ -373,11 +373,10 @@ namespace QuestPDF.Examples
                         .Padding(10)
                         .Padding(10)
                         .Text(text =>
                         .Text(text =>
                         {
                         {
-                            text.DefaultTextStyle(TextStyle.Default.FontSize(20));
-                            text.Span("This is a random image aligned to the baseline: ");
+                            text.DefaultTextStyle(TextStyle.Default.FontSize(25));
+                            text.Span("This is a random image aligned to the middle of the baseline: ");
                             
                             
-                            text.Element()
-                                .PaddingBottom(-6)
+                            text.Element(TextInjectedElementAlignment.Middle)
                                 .Height(24)
                                 .Height(24)
                                 .Width(48)
                                 .Width(48)
                                 .Image(Placeholders.Image);
                                 .Image(Placeholders.Image);
@@ -751,7 +750,7 @@ namespace QuestPDF.Examples
         {
         {
             RenderingTest
             RenderingTest
                 .Create()
                 .Create()
-                .ProduceImages()
+                .ProducePdf()
                 .ShowResults()
                 .ShowResults()
                 .RenderDocument(container =>
                 .RenderDocument(container =>
                 {
                 {
@@ -913,68 +912,6 @@ namespace QuestPDF.Examples
                 });
                 });
         }
         }
         
         
-        [Test]
-        public void DetectSpanPositionExample()
-        {
-            RenderingTest
-                .Create()
-                .PageSize(new PageSize(650, 800))
-                .ProduceImages()
-                .ShowResults()
-                .Render(container =>
-                {
-                    var fontSize = 20;
-                    
-                    var paint = new SKPaint
-                    {
-                        Color = SKColors.Red,
-                        TextSize = fontSize
-                    };
-                    
-                    var fontMetrics = paint.FontMetrics;
-
-                    var start = 0f;
-                    var end = 0f;
-                    
-                    // corner case: what if text is paged? clamp start and end?
-
-                    container
-                        .Padding(25)
-                        .DefaultTextStyle(x => x.FontSize(fontSize).FontFamily("Calibri"))
-                        .Layers(layers =>
-                        {
-                            layers.PrimaryLayer().Text(text =>
-                            {
-                                text.Span(Placeholders.Paragraph());
-                                text.Span(" - ");
-                                
-                                // record start
-                                text.Element().Width(1).Height(1)
-                                    .Canvas((canvas, size) => start = canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
-                                
-                                text.Span(Placeholders.LoremIpsum()).BackgroundColor(Colors.Red.Lighten4);
-                                
-                                // record end
-                                text.Element().Width(1).Height(1)
-                                    .Canvas((canvas, size) => end = canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
-                            
-                                text.Span(" - ");
-                                text.Span(Placeholders.Paragraph());
-                            });
-                            
-                            layers.Layer().Canvas((canvas, size) =>
-                            {
-                                canvas.Save();
-      
-                                canvas.Translate(-canvas.TotalMatrix.TransX / canvas.TotalMatrix.ScaleX, -canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
-                                canvas.DrawRect(10, start + fontMetrics.Ascent, 5, end - start + (fontMetrics.Bottom - fontMetrics.Ascent), paint);
-                                
-                                canvas.Restore();
-                            });
-                        });
-                });
-        }
-        
         [Test]
         [Test]
         public void InconsistentLineHeightWhenUsingNewLineTest()
         public void InconsistentLineHeightWhenUsingNewLineTest()
         {
         {
@@ -1023,11 +960,7 @@ namespace QuestPDF.Examples
                         page.DefaultTextStyle(x => x
                         page.DefaultTextStyle(x => x
                             .FontSize(24)
                             .FontSize(24)
                             .Bold()
                             .Bold()
-                            .FontFamily("Times New Roman")
-                            .Fallback(y => y
-                                .FontFamily("Microsoft YaHei")
-                                .Underline()
-                                .BackgroundColor(Colors.Red.Lighten2)));
+                            .FontFamily("Times New Roman"));
 
 
                         page.Content().Text(text =>
                         page.Content().Text(text =>
                         {
                         {
@@ -1062,5 +995,94 @@ namespace QuestPDF.Examples
                      });
                      });
                  });
                  });
          }
          }
+         
+         [Test]
+         public void LongPageableText()
+         {
+             var longText = string.Join("\n", Enumerable.Range(0, 100).Select(_ => Placeholders.Paragraph()));
+             
+             RenderingTest
+                 .Create()
+                 .ProducePdf()
+                 .ShowResults()
+                 .PageSize(PageSizes.A4)
+                 .Render(container =>
+                 {
+                     container.Padding(25).Background(Colors.Grey.Lighten3).AlignLeft().Text(longText);
+                 });
+         }
+         
+         [Test]
+         public void TextAlignment()
+         {
+             RenderingTest
+                 .Create()
+                 .ProducePdf()
+                 .ShowResults()
+                 .PageSize(PageSizes.A4)
+                 .Render(container =>
+                 {
+                     container.Padding(25).Column(column =>
+                     {
+                         column.Spacing(25);
+
+                         foreach (var contentDirection in Enum.GetValues<ContentDirection>())
+                         {
+                             column.Item().Text(contentDirection.ToString()).FontSize(20).Bold();
+                             
+                             foreach (var horizontalAlignment in Enum.GetValues<HorizontalAlignment>())
+                                 foreach (var textHorizontalAlignment in Enum.GetValues<TextHorizontalAlignment>())
+                                     column.Item().Element(GenerateTextCase(contentDirection, horizontalAlignment, textHorizontalAlignment));
+                         }
+                     });
+
+                     Action<IContainer> GenerateTextCase(ContentDirection contentDirection, HorizontalAlignment horizontalAlignment, TextHorizontalAlignment textAlignment)
+                     {
+                         return container =>
+                         {
+                             container
+                                 .Element(element =>
+                                 {
+                                     return contentDirection switch
+                                     {
+                                         ContentDirection.LeftToRight => element.ContentFromLeftToRight(),
+                                         ContentDirection.RightToLeft => element.ContentFromRightToLeft(),
+                                         _ => throw new Exception()
+                                     };
+                                 })
+                                 .Element(element =>
+                                 {
+                                     return horizontalAlignment switch
+                                     {
+                                         HorizontalAlignment.Left => element.AlignLeft(),
+                                         HorizontalAlignment.Center => element.AlignCenter(),
+                                         HorizontalAlignment.Right => element.AlignRight(),
+                                         _ => throw new Exception()
+                                     };
+                                 })
+                                 .Background(Colors.Grey.Lighten3)
+                                 .Width(200)
+                                 .Padding(10)
+                                 .Text(text =>
+                                 {
+                                     if (textAlignment == TextHorizontalAlignment.Left)
+                                         text.AlignLeft();
+                                     else if (textAlignment == TextHorizontalAlignment.Center)
+                                         text.AlignCenter();
+                                     else if (textAlignment == TextHorizontalAlignment.Right)
+                                         text.AlignRight();
+                                     else if (textAlignment == TextHorizontalAlignment.Justify)
+                                         text.Justify();
+                                     else if (textAlignment == TextHorizontalAlignment.Start)
+                                         text.AlignStart();
+                                     else if (textAlignment == TextHorizontalAlignment.End)
+                                         text.AlignEnd();
+                                     
+                                     text.Span($"{horizontalAlignment.ToString()} - {textAlignment.ToString()}");
+                                 });
+                         };
+                     }
+                 });
+         }
     }
     }
 }
 }

+ 2 - 1
Source/QuestPDF.LayoutTests/QuestPDF.LayoutTests.csproj

@@ -9,9 +9,10 @@
     </PropertyGroup>
     </PropertyGroup>
 
 
     <ItemGroup>
     <ItemGroup>
-        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
         <PackageReference Include="NUnit" Version="4.0.1" />
         <PackageReference Include="NUnit" Version="4.0.1" />
         <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
         <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
+        <PackageReference Include="SkiaSharp" Version="2.88.7" />
     </ItemGroup>
     </ItemGroup>
 
 
     <ItemGroup>
     <ItemGroup>

+ 2 - 2
Source/QuestPDF.LayoutTests/TestEngine/AnnotateInvalidAreaHelper.cs

@@ -7,7 +7,7 @@ internal static class AnnotateInvalidAreaHelper
 {
 {
     private const float StripeThickness = 1f;
     private const float StripeThickness = 1f;
     private const float StripeScale = 3f;
     private const float StripeScale = 3f;
-    private const string LineColor = Colors.Red.Medium;
+    private static readonly Color LineColor = Colors.Red.Medium;
     
     
     public static void Annotate(SKCanvas canvas, SKPath area)
     public static void Annotate(SKCanvas canvas, SKPath area)
     {
     {
@@ -24,7 +24,7 @@ internal static class AnnotateInvalidAreaHelper
 
 
             using var paint = new SKPaint
             using var paint = new SKPaint
             {
             {
-                Color = SKColor.Parse(LineColor),
+                Color = new SKColor(LineColor),
                 PathEffect = SKPathEffect.Create2DLine(StripeThickness, matrix),
                 PathEffect = SKPathEffect.Create2DLine(StripeThickness, matrix),
                 IsAntialias = true
                 IsAntialias = true
             };
             };

+ 19 - 14
Source/QuestPDF.LayoutTests/TestEngine/LayoutTestOutputVisualization.cs

@@ -13,9 +13,9 @@ internal static class LayoutTestResultVisualization
     private const int Padding = 10;
     private const int Padding = 10;
     
     
     // document colors
     // document colors
-    private const string DocumentBackgroundColor = Colors.Grey.Darken2;
-    private const string PageBackgroundColor = Colors.Grey.Lighten1;
-    private const string RequiredAreaBackgroundColor = Colors.White;
+    private static readonly Color DocumentBackgroundColor = Colors.Grey.Darken2;
+    private static readonly Color PageBackgroundColor = Colors.Grey.Lighten1;
+    private static readonly Color RequiredAreaBackgroundColor = Colors.White;
     
     
     // grid configuration
     // grid configuration
     private const float GridSize = 10;
     private const float GridSize = 10;
@@ -25,7 +25,7 @@ internal static class LayoutTestResultVisualization
     // mock drawing settings
     // mock drawing settings
     private const byte OccludedMockBorderThickness = 5;
     private const byte OccludedMockBorderThickness = 5;
 
 
-    private static readonly string[] DefaultElementColors =
+    private static readonly Color[] DefaultElementColors =
     {
     {
         Colors.DeepPurple.Lighten2,
         Colors.DeepPurple.Lighten2,
         Colors.Blue.Lighten2,
         Colors.Blue.Lighten2,
@@ -58,7 +58,7 @@ internal static class LayoutTestResultVisualization
         using var canvas = pdf.BeginPage(canvasWidth * OutputImageScale, canvasHeight * OutputImageScale);
         using var canvas = pdf.BeginPage(canvasWidth * OutputImageScale, canvasHeight * OutputImageScale);
         
         
         canvas.Scale(OutputImageScale, OutputImageScale);
         canvas.Scale(OutputImageScale, OutputImageScale);
-        canvas.Clear(SKColor.Parse(DocumentBackgroundColor));
+        canvas.Clear(new SKColor(DocumentBackgroundColor));
 
 
         // draw content
         // draw content
         var mockColors = AssignColorsToMocks();
         var mockColors = AssignColorsToMocks();
@@ -68,7 +68,7 @@ internal static class LayoutTestResultVisualization
         pdf.EndPage();
         pdf.EndPage();
         pdf.Close();
         pdf.Close();
 
 
-        IDictionary<string, string> AssignColorsToMocks()
+        IDictionary<string, Color> AssignColorsToMocks()
         {
         {
             var mocks = Enumerable
             var mocks = Enumerable
                 .Concat(result.ActualLayout.Pages, result.ExpectedLayout.Pages)
                 .Concat(result.ActualLayout.Pages, result.ExpectedLayout.Pages)
@@ -87,9 +87,14 @@ internal static class LayoutTestResultVisualization
             canvas.Translate(Padding, Padding);
             canvas.Translate(Padding, Padding);
             
             
             // draw title
             // draw title
-            using var textPaint = TextStyle.LibraryDefault.FontSize(8).FontColor(Colors.White).Bold().ToPaint().Clone();
-            textPaint.TextAlign = SKTextAlign.Center;
-
+            using var textPaint = new SKPaint
+            {
+                TextSize = 8,
+                Color = SKColors.White,
+                Typeface = SKTypeface.FromFamilyName("Calibri", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright),
+                TextAlign = SKTextAlign.Center
+            };
+            
             var actualHeaderPosition = new SKPoint(result.PageSize.Width / 2, textPaint.TextSize / 2);
             var actualHeaderPosition = new SKPoint(result.PageSize.Width / 2, textPaint.TextSize / 2);
             canvas.DrawText("ACTUAL", actualHeaderPosition, textPaint);
             canvas.DrawText("ACTUAL", actualHeaderPosition, textPaint);
             
             
@@ -126,7 +131,7 @@ internal static class LayoutTestResultVisualization
             // draw page
             // draw page
             using var availableAreaPaint = new SKPaint
             using var availableAreaPaint = new SKPaint
             {
             {
-                Color = SKColor.Parse(PageBackgroundColor)
+                Color = new SKColor(PageBackgroundColor)
             };
             };
             
             
             canvas.DrawRect(0, 0, result.PageSize.Width, result.PageSize.Height, availableAreaPaint);
             canvas.DrawRect(0, 0, result.PageSize.Width, result.PageSize.Height, availableAreaPaint);
@@ -140,7 +145,7 @@ internal static class LayoutTestResultVisualization
             // draw required area
             // draw required area
             using var requiredAreaPaint = new SKPaint
             using var requiredAreaPaint = new SKPaint
             {
             {
-                Color = SKColor.Parse(RequiredAreaBackgroundColor)
+                Color = new SKColor(RequiredAreaBackgroundColor)
             };
             };
             
             
             canvas.DrawRect(0, 0, pageLayout.RequiredArea.Width, pageLayout.RequiredArea.Height, requiredAreaPaint);
             canvas.DrawRect(0, 0, pageLayout.RequiredArea.Width, pageLayout.RequiredArea.Height, requiredAreaPaint);
@@ -161,7 +166,7 @@ internal static class LayoutTestResultVisualization
                 
                 
             using var mockAreaPaint = new SKPaint
             using var mockAreaPaint = new SKPaint
             {
             {
-                Color = SKColor.Parse(color)
+                Color = new SKColor(color)
             };
             };
             
             
             canvas.Save();
             canvas.Save();
@@ -178,7 +183,7 @@ internal static class LayoutTestResultVisualization
                 
                 
             using var mockBorderPaint = new SKPaint
             using var mockBorderPaint = new SKPaint
             {
             {
-                Color = SKColor.Parse(color),
+                Color = new SKColor(color),
                 IsStroke = true,
                 IsStroke = true,
                 StrokeWidth = OccludedMockBorderThickness
                 StrokeWidth = OccludedMockBorderThickness
             };
             };
@@ -196,7 +201,7 @@ internal static class LayoutTestResultVisualization
         {
         {
             using var paint = new SKPaint
             using var paint = new SKPaint
             {
             {
-                Color = SKColor.Parse(Colors.Black).WithAlpha(GridLineTransparency),
+                Color = SKColors.Black.WithAlpha(GridLineTransparency),
                 StrokeWidth = GridLineThickness
                 StrokeWidth = GridLineThickness
             };
             };
 
 

+ 3 - 3
Source/QuestPDF.LayoutTests/TestEngine/MockChild.cs

@@ -49,18 +49,18 @@ internal class ElementMock : Element
         
         
         HeightOffset += height;
         HeightOffset += height;
         
         
-        Canvas.DrawRectangle(Position.Zero, size, Colors.Grey.Medium);
+        Canvas.DrawFilledRectangle(Position.Zero, size, Colors.Grey.Medium);
         
         
         if (Canvas is not SkiaCanvasBase canvasBase)
         if (Canvas is not SkiaCanvasBase canvasBase)
             return;
             return;
 
 
-        var matrix = canvasBase.Canvas.TotalMatrix;
+        var matrix = canvasBase.Canvas.GetCurrentTotalMatrix();
         
         
         DrawingCommands.Add(new MockDrawingCommand
         DrawingCommands.Add(new MockDrawingCommand
         {
         {
             MockId = MockId,
             MockId = MockId,
             PageNumber = PageContext.CurrentPage,
             PageNumber = PageContext.CurrentPage,
-            Position = new Position(matrix.TransX / matrix.ScaleX, matrix.TransY / matrix.ScaleY),
+            Position = new Position(matrix.TranslateX / matrix.ScaleX, matrix.TranslateY / matrix.ScaleY),
             Size = availableSpace
             Size = availableSpace
         });
         });
 
 

+ 32 - 10
Source/QuestPDF.Previewer/CommunicationService.cs

@@ -12,7 +12,9 @@ class CommunicationService
 {
 {
     public static CommunicationService Instance { get; } = new ();
     public static CommunicationService Instance { get; } = new ();
     
     
-    public event Action<DocumentSnapshot>? OnDocumentRefreshed;
+    public event Action<DocumentStructure>? OnDocumentUpdated;
+    public Func<ICollection<PageSnapshotIndex>>? OnPageSnapshotsRequested { get; set; }
+    public Action<ICollection<RenderedPageSnapshot>> OnPageSnapshotsProvided  { get; set; }
 
 
     private WebApplication? Application { get; set; }
     private WebApplication? Application { get; set; }
 
 
@@ -35,7 +37,9 @@ class CommunicationService
 
 
         Application.MapGet("ping", HandlePing);
         Application.MapGet("ping", HandlePing);
         Application.MapGet("version", HandleVersion);
         Application.MapGet("version", HandleVersion);
-        Application.MapPost("update/preview", HandleUpdatePreview);
+        Application.MapPost("preview/update", HandlePreviewRefresh);
+        Application.MapGet("preview/getRenderingRequests", HandleGetRequests);
+        Application.MapPost("preview/provideRenderedImages", HandleProvidedSnapshotImages);
             
             
         return Application.RunAsync($"http://localhost:{port}/");
         return Application.RunAsync($"http://localhost:{port}/");
     }
     }
@@ -48,7 +52,7 @@ class CommunicationService
 
 
     private async Task<IResult> HandlePing()
     private async Task<IResult> HandlePing()
     {
     {
-        return OnDocumentRefreshed == null 
+        return OnDocumentUpdated == null 
             ? Results.StatusCode(StatusCodes.Status503ServiceUnavailable) 
             ? Results.StatusCode(StatusCodes.Status503ServiceUnavailable) 
             : Results.Ok();
             : Results.Ok();
     }
     }
@@ -58,17 +62,35 @@ class CommunicationService
         return Results.Json(GetType().Assembly.GetName().Version);
         return Results.Json(GetType().Assembly.GetName().Version);
     }
     }
     
     
-    private async Task<IResult> HandleUpdatePreview(HttpRequest request)
+    private async Task HandlePreviewRefresh(DocumentStructure documentStructure)
     {
     {
-        var documentSnapshot = JsonSerializer.Deserialize<DocumentSnapshot>(request.Form["command"], JsonSerializerOptions);
+        Task.Run(() => OnDocumentUpdated(documentStructure));
+    }
+
+    private async Task<ICollection<PageSnapshotIndex>> HandleGetRequests()
+    {
+        return OnPageSnapshotsRequested();
+    }
+    
+    private async Task HandleProvidedSnapshotImages(HttpRequest request)
+    {
+        var renderedPageIndexes = JsonSerializer.Deserialize<ICollection<PageSnapshotIndex>>(request.Form["metadata"], JsonSerializerOptions);
+        var renderedPages = new List<RenderedPageSnapshot>();
 
 
-        foreach (var pageSnapshot in documentSnapshot.Pages)
+        foreach (var index in renderedPageIndexes)
         {
         {
-            using var stream = request.Form.Files[pageSnapshot.Id].OpenReadStream();
-            pageSnapshot.Picture = SKPicture.Deserialize(stream);
+            using var memoryStream = new MemoryStream();
+            await request.Form.Files.GetFile(index.ToString()).CopyToAsync(memoryStream);
+            var image = SKImage.FromEncodedData(memoryStream.ToArray()).ToRasterImage(true);
+
+            var renderedPage = new RenderedPageSnapshot
+            {
+                ZoomLevel = index.ZoomLevel, PageIndex = index.PageIndex, Image = image
+            };
+            
+            renderedPages.Add(renderedPage);
         }
         }
 
 
-        Task.Run(() => OnDocumentRefreshed(documentSnapshot));
-        return Results.Ok();
+        Task.Run(() => OnPageSnapshotsProvided(renderedPages));
     }
     }
 }
 }

+ 0 - 19
Source/QuestPDF.Previewer/DocumentSnapshot.cs

@@ -1,19 +0,0 @@
-using SkiaSharp;
-
-namespace QuestPDF.Previewer;
-
-internal sealed class DocumentSnapshot
-{
-    public bool DocumentContentHasLayoutOverflowIssues { get; set; }
-    public ICollection<PageSnapshot> Pages { get; set; }
-
-    public class PageSnapshot
-    {
-        public string Id { get; set; }
-        
-        public float Width { get; set; }
-        public float Height { get; set; }
-        
-        public SKPicture Picture { get; set; }
-    }
-}

+ 0 - 20
Source/QuestPDF.Previewer/Helpers.cs

@@ -1,20 +0,0 @@
-using SkiaSharp;
-
-namespace QuestPDF.Previewer;
-
-class Helpers
-{
-    public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<DocumentSnapshot.PageSnapshot> pages)
-    {
-        using var stream = File.Create(filePath);
-            
-        using 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();
-        }
-    }
-}

+ 111 - 20
Source/QuestPDF.Previewer/InteractiveCanvas.cs

@@ -1,4 +1,5 @@
-using Avalonia;
+using System.Collections.Concurrent;
+using Avalonia;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.Rendering.SceneGraph;
 using Avalonia.Rendering.SceneGraph;
@@ -10,7 +11,10 @@ namespace QuestPDF.Previewer;
 class InteractiveCanvas : ICustomDrawOperation
 class InteractiveCanvas : ICustomDrawOperation
 {
 {
     public Rect Bounds { get; set; }
     public Rect Bounds { get; set; }
-    public ICollection<DocumentSnapshot.PageSnapshot> Pages { get; set; }
+    public float RenderingScale { get; set; }
+
+    private List<DocumentStructure.PageSize> PageSizes { get; set; } = new();
+    private ConcurrentBag<RenderedPageSnapshot> PageSnapshotCache { get; set; } = new();
 
 
     private float Width => (float)Bounds.Width;
     private float Width => (float)Bounds.Width;
     private float Height => (float)Bounds.Height;
     private float Height => (float)Bounds.Height;
@@ -19,15 +23,15 @@ class InteractiveCanvas : ICustomDrawOperation
     public float TranslateX { get; set; }
     public float TranslateX { get; set; }
     public float TranslateY { get; set; }
     public float TranslateY { get; set; }
 
 
-    private const float MinScale = 0.1f;
-    private const float MaxScale = 10f;
+    private const float MinScale = 1 / 8f;
+    private const float MaxScale = 8f;
 
 
     private const float PageSpacing = 25f;
     private const float PageSpacing = 25f;
     private const float SafeZone = 25f;
     private const float SafeZone = 25f;
 
 
-    public float TotalPagesHeight => Pages.Sum(x => x.Height) + (Pages.Count - 1) * PageSpacing;
+    public float TotalPagesHeight => PageSizes.Sum(x => x.Height) + (PageSizes.Count - 1) * PageSpacing;
     public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
     public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
-    public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0;
+    public float MaxWidth => PageSizes.Any() ? PageSizes.Max(x => x.Width) : 0;
     
     
     public float MaxTranslateY => TotalHeight - Height / Scale;
     public float MaxTranslateY => TotalHeight - Height / Scale;
 
 
@@ -52,6 +56,49 @@ class InteractiveCanvas : ICustomDrawOperation
         }
         }
     }
     }
 
 
+    #region interaction
+
+    public void SetNewDocumentStructure(DocumentStructure document)
+    {
+        var imagesToDispose = PageSnapshotCache.Select(x => x.Image).ToList();
+
+        // minimize the risk of a race condition, when Avalonia is still using the images and code disposes them
+        Task.Run(async () =>
+        {
+            await Task.Delay(TimeSpan.FromSeconds(5));
+            
+            foreach (var image in imagesToDispose)
+                image.Dispose();
+        });
+
+        PageSnapshotCache.Clear();
+        PageSizes = document.Pages.ToList();
+    }
+    
+    public ICollection<PageSnapshotIndex> GetMissingSnapshots()
+    {
+        var requiredKeys = GetVisiblePages(padding: 500).Select(x => (x.pageIndex, PreferredZoomLevel)).ToList();
+        var availableKeys = PageSnapshotCache.Select(x => (x.PageIndex, x.ZoomLevel)).ToList();
+
+        var missingKeys = requiredKeys.Except(availableKeys).ToArray();
+
+        return missingKeys
+            .Select(x => new PageSnapshotIndex
+            {
+                PageIndex = x.Item1,
+                ZoomLevel = x.Item2
+            })
+            .ToList();
+    }
+
+    public void AddSnapshots(ICollection<RenderedPageSnapshot> snapshots)
+    {
+        foreach (var snapshot in snapshots)
+            PageSnapshotCache.Add(snapshot);
+    }
+    
+    #endregion
+    
     #region transformations
     #region transformations
     
     
     private void LimitScale()
     private void LimitScale()
@@ -109,6 +156,31 @@ class InteractiveCanvas : ICustomDrawOperation
     #endregion
     #endregion
     
     
     #region rendering
     #region rendering
+
+    private int PreferredZoomLevel => (int)Math.Min(3, Math.Ceiling(Math.Log2(Scale * RenderingScale)));
+    
+    private IEnumerable<(int pageIndex, float verticalOffset)> GetVisiblePages(float padding = 100)
+    {
+        padding /= Scale;
+        
+        var visibleOffsetFrom = TranslateY - padding;
+        var visibleOffsetTo = TranslateY + Height / Scale + padding;
+        
+        var topOffset = 0f;
+
+        foreach (var pageIndex in Enumerable.Range(0, PageSizes.Count))
+        {
+            var page = PageSizes.ElementAt(pageIndex);
+            
+            if (topOffset + page.Height > visibleOffsetFrom)
+                yield return (pageIndex, topOffset);
+            
+            topOffset += page.Height + PageSpacing;
+            
+            if (topOffset > visibleOffsetTo)
+                yield break;
+        }
+    }
     
     
     public void Render(ImmediateDrawingContext context)
     public void Render(ImmediateDrawingContext context)
     {
     {
@@ -122,14 +194,11 @@ class InteractiveCanvas : ICustomDrawOperation
             return;
             return;
         
         
         // draw document
         // draw document
-        if (Pages.Count <= 0)
+        if (PageSizes.Count <= 0)
             return;
             return;
-
+  
         LimitScale();
         LimitScale();
         LimitTranslate();
         LimitTranslate();
-    
-        if (canvas == null)
-            throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}");
 
 
         var originalMatrix = canvas.TotalMatrix;
         var originalMatrix = canvas.TotalMatrix;
 
 
@@ -137,24 +206,46 @@ class InteractiveCanvas : ICustomDrawOperation
         
         
         canvas.Scale(Scale);
         canvas.Scale(Scale);
         canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale);
         canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale);
-        
-        foreach (var page in Pages)
+
+        foreach (var (pageIndex, offset) in GetVisiblePages())
         {
         {
-            canvas.Translate(-page.Width / 2f, 0);
-            DrawBlankPage(canvas, page.Width, page.Height);
-            DrawPageSnapshot(canvas, page);
-            canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
+            var pageSize = PageSizes.ElementAt(pageIndex);
+            
+            canvas.Save();
+            canvas.Translate(-pageSize.Width / 2f, offset);
+            DrawBlankPage(canvas, pageSize.Width, pageSize.Height);
+            DrawPageSnapshot(canvas, pageIndex);
+            canvas.Restore();
         }
         }
 
 
         canvas.SetMatrix(originalMatrix);
         canvas.SetMatrix(originalMatrix);
         DrawInnerGradient(canvas);
         DrawInnerGradient(canvas);
     }
     }
     
     
-    private static void DrawPageSnapshot(SKCanvas canvas, DocumentSnapshot.PageSnapshot pageSnapshot)
+    private void DrawPageSnapshot(SKCanvas canvas, int pageIndex)
     {
     {
+        var page = PageSizes.ElementAt(pageIndex);
+
+        var renderedSnapshot = PageSnapshotCache
+            .Where(x => x.PageIndex == pageIndex)
+            .OrderBy(x => Math.Abs(PreferredZoomLevel - x.ZoomLevel))
+            .ThenByDescending(x => x.ZoomLevel)
+            .FirstOrDefault();
+        
+        if (renderedSnapshot == null)
+            return;
+        
+        using var drawImagePaint = new SKPaint
+        {
+            FilterQuality = SKFilterQuality.Medium // optimal for down-scaling
+        };
+
+        var renderingScale = (float)Math.Pow(2, renderedSnapshot.ZoomLevel);
+        
         canvas.Save();
         canvas.Save();
-        canvas.ClipRect(new SKRect(0, 0, pageSnapshot.Width, pageSnapshot.Height));
-        canvas.DrawPicture(pageSnapshot.Picture);
+        canvas.ClipRect(new SKRect(0, 0, page.Width, page.Height));
+        canvas.Scale(1 / renderingScale);
+        canvas.DrawImage(renderedSnapshot.Image, SKPoint.Empty, drawImagePaint);
         canvas.Restore();
         canvas.Restore();
     }
     }
     
     

+ 28 - 0
Source/QuestPDF.Previewer/Models.cs

@@ -0,0 +1,28 @@
+using SkiaSharp;
+
+namespace QuestPDF.Previewer;
+
+class DocumentStructure
+{
+    public bool DocumentContentHasLayoutOverflowIssues { get; set; }
+    public ICollection<PageSize> Pages { get; set; }
+    
+    public class PageSize
+    {
+        public float Width { get; set; }
+        public float Height { get; set; }
+    }
+}
+
+class PageSnapshotIndex
+{
+    public int PageIndex { get; set; }
+    public int ZoomLevel { get; set; }
+
+    public override string ToString() => $"{ZoomLevel}/{PageIndex}";
+}
+
+class RenderedPageSnapshot : PageSnapshotIndex
+{
+    public SKImage Image { get; set; }
+}

+ 15 - 15
Source/QuestPDF.Previewer/PreviewerControl.cs

@@ -9,16 +9,7 @@ namespace QuestPDF.Previewer
 {
 {
     class PreviewerControl : Control
     class PreviewerControl : Control
     {
     {
-        private InteractiveCanvas InteractiveCanvas { get; set; } = new ();
-        
-        public static readonly StyledProperty<ObservableCollection<DocumentSnapshot.PageSnapshot>> PagesProperty =
-            AvaloniaProperty.Register<PreviewerControl, ObservableCollection<DocumentSnapshot.PageSnapshot>>(nameof(Pages));
-        
-        public ObservableCollection<DocumentSnapshot.PageSnapshot>? Pages
-        {
-            get => GetValue(PagesProperty);
-            set => SetValue(PagesProperty, value);
-        }
+        private InteractiveCanvas InteractiveCanvas { get; set; } = new();
 
 
         public static readonly StyledProperty<float> CurrentScrollProperty = AvaloniaProperty.Register<PreviewerControl, float>(nameof(CurrentScroll));
         public static readonly StyledProperty<float> CurrentScrollProperty = AvaloniaProperty.Register<PreviewerControl, float>(nameof(CurrentScroll));
         
         
@@ -38,11 +29,19 @@ namespace QuestPDF.Previewer
         
         
         public PreviewerControl()
         public PreviewerControl()
         {
         {
-            PagesProperty.Changed.Subscribe(x =>
+            CommunicationService.Instance.OnDocumentUpdated += document =>
             {
             {
-                InteractiveCanvas.Pages = x.NewValue.Value;
-                InvalidateVisual();
-            });
+                InteractiveCanvas.SetNewDocumentStructure(document);
+                Dispatcher.UIThread.InvokeAsync(InvalidateVisual).GetTask();
+            };
+            
+            CommunicationService.Instance.OnPageSnapshotsRequested += InteractiveCanvas.GetMissingSnapshots;
+            
+            CommunicationService.Instance.OnPageSnapshotsProvided += snapshots =>
+            {
+                InteractiveCanvas.AddSnapshots(snapshots);
+                Dispatcher.UIThread.InvokeAsync(InvalidateVisual).GetTask();
+            };
 
 
             CurrentScrollProperty.Changed.Subscribe(x =>
             CurrentScrollProperty.Changed.Subscribe(x =>
             {
             {
@@ -109,7 +108,8 @@ namespace QuestPDF.Previewer
         {
         {
             CurrentScroll = InteractiveCanvas.ScrollPercentY;
             CurrentScroll = InteractiveCanvas.ScrollPercentY;
             ScrollViewportSize = InteractiveCanvas.ScrollViewportSizeY;
             ScrollViewportSize = InteractiveCanvas.ScrollViewportSizeY;
-    
+
+            InteractiveCanvas.RenderingScale = (float)VisualRoot.RenderScaling;
             InteractiveCanvas.Bounds = new Rect(0, 0, Bounds.Width, Bounds.Height);
             InteractiveCanvas.Bounds = new Rect(0, 0, Bounds.Width, Bounds.Height);
 
 
             context.Custom(InteractiveCanvas);
             context.Custom(InteractiveCanvas);

+ 2 - 36
Source/QuestPDF.Previewer/PreviewerWindow.axaml

@@ -49,8 +49,7 @@
 			                            HorizontalAlignment="Stretch" 
 			                            HorizontalAlignment="Stretch" 
 			                            VerticalAlignment="Stretch"
 			                            VerticalAlignment="Stretch"
 			                            CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
 			                            CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
-			                            ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
-			                            Pages="{Binding Pages, Mode=OneWay}" />
+			                            ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}" />
 			
 			
 			<Border IsVisible="{Binding DocumentContentHasLayoutOverflowIssues}" Grid.Row="1" Grid.Column="0" VerticalAlignment="Top" HorizontalAlignment="Left" Padding="16,8" Background="#F44336" CornerRadius="8" BoxShadow="0 0 8 0 #44000000" Margin="32">
 			<Border IsVisible="{Binding DocumentContentHasLayoutOverflowIssues}" Grid.Row="1" Grid.Column="0" VerticalAlignment="Top" HorizontalAlignment="Left" Padding="16,8" Background="#F44336" CornerRadius="8" BoxShadow="0 0 8 0 #44000000" Margin="32">
 				<StackPanel Orientation="Horizontal" Spacing="8">
 				<StackPanel Orientation="Horizontal" Spacing="8">
@@ -63,40 +62,7 @@
 					<TextBlock VerticalAlignment="Center">Document has layout problems</TextBlock>
 					<TextBlock VerticalAlignment="Center">Document has layout problems</TextBlock>
 				</StackPanel>
 				</StackPanel>
 			</Border>
 			</Border>
-			
-			<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"
 			<ScrollBar Grid.Row="1" Grid.Column="1"
 			           Orientation="Vertical" 
 			           Orientation="Vertical" 
 			           AllowAutoHide="False" 
 			           AllowAutoHide="False" 

+ 1 - 37
Source/QuestPDF.Previewer/PreviewerWindowViewModel.cs

@@ -2,19 +2,11 @@
 using System.Diagnostics;
 using System.Diagnostics;
 using Avalonia.Threading;
 using Avalonia.Threading;
 using ReactiveUI;
 using ReactiveUI;
-using Unit = System.Reactive.Unit;
 
 
 namespace QuestPDF.Previewer
 namespace QuestPDF.Previewer
 {
 {
     internal sealed class PreviewerWindowViewModel : ReactiveObject
     internal sealed class PreviewerWindowViewModel : ReactiveObject
     {
     {
-        private ObservableCollection<DocumentSnapshot.PageSnapshot> _pages = new();
-        public ObservableCollection<DocumentSnapshot.PageSnapshot> Pages
-        {
-            get => _pages;
-            set => this.RaiseAndSetIfChanged(ref _pages, value);
-        }
-        
         private bool _documentContentHasLayoutOverflowIssues;
         private bool _documentContentHasLayoutOverflowIssues;
         public bool DocumentContentHasLayoutOverflowIssues
         public bool DocumentContentHasLayoutOverflowIssues
         {
         {
@@ -47,25 +39,9 @@ namespace QuestPDF.Previewer
             private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _verticalScrollbarVisible, value));
             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()
         public PreviewerWindowViewModel()
         {
         {
-            CommunicationService.Instance.OnDocumentRefreshed += HandleUpdatePreview;
-            
-            ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
-            ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/api-reference/index.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);
+            CommunicationService.Instance.OnDocumentUpdated += x => DocumentContentHasLayoutOverflowIssues = x.DocumentContentHasLayoutOverflowIssues;
         }
         }
         
         
         private static void OpenLink(string path)
         private static void OpenLink(string path)
@@ -81,17 +57,5 @@ namespace QuestPDF.Previewer
 
 
             openBrowserProcess.Start();
             openBrowserProcess.Start();
         }
         }
-        
-        private void HandleUpdatePreview(DocumentSnapshot documentSnapshot)
-        {
-            var oldPages = Pages;
-            
-            Pages.Clear();
-            Pages = new ObservableCollection<DocumentSnapshot.PageSnapshot>(documentSnapshot.Pages);
-            DocumentContentHasLayoutOverflowIssues = documentSnapshot.DocumentContentHasLayoutOverflowIssues;
-            
-            foreach (var page in oldPages)
-                page.Picture.Dispose();
-        }
     }
     }
 }
 }

+ 8 - 9
Source/QuestPDF.Previewer/QuestPDF.Previewer.csproj

@@ -1,10 +1,9 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
-
     <PropertyGroup>
     <PropertyGroup>
         <Authors>MarcinZiabek</Authors>
         <Authors>MarcinZiabek</Authors>
         <Company>CodeFlint</Company>
         <Company>CodeFlint</Company>
         <PackageId>QuestPDF.Previewer</PackageId>
         <PackageId>QuestPDF.Previewer</PackageId>
-        <Version>2023.12.2</Version>
+        <Version>2024.3.0</Version>
         <PackAsTool>true</PackAsTool>
         <PackAsTool>true</PackAsTool>
         <ToolCommandName>questpdf-previewer</ToolCommandName>
         <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>
         <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>
@@ -17,10 +16,10 @@
         <RepositoryUrl>https://github.com/QuestPDF/library.git</RepositoryUrl>
         <RepositoryUrl>https://github.com/QuestPDF/library.git</RepositoryUrl>
         <RepositoryType>git</RepositoryType>
         <RepositoryType>git</RepositoryType>
         <Copyright>Marcin Ziąbek, QuestPDF contributors</Copyright>
         <Copyright>Marcin Ziąbek, QuestPDF contributors</Copyright>
-        <PackageTags>previewer pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core</PackageTags>
+        <PackageTags>PDF;C#;dotnet;csharp;.NET;PDF library;PDF document;PDF generation;PDF creation;PDF report;PDF invoice;PDF export;windows;linux;mac;azure;aws;open-source;open source;free;adobe;file;SVG;HTML;XPS</PackageTags>
         <PackageLicenseExpression>MIT</PackageLicenseExpression>
         <PackageLicenseExpression>MIT</PackageLicenseExpression>
         <OutputType>exe</OutputType>
         <OutputType>exe</OutputType>
-        <TargetFramework>net6.0</TargetFramework>
+        <TargetFramework>net8.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
         <Nullable>enable</Nullable>
     </PropertyGroup>
     </PropertyGroup>
@@ -37,11 +36,11 @@
     
     
     <ItemGroup>
     <ItemGroup>
         <FrameworkReference Include="Microsoft.AspNetCore.App" />
         <FrameworkReference Include="Microsoft.AspNetCore.App" />
-        <PackageReference Include="Avalonia.Desktop" Version="11.0.7" />
-        <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.7" />
-        <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.7" />
-        <PackageReference Include="Avalonia.Skia" Version="11.0.7" />
-        <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.7" />
+        <PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
+        <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
+        <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.10" />
+        <PackageReference Include="Avalonia.Skia" Version="11.0.10" />
+        <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.7" />
         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.7" />
         <PackageReference Include="SkiaSharp.NativeAssets.Win32" Version="2.88.7" />
         <PackageReference Include="SkiaSharp.NativeAssets.Win32" Version="2.88.7" />
         <PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="2.88.7" />
         <PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="2.88.7" />

+ 35 - 1
Source/QuestPDF.ReportSample/Helpers.cs

@@ -1,13 +1,16 @@
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.IO;
 using System.IO;
+using System.Text;
+using QuestPDF.Fluent;
+using QuestPDF.Infrastructure;
 using SkiaSharp;
 using SkiaSharp;
 
 
 namespace QuestPDF.ReportSample
 namespace QuestPDF.ReportSample
 {
 {
     public static class Helpers
     public static class Helpers
     {
     {
-        public static Random Random { get; } = new Random(1);
+        public static Random Random { get; } = new Random();
         
         
         public static string GetTestItem(string path) => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", path);
         public static string GetTestItem(string path) => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", path);
 
 
@@ -52,5 +55,36 @@ namespace QuestPDF.ReportSample
                 return string.Empty;  
                 return string.Empty;  
             });
             });
         }
         }
+        
+        public static void SkiaSharpCanvas(this IContainer container, Action<SKCanvas, Size> drawOnCanvas)
+        {
+            container.Svg(size =>
+            {
+                using var stream = new MemoryStream();
+
+                using (var canvas = SKSvgCanvas.Create(new SKRect(0, 0, size.Width, size.Height), stream))
+                    drawOnCanvas(canvas, size);
+            
+                var svgData = stream.ToArray();
+                return Encoding.UTF8.GetString(svgData);
+            });
+        }
+        
+        public static void SkiaSharpRasterized(this IContainer container, Action<SKCanvas, Size> drawOnCanvas)
+        {
+            container.Image(payload =>
+            {
+                using var bitmap = new SKBitmap(payload.ImageSize.Width, payload.ImageSize.Height);
+
+                using (var canvas = new SKCanvas(bitmap))
+                {
+                    var scalingFactor = payload.Dpi / (float)DocumentSettings.DefaultRasterDpi;
+                    canvas.Scale(scalingFactor);
+                    drawOnCanvas(canvas, payload.AvailableSpace);
+                }
+                
+                return bitmap.Encode(SKEncodedImageFormat.Png, 100).ToArray();
+            });
+        }
     }
     }
 }
 }

+ 1 - 0
Source/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs

@@ -8,6 +8,7 @@ namespace QuestPDF.ReportSample.Layouts
     public class DifferentHeadersTemplate : IDocument
     public class DifferentHeadersTemplate : IDocument
     {
     {
         public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
         public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
+        public DocumentSettings GetSettings() => DocumentSettings.Default;
 
 
         public void Compose(IDocumentContainer container)
         public void Compose(IDocumentContainer container)
         {
         {

+ 2 - 0
Source/QuestPDF.ReportSample/Layouts/StandardReport.cs

@@ -21,6 +21,8 @@ namespace QuestPDF.ReportSample.Layouts
                 Title = Model.Title
                 Title = Model.Title
             };
             };
         }
         }
+        
+        public DocumentSettings GetSettings() => DocumentSettings.Default;
 
 
         public void Compose(IDocumentContainer container)
         public void Compose(IDocumentContainer container)
         {
         {

+ 15 - 9
Source/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs

@@ -1,5 +1,7 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Drawing;
 using System.Drawing;
+using System.IO;
+using System.Text;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
@@ -48,17 +50,21 @@ namespace QuestPDF.ReportSample.Layouts
                     row.ConstantItem(20).Text($"{number}.");
                     row.ConstantItem(20).Text($"{number}.");
                     row.AutoItem().Text(locationName);
                     row.AutoItem().Text(locationName);
                     
                     
-                    row.RelativeItem().PaddingHorizontal(2).AlignBottom().TranslateY(-3).Height(1).Canvas((canvas, space) =>
-                    {
-                        // best to statically cache
-                        using var paint = new SKPaint
+                    row.RelativeItem()
+                        .PaddingHorizontal(2)
+                        .AlignBottom()
+                        .Height(3)
+                        .SkiaSharpRasterized((canvas, size) =>
                         {
                         {
-                            StrokeWidth = space.Height,
-                            PathEffect = SKPathEffect.CreateDash(new float[] { 1, 3 }, 0)
-                        };
+                            using var paint = new SKPaint
+                            {
+                                StrokeWidth = 1,
+                                PathEffect = SKPathEffect.CreateDash(new float[] { 1, 3 }, 0),
+                            };
                         
                         
-                        canvas.DrawLine(0, 0, space.Width, 0, paint);
-                    });
+                            canvas.Translate(0, 1);
+                            canvas.DrawLine(0, 0, size.Width, 0, paint);
+                        });
                     
                     
                     row.AutoItem().Text(text =>
                     row.AutoItem().Text(text =>
                     {
                     {

+ 2 - 2
Source/QuestPDF.ReportSample/QuestPDF.ReportSample.csproj

@@ -9,9 +9,9 @@
 
 
     <ItemGroup>
     <ItemGroup>
         <PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
         <PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
-        <PackageReference Include="nunit" Version="4.0.1" />
+        <PackageReference Include="nunit" Version="4.1.0" />
         <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
         <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
-        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
         <PackageReference Include="SkiaSharp" Version="2.88.7" />
         <PackageReference Include="SkiaSharp" Version="2.88.7" />
     </ItemGroup>
     </ItemGroup>
 
 

+ 82 - 0
Source/QuestPDF.UnitTests/DocumentCompressionTests.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using FluentAssertions;
+using NUnit.Framework;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.UnitTests;
+
+public class DocumentCompressionTests
+{
+    [Test]
+    public void Test()
+    {
+        var document = Document.Create(document =>
+        {
+            document.Page(page =>
+            {
+                page.Size(PageSizes.A4);
+                
+                page.Content()
+                    .Table(table =>
+                    {
+                        table.ColumnsDefinition(columns =>
+                        {
+                            columns.RelativeColumn();
+                            columns.RelativeColumn();
+                            columns.RelativeColumn();
+                            columns.RelativeColumn();
+                            columns.ConstantColumn(100);
+                        });
+
+                        foreach (var y in Enumerable.Range(1, 1_00))
+                        {
+                            foreach (var x in Enumerable.Range(1, 4))
+                            {
+                                table
+                                    .Cell()
+                                    .Padding(5)
+                                    .Border(1)
+                                    .Background(Placeholders.BackgroundColor())
+                                    .Padding(5)
+                                    .Text($"f({y}, {x}) = '{Placeholders.Sentence()}'");
+                            }
+                        
+                            table
+                                .Cell()
+                                .Padding(5)
+                                .AspectRatio(2f)
+                                .Image(Placeholders.Image);
+                        }
+                    });
+            });
+        });
+
+        var withoutCompression = MeasureDocumentSizeAndGenerationTime(false);
+        var withCompression = MeasureDocumentSizeAndGenerationTime(true);
+        
+        var sizeRatio = withoutCompression.documentSize / (float)withCompression.documentSize;
+        sizeRatio.Should().BeGreaterThan(3);
+
+        Math.Abs(withCompression.generationTime - withoutCompression.generationTime).Should().BeLessThan(100);
+        
+        (int documentSize, float generationTime) MeasureDocumentSizeAndGenerationTime(bool compress)
+        {
+            var stopwatch = new Stopwatch();
+            
+            stopwatch.Restart();
+            
+            var documentSize = document
+                .WithSettings(new DocumentSettings { CompressDocument = compress })
+                .GeneratePdf()
+                .Length;
+            
+            stopwatch.Stop();
+            
+            return (documentSize, stopwatch.ElapsedMilliseconds);
+        }
+    }
+}

+ 7 - 7
Source/QuestPDF.UnitTests/DynamicImageTests.cs

@@ -19,7 +19,7 @@ namespace QuestPDF.UnitTests
                 {
                 {
                     TargetDpi = DocumentSettings.DefaultRasterDpi,
                     TargetDpi = DocumentSettings.DefaultRasterDpi,
                     CompressionQuality = ImageCompressionQuality.High,
                     CompressionQuality = ImageCompressionQuality.High,
-                    Source = GenerateImage
+                    Source = payload => GenerateImage(payload.ImageSize)
                 })
                 })
                 .MeasureElement(new Size(300, 200))
                 .MeasureElement(new Size(300, 200))
                 .CheckMeasureResult(SpacePlan.FullRender(300, 200));
                 .CheckMeasureResult(SpacePlan.FullRender(300, 200));
@@ -47,7 +47,7 @@ namespace QuestPDF.UnitTests
                 {
                 {
                     TargetDpi = DocumentSettings.DefaultRasterDpi,
                     TargetDpi = DocumentSettings.DefaultRasterDpi,
                     CompressionQuality = ImageCompressionQuality.High,
                     CompressionQuality = ImageCompressionQuality.High,
-                    Source = GenerateImage
+                    Source = payload => GenerateImage(payload.ImageSize)
                 })
                 })
                 .DrawElement(new Size(300, 200))
                 .DrawElement(new Size(300, 200))
                 .ExpectCanvasDrawImage(Position.Zero, new Size(300, 200))
                 .ExpectCanvasDrawImage(Position.Zero, new Size(300, 200))
@@ -64,10 +64,10 @@ namespace QuestPDF.UnitTests
                 {
                 {
                     TargetDpi = DocumentSettings.DefaultRasterDpi * 3,
                     TargetDpi = DocumentSettings.DefaultRasterDpi * 3,
                     CompressionQuality = ImageCompressionQuality.High,
                     CompressionQuality = ImageCompressionQuality.High,
-                    Source = size =>
+                    Source = payload =>
                     {
                     {
-                        passedSize = size;
-                        return GenerateImage(size);
+                        passedSize = payload.ImageSize;
+                        return GenerateImage(payload.ImageSize);
                     }
                     }
                 })
                 })
                 .DrawElement(new Size(400, 300))
                 .DrawElement(new Size(400, 300))
@@ -82,8 +82,8 @@ namespace QuestPDF.UnitTests
         {
         {
             var image = GenerateImage(size.Width, size.Height);
             var image = GenerateImage(size.Width, size.Height);
             return image.Encode(SKEncodedImageFormat.Png, 100).ToArray();
             return image.Encode(SKEncodedImageFormat.Png, 100).ToArray();
-        }
-
+        }
+
         static SKImage GenerateImage(int width, int height)
         static SKImage GenerateImage(int width, int height)
         {
         {
             var imageInfo = new SKImageInfo(width, height);
             var imageInfo = new SKImageInfo(width, height);

+ 0 - 146
Source/QuestPDF.UnitTests/FontStyleSetTests.cs

@@ -1,146 +0,0 @@
-using FluentAssertions;
-using NUnit.Framework;
-using QuestPDF.Drawing;
-using SkiaSharp;
-using static SkiaSharp.SKFontStyleSlant;
-
-namespace QuestPDF.UnitTests
-{
-    [TestFixture]
-    public class FontStyleSetTests
-    {
-        private static void ExpectComparisonOrder(SKFontStyle target, SKFontStyle[] styles)
-        {
-            for (var i = 0; i < styles.Length - 1; i++)
-            {
-                var currentStyle = styles[i];
-                var nextStyle = styles[i + 1];
-                
-                FontStyleSet.IsBetterMatch(target, currentStyle, nextStyle).Should().BeTrue();
-                FontStyleSet.IsBetterMatch(target, nextStyle, currentStyle).Should().BeFalse();
-            }
-        }
-
-        [Test]
-        public void FontStyleSet_IsBetterMatch_CondensedWidth()
-        {
-            var styles = new[]
-            {
-                new SKFontStyle(500, 5, Upright),
-                new SKFontStyle(500, 4, Upright),
-                new SKFontStyle(500, 3, Upright),
-                new SKFontStyle(500, 6, Upright)
-            };
-            
-            ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
-        }
-
-        [Test]
-        public void FontStyleSet_IsBetterMatch_ExpandedWidth()
-        {
-            var styles = new[]
-            {
-                new SKFontStyle(500, 6, Upright),
-                new SKFontStyle(500, 7, Upright),
-                new SKFontStyle(500, 8, Upright),
-                new SKFontStyle(500, 5, Upright)
-            };
-            
-            ExpectComparisonOrder(new SKFontStyle(500, 6, Upright), styles);
-        }
-
-        [Test]
-        public void FontStyleSet_IsBetterMatch_ItalicSlant()
-        {
-            var styles = new[]
-            {
-                new SKFontStyle(500, 5, Italic),
-                new SKFontStyle(500, 5, Oblique),
-                new SKFontStyle(500, 5, Upright)
-            };
-        
-            ExpectComparisonOrder(new SKFontStyle(500, 5, Italic), styles);
-        }
-
-        [Test]
-        public void FontStyleSet_IsBetterMatch_ObliqueSlant()
-        {
-            var styles = new[]
-            {
-                new SKFontStyle(500, 5, Oblique),
-                new SKFontStyle(500, 5, Italic),
-                new SKFontStyle(500, 5, Upright)
-            };
-        
-            ExpectComparisonOrder(new SKFontStyle(500, 5, Oblique), styles);
-        }
-
-        [Test]
-        public void FontStyleSet_IsBetterMatch_UprightSlant()
-        {
-            var styles = new[]
-            {
-                new SKFontStyle(500, 5, Upright),
-                new SKFontStyle(500, 5, Oblique),
-                new SKFontStyle(500, 5, Italic)
-            };
-        
-            ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
-        }
-
-        [Test]
-        public void FontStyleSet_IsBetterMatch_ThinWeight()
-        {
-            var styles = new[]
-            {
-                new SKFontStyle(300, 5, Upright),
-                new SKFontStyle(200, 5, Upright),
-                new SKFontStyle(100, 5, Upright),
-                new SKFontStyle(400, 5, Upright)
-            };
-        
-            ExpectComparisonOrder(new SKFontStyle(300, 5, Upright), styles);
-        }
-
-        [Test]
-        public void FontStyleSet_IsBetterMatch_RegularWeight()
-        {
-            var styles = new[]
-            {
-                new SKFontStyle(500, 5, Upright),
-                new SKFontStyle(300, 5, Upright),
-                new SKFontStyle(100, 5, Upright),
-                new SKFontStyle(600, 5, Upright)
-            };
-        
-            ExpectComparisonOrder(new SKFontStyle(400, 5, Upright), styles);
-        }
-
-        [Test]
-        public void FontStyleSet_IsBetterMatch_BoldWeight()
-        {
-            var styles = new[]
-            {
-                new SKFontStyle(600, 5, Upright),
-                new SKFontStyle(700, 5, Upright),
-                new SKFontStyle(800, 5, Upright),
-                new SKFontStyle(500, 5, Upright)
-            };
-        
-            ExpectComparisonOrder(new SKFontStyle(600, 5, Upright), styles);
-        }
-
-        [Test]
-        public void FontStyleSet_RespectsPriority()
-        {
-            var styles = new[]
-            {
-                new SKFontStyle(600, 5, Italic),
-                new SKFontStyle(600, 6, Upright),
-                new SKFontStyle(500, 6, Italic)
-            };
-        
-            ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
-        }
-    }
-}

+ 2 - 2
Source/QuestPDF.UnitTests/ImageTests.cs

@@ -51,7 +51,7 @@ namespace QuestPDF.UnitTests
         [Test]
         [Test]
         public void Fluent_RecognizesImageProportions()
         public void Fluent_RecognizesImageProportions()
         {
         {
-            var image = GenerateDocumentImage(600, 200);
+            var image = GenerateDocumentImage(60, 20);
             
             
             TestPlan
             TestPlan
                 .For(x =>
                 .For(x =>
@@ -61,7 +61,7 @@ namespace QuestPDF.UnitTests
                     return container;
                     return container;
                 })
                 })
                 .MeasureElement(new Size(300, 200))
                 .MeasureElement(new Size(300, 200))
-                .CheckMeasureResult(SpacePlan.FullRender(300, 100));;
+                .CheckMeasureResult(SpacePlan.FullRender(300, 100));
         }
         }
         
         
         [Test]
         [Test]

+ 2 - 2
Source/QuestPDF.UnitTests/QuestPDF.UnitTests.csproj

@@ -8,9 +8,9 @@
 
 
     <ItemGroup>
     <ItemGroup>
         <PackageReference Include="FluentAssertions" Version="6.12.0" />
         <PackageReference Include="FluentAssertions" Version="6.12.0" />
-        <PackageReference Include="nunit" Version="4.0.1" />
+        <PackageReference Include="nunit" Version="4.1.0" />
         <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
         <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
-        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
         <PackageReference Include="SkiaSharp" Version="2.88.7" />
         <PackageReference Include="SkiaSharp" Version="2.88.7" />
     </ItemGroup>
     </ItemGroup>
 
 

+ 19 - 6
Source/QuestPDF.UnitTests/TestEngine/MockCanvas.cs

@@ -1,5 +1,7 @@
 using System;
 using System;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+using QuestPDF.Skia.Text;
 using SkiaSharp;
 using SkiaSharp;
 
 
 namespace QuestPDF.UnitTests.TestEngine
 namespace QuestPDF.UnitTests.TestEngine
@@ -9,17 +11,28 @@ namespace QuestPDF.UnitTests.TestEngine
         public Action<Position> TranslateFunc { get; set; }
         public Action<Position> TranslateFunc { get; set; }
         public Action<float> RotateFunc { get; set; }
         public Action<float> RotateFunc { get; set; }
         public Action<float, float> ScaleFunc { get; set; }
         public Action<float, float> ScaleFunc { get; set; }
-        public Action<SKImage, Position, Size> DrawImageFunc { get; set; }
-        public Action<Position, Size, string> DrawRectFunc { get; set; }
+        public Action<SkImage, Position, Size> DrawImageFunc { get; set; }
+        public Action<Position, Size, Color> DrawRectFunc { get; set; }
 
 
+        public void Save() => throw new NotImplementedException();
+        public void Restore() => throw new NotImplementedException();
+        
         public void Translate(Position vector) => TranslateFunc(vector);
         public void Translate(Position vector) => TranslateFunc(vector);
         public void Rotate(float angle) => RotateFunc(angle);
         public void Rotate(float angle) => RotateFunc(angle);
         public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);
         public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);
 
 
-        public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color);
-        public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException();
-        public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
-
+        public void DrawFilledRectangle(Position vector, Size size, Color color) => DrawRectFunc(vector, size, color);
+        public void DrawStrokeRectangle(Position vector, Size size, float strokeWidth, Color color) => throw new NotImplementedException();
+        public void DrawParagraph(SkParagraph paragraph) => throw new NotImplementedException();
+        public void DrawImage(SkImage image, Size size) => DrawImageFunc(image, Position.Zero, size);
+        public void DrawPicture(SkPicture picture) => throw new NotImplementedException();
+        public void DrawSvgPath(string path, Color color) => throw new NotImplementedException();
+        public void DrawSvg(SkSvgImage svgImage, Size size) => throw new NotImplementedException();
+        
+        public void DrawOverflowArea(SkRect area) => throw new NotImplementedException();
+        public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace) => throw new NotImplementedException();
+        public void ClipRectangle(SkRect clipArea) => throw new NotImplementedException();
+        
         public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
         public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
         public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
         public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
         public void DrawSection(string sectionName) => throw new NotImplementedException();
         public void DrawSection(string sectionName) => throw new NotImplementedException();

+ 17 - 5
Source/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs

@@ -1,22 +1,34 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+using QuestPDF.Skia.Text;
 using QuestPDF.UnitTests.TestEngine.Operations;
 using QuestPDF.UnitTests.TestEngine.Operations;
-using SkiaSharp;
 
 
 namespace QuestPDF.UnitTests.TestEngine
 namespace QuestPDF.UnitTests.TestEngine
 {
 {
     internal sealed class OperationRecordingCanvas : ICanvas
     internal sealed class OperationRecordingCanvas : ICanvas
     {
     {
         public ICollection<OperationBase> Operations { get; } = new List<OperationBase>();
         public ICollection<OperationBase> Operations { get; } = new List<OperationBase>();
-
+        
+        public void Save() => throw new NotImplementedException();
+        public void Restore() => throw new NotImplementedException();
+        
         public void Translate(Position vector) => Operations.Add(new CanvasTranslateOperation(vector));
         public void Translate(Position vector) => Operations.Add(new CanvasTranslateOperation(vector));
         public void Rotate(float angle) => Operations.Add(new CanvasRotateOperation(angle));
         public void Rotate(float angle) => Operations.Add(new CanvasRotateOperation(angle));
         public void Scale(float scaleX, float scaleY) => Operations.Add(new CanvasScaleOperation(scaleX, scaleY));
         public void Scale(float scaleX, float scaleY) => Operations.Add(new CanvasScaleOperation(scaleX, scaleY));
 
 
-        public void DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
-        public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException();
-        public void DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
+        public void DrawFilledRectangle(Position vector, Size size, Color color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
+        public void DrawStrokeRectangle(Position vector, Size size, float strokeWidth, Color color) => throw new NotImplementedException();
+        public void DrawParagraph(SkParagraph paragraph) => throw new NotImplementedException();
+        public void DrawImage(SkImage image, Size size) => Operations.Add(new CanvasDrawImageOperation(Position.Zero, size));
+        public void DrawPicture(SkPicture picture) => throw new NotImplementedException();
+        public void DrawSvgPath(string path, Color color) => throw new NotImplementedException();
+        public void DrawSvg(SkSvgImage svgImage, Size size) => throw new NotImplementedException();
+        
+        public void DrawOverflowArea(SkRect area) => throw new NotImplementedException();
+        public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace) => throw new NotImplementedException();
+        public void ClipRectangle(SkRect clipArea) => throw new NotImplementedException();
         
         
         public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
         public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
         public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
         public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();

+ 2 - 2
Source/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawRectangleOperation.cs

@@ -6,9 +6,9 @@ namespace QuestPDF.UnitTests.TestEngine.Operations
     {
     {
         public Position Position { get; } 
         public Position Position { get; } 
         public Size Size { get; }
         public Size Size { get; }
-        public string Color { get; }
+        public Color Color { get; }
 
 
-        public CanvasDrawRectangleOperation(Position position, Size size, string color)
+        public CanvasDrawRectangleOperation(Position position, Size size, Color color)
         {
         {
             Position = position;
             Position = position;
             Size = size;
             Size = size;

+ 2 - 2
Source/QuestPDF.UnitTests/TestEngine/TestPlan.cs

@@ -184,7 +184,7 @@ namespace QuestPDF.UnitTests.TestEngine
             return AddOperation(new CanvasRotateOperation(angle));
             return AddOperation(new CanvasRotateOperation(angle));
         }
         }
         
         
-        public TestPlan ExpectCanvasDrawRectangle(Position position, Size size, string color)
+        public TestPlan ExpectCanvasDrawRectangle(Position position, Size size, Color color)
         {
         {
             return AddOperation(new CanvasDrawRectangleOperation(position, size, color));
             return AddOperation(new CanvasDrawRectangleOperation(position, size, color));
         }
         }
@@ -240,7 +240,7 @@ namespace QuestPDF.UnitTests.TestEngine
                 {
                 {
                     CompressionQuality = ImageCompressionQuality.Medium,
                     CompressionQuality = ImageCompressionQuality.Medium,
                     TargetDpi = DocumentSettings.DefaultRasterDpi,
                     TargetDpi = DocumentSettings.DefaultRasterDpi,
-                    Source = Placeholders.Image
+                    Source = paylaod => Placeholders.Image(paylaod.ImageSize)
                 }
                 }
             };
             };
         }
         }

+ 9 - 20
Source/QuestPDF.UnitTests/TextStyleTests.cs

@@ -1,4 +1,5 @@
-using FluentAssertions;
+using System.Collections.Generic;
+using FluentAssertions;
 using NUnit.Framework;
 using NUnit.Framework;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
@@ -16,17 +17,12 @@ namespace QuestPDF.UnitTests
             var defaultTextStyle = TextStyle
             var defaultTextStyle = TextStyle
                 .Default
                 .Default
                 .FontSize(20)
                 .FontSize(20)
-                .FontFamily("Arial")
-                .BackgroundColor(Colors.Green.Lighten2)
-                .Fallback(y => y
-                    .FontFamily("Microsoft YaHei")
-                    .Underline()
-                    .NormalWeight()
-                    .BackgroundColor(Colors.Blue.Lighten2));
+                .FontFamily("Arial", "Microsoft YaHei")
+                .BackgroundColor(Colors.Green.Lighten2);
 
 
             var spanTextStyle = TextStyle
             var spanTextStyle = TextStyle
                 .Default
                 .Default
-                .FontFamily("Times New Roman")
+                .FontFamily("Times New Roman", "Arial", "Calibri")
                 .Bold()
                 .Bold()
                 .Strikethrough()
                 .Strikethrough()
                 .BackgroundColor(Colors.Red.Lighten2);
                 .BackgroundColor(Colors.Red.Lighten2);
@@ -37,22 +33,15 @@ namespace QuestPDF.UnitTests
             // assert
             // assert
             var expectedStyle = TextStyle.LibraryDefault with
             var expectedStyle = TextStyle.LibraryDefault with
             {
             {
+                Id = targetStyle.Id, // expect to break when adding new TextStyle properties, so use the real one
                 Size = 20, 
                 Size = 20, 
-                FontFamily = "Times New Roman",
+                FontFamilies = new[] { "Times New Roman", "Arial", "Calibri", "Microsoft YaHei", "Lato" },
                 FontWeight = FontWeight.Bold,
                 FontWeight = FontWeight.Bold,
                 BackgroundColor = Colors.Red.Lighten2,
                 BackgroundColor = Colors.Red.Lighten2,
-                HasStrikethrough = true,
-                Fallback = TextStyle.LibraryDefault with
-                {
-                    Size = 20,
-                    FontFamily = "Microsoft YaHei",
-                    FontWeight = FontWeight.Bold,
-                    BackgroundColor = Colors.Red.Lighten2,
-                    HasUnderline = true,
-                    HasStrikethrough = true
-                }
+                HasStrikethrough = true
             };
             };
 
 
+            spanTextStyle.Id.Should().BeGreaterThan(1);
             targetStyle.Should().BeEquivalentTo(expectedStyle);
             targetStyle.Should().BeEquivalentTo(expectedStyle);
         }
         }
     }
     }

+ 14 - 0
Source/QuestPDF/Build/net4/QuestPDF.targets

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+    <ItemGroup>
+        <None Include="$(MSBuildThisFileDirectory)\..\..\runtimes\**\*.*">
+            <Link>runtimes\%(RecursiveDir)%(Filename)%(Extension)</Link>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Include="$(MSBuildThisFileDirectory)\..\..\runtimes\any\native\LatoFont\*.*">
+            <Link>LatoFont\%(Filename)%(Extension)</Link>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+    </ItemGroup>
+</Project>

+ 3 - 16
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -1,6 +1,5 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Exceptions;
@@ -8,10 +7,9 @@ using QuestPDF.Drawing.Proxy;
 using QuestPDF.Elements;
 using QuestPDF.Elements;
 using QuestPDF.Elements.Text;
 using QuestPDF.Elements.Text;
 using QuestPDF.Elements.Text.Items;
 using QuestPDF.Elements.Text.Items;
-using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using QuestPDF.Previewer;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
@@ -22,10 +20,9 @@ namespace QuestPDF.Drawing
             NativeDependencyCompatibilityChecker.Test();
             NativeDependencyCompatibilityChecker.Test();
         }
         }
         
         
-        internal static void GeneratePdf(Stream stream, IDocument document)
+        internal static void GeneratePdf(SkWriteStream stream, IDocument document)
         {
         {
             ValidateLicense();
             ValidateLicense();
-            CheckIfStreamIsCompatible(stream);
             
             
             var metadata = document.GetMetadata();
             var metadata = document.GetMetadata();
             var settings = document.GetSettings();
             var settings = document.GetSettings();
@@ -33,24 +30,14 @@ namespace QuestPDF.Drawing
             RenderDocument(canvas, document, settings);
             RenderDocument(canvas, document, settings);
         }
         }
         
         
-        internal static void GenerateXps(Stream stream, IDocument document)
+        internal static void GenerateXps(SkWriteStream stream, IDocument document)
         {
         {
             ValidateLicense();
             ValidateLicense();
-            CheckIfStreamIsCompatible(stream);
             
             
             var settings = document.GetSettings();
             var settings = document.GetSettings();
             var canvas = new XpsCanvas(stream, settings);
             var canvas = new XpsCanvas(stream, settings);
             RenderDocument(canvas, document, settings);
             RenderDocument(canvas, document, settings);
         }
         }
-
-        private static void CheckIfStreamIsCompatible(Stream stream)
-        {
-            if (!stream.CanWrite)
-                throw new ArgumentException("The library requires a Stream object with the 'write' capability available (the CanWrite flag). Please consider using the MemoryStream class.");
-            
-            if (!stream.CanSeek)
-                throw new ArgumentException("The library requires a Stream object with the 'seek' capability available (the CanSeek flag). Please consider using the MemoryStream class.");
-        }
         
         
         internal static ICollection<byte[]> GenerateImages(IDocument document, ImageGenerationSettings imageGenerationSettings)
         internal static ICollection<byte[]> GenerateImages(IDocument document, ImageGenerationSettings imageGenerationSettings)
         {
         {

+ 5 - 0
Source/QuestPDF/Drawing/Exceptions/InitializationException.cs

@@ -4,6 +4,11 @@ namespace QuestPDF.Drawing.Exceptions
 {
 {
     public class InitializationException : Exception
     public class InitializationException : Exception
     {
     {
+        internal InitializationException(string message) : base(message)
+        {
+            
+        }
+        
         internal InitializationException(string message, Exception inner) : base(message, inner)
         internal InitializationException(string message, Exception inner) : base(message, inner)
         {
         {
             
             

+ 14 - 234
Source/QuestPDF/Drawing/FontManager.cs

@@ -1,15 +1,11 @@
 using System;
 using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Reflection;
 using System.Reflection;
-using HarfBuzzSharp;
-using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
-using SkiaSharp.HarfBuzz;
+using QuestPDF.Skia;
+using QuestPDF.Skia.Text;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
@@ -20,36 +16,19 @@ namespace QuestPDF.Drawing
     /// </summary>
     /// </summary>
     public static class FontManager
     public static class FontManager
     {
     {
-        private static readonly ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
-        private static readonly ConcurrentDictionary<TextStyle, FontMetrics> FontMetrics = new();
-        private static readonly ConcurrentDictionary<TextStyle, SKPaint> FontPaints = new();
-        private static readonly ConcurrentDictionary<string, SKPaint> ColorPaints = new();
-        private static readonly ConcurrentDictionary<TextStyle, Font> ShaperFonts = new();
-        private static readonly ConcurrentDictionary<TextStyle, SKFont> Fonts = new();
-        private static readonly ConcurrentDictionary<TextStyle, TextShaper> TextShapers = new();
+        private static SkTypefaceProvider TypefaceProvider { get; } = new();
+        
+        private static SkFontCollection LocalFontCollection { get; } = SkFontCollection.Create(TypefaceProvider, SkFontManager.Local);
+        private static SkFontCollection GlobalFontCollection { get; } = SkFontCollection.Create(TypefaceProvider, SkFontManager.Global);
+        
+        internal static SkFontCollection CurrentFontCollection => Settings.UseEnvironmentFonts ? GlobalFontCollection : LocalFontCollection;
+        internal static SkFontManager CurrentFontManager => Settings.UseEnvironmentFonts ? SkFontManager.Global : SkFontManager.Local;
 
 
         static FontManager()
         static FontManager()
         {
         {
             NativeDependencyCompatibilityChecker.Test();
             NativeDependencyCompatibilityChecker.Test();
-            RegisterLibraryDefaultFonts();
         }
         }
         
         
-        private static void RegisterFontType(SKData fontData, string? customName = null)
-        {
-            foreach (var index in Enumerable.Range(0, 256))
-            {
-                var typeface = SKTypeface.FromData(fontData, index);
-                
-                if (typeface == null)
-                    break;
-                
-                var typefaceName = customName ?? typeface.FamilyName;
-
-                var fontStyleSet = StyleSets.GetOrAdd(typefaceName, _ => new FontStyleSet());
-                fontStyleSet.Add(typeface);
-            }
-        }
-
         [Obsolete("Since version 2022.8 this method has been renamed. Please use the RegisterFontWithCustomName method.")]
         [Obsolete("Since version 2022.8 this method has been renamed. Please use the RegisterFontWithCustomName method.")]
         public static void RegisterFontType(string fontName, Stream stream)
         public static void RegisterFontType(string fontName, Stream stream)
         {
         {
@@ -63,9 +42,9 @@ namespace QuestPDF.Drawing
         /// </summary>
         /// </summary>
         public static void RegisterFontWithCustomName(string fontName, Stream stream)
         public static void RegisterFontWithCustomName(string fontName, Stream stream)
         {
         {
-            using var fontData = SKData.Create(stream);
-            RegisterFontType(fontData);
-            RegisterFontType(fontData, customName: fontName);
+            using var fontData = SkData.FromStream(stream);
+            TypefaceProvider.AddTypefaceFromData(fontData);
+            TypefaceProvider.AddTypefaceFromData(fontData, fontName);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -74,8 +53,8 @@ namespace QuestPDF.Drawing
         /// </summary>
         /// </summary>
         public static void RegisterFont(Stream stream)
         public static void RegisterFont(Stream stream)
         {
         {
-            using var fontData = SKData.Create(stream);
-            RegisterFontType(fontData);
+            using var fontData = SkData.FromStream(stream);
+            TypefaceProvider.AddTypefaceFromData(fontData);
         }
         }
         
         
         /// <summary>
         /// <summary>
@@ -92,204 +71,5 @@ namespace QuestPDF.Drawing
             
             
             RegisterFont(stream);
             RegisterFont(stream);
         }
         }
-        
-        private static void RegisterLibraryDefaultFonts()
-        {
-            var fontFileNames = new[]
-            {
-                "Lato-Black.ttf",
-                "Lato-BlackItalic.ttf",
-                
-                "Lato-Bold.ttf",
-                "Lato-BoldItalic.ttf",
-                
-                "Lato-Regular.ttf",
-                "Lato-Italic.ttf",
-                
-                "Lato-Light.ttf",
-                "Lato-LightItalic.ttf",
-                
-                "Lato-Thin.ttf",
-                "Lato-ThinItalic.ttf"
-            };
-            
-            foreach (var fileName in fontFileNames)
-            {
-                var filePath = $"QuestPDF.Resources.DefaultFont.{fileName}";
-                
-                using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filePath);
-                RegisterFont(stream);
-            }
-        }
-
-        internal static SKPaint ColorToPaint(this string color)
-        {
-            return ColorPaints.GetOrAdd(color, Convert);
-
-            static SKPaint Convert(string color)
-            {
-                return new SKPaint
-                {
-                    Color = SKColor.Parse(color),
-                    IsAntialias = true
-                };
-            }
-        }
-
-        internal static SKPaint ToPaint(this TextStyle style)
-        {
-            return FontPaints.GetOrAdd(style, Convert);
-
-            static SKPaint Convert(TextStyle style)
-            {
-                var targetTypeface = GetTypeface(style);
-                
-                return new SKPaint
-                {
-                    Color = SKColor.Parse(style.Color),
-                    Typeface = targetTypeface,
-                    TextSize = (style.Size ?? 12) * GetTextScale(style),
-                    IsAntialias = true,
-                    TextSkewX = GetTextSkew(style, targetTypeface),
-                    FakeBoldText = UseFakeBoldText(style, targetTypeface)
-                };
-            }
-
-            static SKTypeface GetTypeface(TextStyle style)
-            {
-                var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal);
-
-                // superscript and subscript use slightly bolder font to match visually line thickness
-                if (style.FontPosition is FontPosition.Superscript or FontPosition.Subscript)
-                {
-                    var weightValue = (int)weight;
-                    weightValue = Math.Min(weightValue + 100, 1000);
-                    
-                    weight = (SKFontStyleWeight) (weightValue);
-                }
-
-                var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
-
-                var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant);
-
-                if (StyleSets.TryGetValue(style.FontFamily, out var fontStyleSet))
-                    return fontStyleSet.Match(fontStyle);
-
-                var fontFromDefaultSource = SKFontManager.Default.MatchFamily(style.FontFamily, fontStyle);
-                
-                if (fontFromDefaultSource != null)
-                    return fontFromDefaultSource;
-
-                var availableFontNames = string.Join(", ", SKFontManager.Default.GetFontFamilies());
-                
-                throw new ArgumentException(
-                    $"The typeface '{style.FontFamily}' could not be found. " +
-                    $"Please consider the following options: " +
-                    $"1) install the font on your operating system or execution environment. " +
-                    $"2) load a font file specifically for QuestPDF usage via the QuestPDF.Drawing.FontManager.RegisterFontType(Stream fileContentStream) static method. " +
-                    $"Available font family names: [{availableFontNames}]");
-            }
-            
-            static float GetTextScale(TextStyle style)
-            {
-                return style.FontPosition switch
-                {
-                    FontPosition.Normal => 1f,
-                    FontPosition.Subscript => 0.625f,
-                    FontPosition.Superscript => 0.625f,
-                    _ => throw new ArgumentOutOfRangeException()
-                };
-            }
-
-            static float GetTextSkew(TextStyle originalTextStyle, SKTypeface targetTypeface)
-            {
-                // requested italic text but got typeface that is not italic
-                var useObliqueText = originalTextStyle.IsItalic == true && !targetTypeface.IsItalic;
-                
-                return useObliqueText ? -0.25f : 0;
-            }
-            
-            static bool UseFakeBoldText(TextStyle originalTextStyle, SKTypeface targetTypeface)
-            {
-                // requested bold text but got typeface that is not bold
-                return originalTextStyle.FontWeight > FontWeight.Medium && !targetTypeface.IsBold;
-            }
-        }
-
-        internal static FontMetrics ToFontMetrics(this TextStyle style)
-        {
-            return FontMetrics.GetOrAdd(style, key =>
-            {
-                var skiaFontMetrics = key.NormalPosition().ToPaint().FontMetrics;
-                
-                return new FontMetrics
-                {
-                    Ascent = skiaFontMetrics.Ascent,
-                    Descent = skiaFontMetrics.Descent,
-                    
-                    UnderlineThickness = GetUnderlineThickness(),
-                    UnderlinePosition = GetUnderlinePosition(),
-                    
-                    StrikeoutThickness = GetStrikeoutThickness(),
-                    StrikeoutPosition = GetStrikeoutPosition()
-                };
-
-                // HACK: On MacOS, certain font metrics are not determined accurately.
-                // Provide defaults based on other metrics.
-                
-                float GetUnderlineThickness()
-                {
-                    return skiaFontMetrics.UnderlineThickness ?? (skiaFontMetrics.XHeight * 0.15f);
-                }
-                
-                float GetUnderlinePosition()
-                {
-                    return skiaFontMetrics.UnderlinePosition ?? (skiaFontMetrics.XHeight * 0.2f);
-                }
-                
-                float GetStrikeoutThickness()
-                {
-                    return skiaFontMetrics.StrikeoutThickness ?? (skiaFontMetrics.XHeight * 0.15f);
-                }
-                
-                float GetStrikeoutPosition()
-                {
-                    return skiaFontMetrics.StrikeoutPosition ?? (-skiaFontMetrics.XHeight * 0.6f);
-                }
-            });
-        }
-
-        internal static Font ToShaperFont(this TextStyle style)
-        {
-            return ShaperFonts.GetOrAdd(style, key =>
-            {
-                var typeface = key.ToPaint().Typeface;
-
-                using var harfBuzzBlob = typeface.OpenStream(out var ttcIndex).ToHarfBuzzBlob();
-                
-                using var face = new Face(harfBuzzBlob, ttcIndex)
-                {
-                    Index = ttcIndex,
-                    UnitsPerEm = typeface.UnitsPerEm,
-                    GlyphCount = typeface.GlyphCount
-                };
-                
-                var font = new Font(face);
-                font.SetScale(TextShaper.FontShapingScale, TextShaper.FontShapingScale);
-                font.SetFunctionsOpenType();
-
-                return font;
-            });
-        }
-        
-        internal static TextShaper ToTextShaper(this TextStyle style)
-        {
-            return TextShapers.GetOrAdd(style, key => new TextShaper(key));
-        }
-        
-        internal static SKFont ToFont(this TextStyle style)
-        {
-            return Fonts.GetOrAdd(style, key => key.ToPaint().ToFont());
-        }
     }
     }
 }
 }

+ 0 - 132
Source/QuestPDF/Drawing/FontStyleSet.cs

@@ -1,132 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.Concurrent;
-using SkiaSharp;
-
-namespace QuestPDF.Drawing
-{
-    internal sealed class FontStyleSet
-    {
-        private ConcurrentDictionary<SKFontStyle, SKTypeface> Styles { get; } = new();
-
-        public void Add(SKTypeface typeface)
-        {
-            var style = typeface.FontStyle;
-            Styles.AddOrUpdate(style, _ => typeface, (_, _) => typeface);
-        }
-
-        public SKTypeface? Match(SKFontStyle target)
-        {
-            SKFontStyle? bestStyle = null;
-            SKTypeface? bestTypeface = null;
-
-            foreach (var entry in Styles)
-            {
-                if (IsBetterMatch(target, entry.Key, bestStyle))
-                {
-                    bestStyle = entry.Key;
-                    bestTypeface = entry.Value;
-                }
-            }
-
-            return bestTypeface;
-        }
-
-        private static Dictionary<SKFontStyleSlant, List<SKFontStyleSlant>> SlantFallbacks = new()
-        {
-            { SKFontStyleSlant.Italic, new() { SKFontStyleSlant.Italic, SKFontStyleSlant.Oblique, SKFontStyleSlant.Upright } },
-            { SKFontStyleSlant.Oblique, new() { SKFontStyleSlant.Oblique, SKFontStyleSlant.Italic, SKFontStyleSlant.Upright } },
-            { SKFontStyleSlant.Upright, new() { SKFontStyleSlant.Upright, SKFontStyleSlant.Oblique, SKFontStyleSlant.Italic } },
-        };
-
-        // Checks whether style a is a better match for the target then style b. Uses the CSS font style matching algorithm
-        internal static bool IsBetterMatch(SKFontStyle? target, SKFontStyle? a, SKFontStyle? b)
-        {
-            // A font is better than no font
-            if (b == null) 
-                return true;
-            
-            if (a == null) 
-                return false;
-
-            // First check font width
-            // For normal and condensed widths prefer smaller widths
-            // For expanded widths prefer larger widths
-            if (target.Width <= (int)SKFontStyleWidth.Normal)
-            {
-                if (a.Width <= target.Width && b.Width > target.Width) 
-                    return true;
-                
-                if (a.Width > target.Width && b.Width <= target.Width) 
-                    return false;
-            }
-            else
-            {
-                if (a.Width >= target.Width && b.Width < target.Width) 
-                    return true;
-                
-                if (a.Width < target.Width && b.Width >= target.Width) 
-                    return false;
-            }
-
-            // Prefer closest match
-            var widthDifferenceA = Math.Abs(a.Width - target.Width);
-            var widthDifferenceB = Math.Abs(b.Width - target.Width);
-
-            if (widthDifferenceA < widthDifferenceB) 
-                return true;
-            
-            if (widthDifferenceB < widthDifferenceA) 
-                return false;
-
-            // Prefer closest slant based on provided fallback list
-            var slantFallback = SlantFallbacks[target.Slant];
-            var slantIndexA = slantFallback.IndexOf(a.Slant);
-            var slantIndexB = slantFallback.IndexOf(b.Slant);
-
-            if (slantIndexA < slantIndexB) 
-                return true;
-            
-            if (slantIndexB < slantIndexA) 
-                return false;
-
-            // Check weight last
-            // For thin (<400) weights, prefer thinner weights
-            // For regular (400-500) weights, prefer other regular weights, then use rule for thin or bold
-            // For bold (>500) weights, prefer thicker weights
-            // Behavior for values other than multiples of 100 is not given in the specification
-
-            if (target.Weight >= 400 && target.Weight <= 500)
-            {
-                if ((a.Weight >= 400 && a.Weight <= 500) && !(b.Weight >= 400 && b.Weight <= 500)) 
-                    return true;
-                
-                if (!(a.Weight >= 400 && a.Weight <= 500) && (b.Weight >= 400 && b.Weight <= 500)) 
-                    return false;
-            }
-
-            if (target.Weight < 450)
-            {
-                if (a.Weight <= target.Weight && b.Weight > target.Weight) 
-                    return true;
-                
-                if (a.Weight > target.Weight && b.Weight <= target.Weight)
-                    return false;
-            }
-            else
-            {
-                if (a.Weight >= target.Weight && b.Weight < target.Weight) 
-                    return true;
-                
-                if (a.Weight < target.Weight && b.Weight >= target.Weight) 
-                    return false;
-            }
-
-            // Prefer closest weight
-            var weightDifferenceA = Math.Abs(a.Weight - target.Weight);
-            var weightDifferenceB = Math.Abs(b.Weight - target.Weight);
-
-            return weightDifferenceA < weightDifferenceB;
-        }
-    }
-}

+ 50 - 4
Source/QuestPDF/Drawing/FreeCanvas.cs

@@ -1,5 +1,6 @@
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
+using QuestPDF.Skia;
+using QuestPDF.Skia.Text;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
@@ -38,26 +39,71 @@ namespace QuestPDF.Drawing
 
 
         #region ICanvas
         #region ICanvas
 
 
+        public void Save()
+        {
+            
+        }
+
+        public void Restore()
+        {
+            
+        }
+        
         public void Translate(Position vector)
         public void Translate(Position vector)
         {
         {
             
             
         }
         }
 
 
-        public void DrawRectangle(Position vector, Size size, string color)
+        public void DrawFilledRectangle(Position vector, Size size, Color color)
+        {
+            
+        }
+
+        public void DrawStrokeRectangle(Position vector, Size size, float strokeWidth, Color color)
+        {
+            
+        }
+        
+        public void DrawParagraph(SkParagraph paragraph)
+        {
+            
+        }
+
+        public void DrawImage(SkImage image, Size size)
+        {
+            
+        }
+
+        public void DrawPicture(SkPicture picture)
+        {
+            
+        }
+
+        public void DrawSvgPath(string path, Color color)
+        {
+            
+        }
+
+        public void DrawSvg(SkSvgImage svgImage, Size size)
         {
         {
             
             
         }
         }
 
 
-        public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style)
+        public void DrawOverflowArea(SkRect area)
         {
         {
             
             
         }
         }
 
 
-        public void DrawImage(SKImage image, Position position, Size size)
+        public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace)
         {
         {
             
             
         }
         }
 
 
+        public void ClipRectangle(SkRect clipArea)
+        {
+            
+        }
+        
         public void DrawHyperlink(string url, Size size)
         public void DrawHyperlink(string url, Size size)
         {
         {
            
            

+ 26 - 14
Source/QuestPDF/Drawing/ImageCanvas.cs

@@ -1,15 +1,18 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
     internal sealed class ImageCanvas : SkiaCanvasBase
     internal sealed class ImageCanvas : SkiaCanvasBase
     {
     {
         private ImageGenerationSettings Settings { get; }
         private ImageGenerationSettings Settings { get; }
-        private SKSurface Surface { get; set; }
+        private SkBitmap Bitmap { get; set; }
 
 
+        // TODO: consider using SkSurface to cache drawing operations and then encode the surface to an image at the very end of the generation process      
+        // this change should reduce memory usage and improve performance
         internal ICollection<byte[]> Images { get; } = new List<byte[]>();
         internal ICollection<byte[]> Images { get; } = new List<byte[]>();
         
         
         public ImageCanvas(ImageGenerationSettings settings)
         public ImageCanvas(ImageGenerationSettings settings)
@@ -24,29 +27,38 @@ namespace QuestPDF.Drawing
 
 
         public override void EndDocument()
         public override void EndDocument()
         {
         {
-            Canvas?.Dispose();
-            Surface?.Dispose();
+            
         }
         }
 
 
         public override void BeginPage(Size size)
         public override void BeginPage(Size size)
         {
         {
             var scalingFactor = Settings.RasterDpi / (float) PageSizes.PointsPerInch;
             var scalingFactor = Settings.RasterDpi / (float) PageSizes.PointsPerInch;
-            var imageInfo = new SKImageInfo((int) (size.Width * scalingFactor), (int) (size.Height * scalingFactor));
-            
-            Surface = SKSurface.Create(imageInfo);
-            Canvas = Surface.Canvas;
+
+            Bitmap = new SkBitmap((int) (size.Width * scalingFactor), (int) (size.Height * scalingFactor));
+            Canvas = SkCanvas.CreateFromBitmap(Bitmap);
             
             
-            Canvas.Scale(scalingFactor);
+            Canvas.Scale(scalingFactor, scalingFactor);
         }
         }
 
 
         public override void EndPage()
         public override void EndPage()
         {
         {
             Canvas.Save();
             Canvas.Save();
-            var image = Surface.Snapshot().Encode(Settings.ImageFormat.ToSkImageFormat(), Settings.ImageCompressionQuality.ToQualityValue()).ToArray();
-            Images.Add(image);
+            using var imageData = EncodeBitmap();
+            var imageBytes = imageData.ToBytes();
+            Images.Add(imageBytes);
             
             
-            Canvas.Dispose();
-            Surface.Dispose();
+            Bitmap.Dispose();
+
+            SkData EncodeBitmap()
+            {
+                return Settings.ImageFormat switch
+                {
+                    ImageFormat.Jpeg => Bitmap.EncodeAsJpeg(Settings.ImageCompressionQuality.ToQualityValue()),
+                    ImageFormat.Png => Bitmap.EncodeAsPng(),
+                    ImageFormat.Webp => Bitmap.EncodeAsWebp(Settings.ImageCompressionQuality.ToQualityValue()),
+                    _ => throw new ArgumentOutOfRangeException()
+                };
+            }
         }
         }
     }
     }
 }
 }

+ 32 - 24
Source/QuestPDF/Drawing/PdfCanvas.cs

@@ -3,48 +3,56 @@ using System.IO;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
     internal sealed class PdfCanvas : SkiaDocumentCanvasBase
     internal sealed class PdfCanvas : SkiaDocumentCanvasBase
     {
     {
-        public PdfCanvas(Stream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings) 
+        public PdfCanvas(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings) 
             : base(CreatePdf(stream, documentMetadata, documentSettings))
             : base(CreatePdf(stream, documentMetadata, documentSettings))
         {
         {
             
             
         }
         }
 
 
-        private static SKDocument CreatePdf(Stream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings)
+        private static SkDocument CreatePdf(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings)
         {
         {
+            // do not extract to another method, as it will cause the SkText objects
+            // to be disposed before the SkPdfDocument is created
+            using var title = new SkText(documentMetadata.Title);
+            using var author = new SkText(documentMetadata.Author);
+            using var subject = new SkText(documentMetadata.Subject);
+            using var keywords = new SkText(documentMetadata.Keywords);
+            using var creator = new SkText(documentMetadata.Creator);
+            using var producer = new SkText(documentMetadata.Producer);
+            
+            var internalMetadata = new SkPdfDocumentMetadata
+            {
+                Title = title,
+                Author = author,
+                Subject = subject,
+                Keywords = keywords,
+                Creator = creator,
+                Producer = producer,
+                
+                CreationDate = new SkDateTime(documentMetadata.CreationDate),
+                ModificationDate = new SkDateTime(documentMetadata.ModifiedDate),
+                
+                RasterDPI = documentSettings.ImageRasterDpi,
+                ImageEncodingQuality = documentSettings.ImageCompressionQuality.ToQualityValue(),
+                
+                SupportPDFA = documentSettings.PdfA,
+                CompressDocument = documentSettings.CompressDocument
+            };
+            
             try
             try
             {
             {
-                return SKDocument.CreatePdf(stream, MapMetadata(documentMetadata, documentSettings));
+                return SkPdfDocument.Create(stream, internalMetadata);
             }
             }
             catch (TypeInitializationException exception)
             catch (TypeInitializationException exception)
             {
             {
                 throw new InitializationException("PDF", exception);
                 throw new InitializationException("PDF", exception);
             }
             }
         }
         }
-
-        private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata, DocumentSettings documentSettings)
-        {
-            return new SKDocumentPdfMetadata
-            {
-                Title = metadata.Title,
-                Author = metadata.Author,
-                Subject = metadata.Subject,
-                Keywords = metadata.Keywords,
-                Creator = metadata.Creator,
-                Producer = metadata.Producer,
-                
-                Creation = metadata.CreationDate,
-                Modified = metadata.ModifiedDate,
-                
-                RasterDpi = documentSettings.ImageRasterDpi,
-                EncodingQuality = documentSettings.ImageCompressionQuality.ToQualityValue(),
-                PdfA = documentSettings.PdfA
-            };
-        }
     }
     }
 }
 }

+ 19 - 7
Source/QuestPDF/Drawing/PreviewerCanvas.cs

@@ -1,19 +1,31 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
     internal class PreviewerPageSnapshot
     internal class PreviewerPageSnapshot
     {
     {
-        public SKPicture Picture { get; set; }
+        public SkPicture Picture { get; set; }
         public Size Size { get; set; }
         public Size Size { get; set; }
 
 
-        public PreviewerPageSnapshot(SKPicture picture, Size size)
+        public PreviewerPageSnapshot(SkPicture picture, Size size)
         {
         {
             Picture = picture;
             Picture = picture;
             Size = size;
             Size = size;
         }
         }
+        
+        public byte[] RenderImage(int zoomLevel)
+        {
+            var scale = (float)Math.Pow(2, zoomLevel);
+            
+            using var bitmap = new SkBitmap((int)(Size.Width * scale), (int)(Size.Height * scale));
+            using var canvas = SkCanvas.CreateFromBitmap(bitmap);
+            canvas.Scale(scale, scale);
+            canvas.DrawPicture(Picture);
+            return bitmap.EncodeAsJpeg(90).ToBytes();
+        }
     }
     }
     
     
     internal class PreviewerDocumentSnapshot
     internal class PreviewerDocumentSnapshot
@@ -24,7 +36,7 @@ namespace QuestPDF.Drawing
     
     
     internal class PreviewerCanvas : SkiaCanvasBase
     internal class PreviewerCanvas : SkiaCanvasBase
     {
     {
-        private SKPictureRecorder? PictureRecorder { get; set; }
+        private SkPictureRecorder? PictureRecorder { get; set; }
         private Size? CurrentPageSize { get; set; }
         private Size? CurrentPageSize { get; set; }
 
 
         private ICollection<PreviewerPageSnapshot> PageSnapshots { get; } = new List<PreviewerPageSnapshot>();
         private ICollection<PreviewerPageSnapshot> PageSnapshots { get; } = new List<PreviewerPageSnapshot>();
@@ -37,9 +49,9 @@ namespace QuestPDF.Drawing
         public override void BeginPage(Size size)
         public override void BeginPage(Size size)
         {
         {
             CurrentPageSize = size;
             CurrentPageSize = size;
-            PictureRecorder = new SKPictureRecorder();
+            PictureRecorder = new SkPictureRecorder();
 
 
-            Canvas = PictureRecorder.BeginRecording(new SKRect(0, 0, size.Width, size.Height));
+            Canvas = PictureRecorder.BeginRecording(size.Width, size.Height);
         }
         }
 
 
         public override void EndPage()
         public override void EndPage()

+ 67 - 23
Source/QuestPDF/Drawing/SkiaCanvasBase.cs

@@ -1,12 +1,13 @@
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
+using QuestPDF.Skia;
+using QuestPDF.Skia.Text;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
     internal abstract class SkiaCanvasBase : ICanvas, IRenderingCanvas
     internal abstract class SkiaCanvasBase : ICanvas, IRenderingCanvas
     {
     {
-        internal SKCanvas Canvas { get; set; }
+        internal SkCanvas Canvas { get; set; }
 
 
         #region IRenderingCanvas
         #region IRenderingCanvas
         
         
@@ -39,67 +40,110 @@ namespace QuestPDF.Drawing
         private void DrawLayoutIssuesIndicatorOnCurrentPage()
         private void DrawLayoutIssuesIndicatorOnCurrentPage()
         {
         {
             // visual configuration
             // visual configuration
-            const string lineColor = Colors.Red.Medium;
+            var lineColor = Colors.Red.Medium;
             const byte lineOpacity = 64;
             const byte lineOpacity = 64;
-            const float borderThickness = 24f;
         
         
             // implementation
             // implementation
-            using var indicatorPaint = new SKPaint
-            {
-                StrokeWidth = borderThickness * 2, // half of the width will be outside of the page area
-                Color = SKColor.Parse(lineColor).WithAlpha(lineOpacity),
-                IsStroke = true
-            };
-        
-            Canvas.DrawRect(0, 0, CurrentPageSize.Width, CurrentPageSize.Height, indicatorPaint);
+            var indicatorColor = lineColor.WithAlpha(lineOpacity);
+            var position = new SkRect(0, 0, CurrentPageSize.Width, CurrentPageSize.Height);
+            Canvas.DrawFilledRectangle(position, indicatorColor);
         }
         }
         
         
         #endregion
         #endregion
         
         
         #region ICanvas
         #region ICanvas
         
         
+        public void Save()
+        {
+            Canvas.Save();
+        }
+
+        public void Restore()
+        {
+            Canvas.Restore();
+        }
+        
         public void Translate(Position vector)
         public void Translate(Position vector)
         {
         {
             Canvas.Translate(vector.X, vector.Y);
             Canvas.Translate(vector.X, vector.Y);
         }
         }
 
 
-        public void DrawRectangle(Position vector, Size size, string color)
+        public void DrawFilledRectangle(Position vector, Size size, Color color)
         {
         {
             if (size.Width < Size.Epsilon || size.Height < Size.Epsilon)
             if (size.Width < Size.Epsilon || size.Height < Size.Epsilon)
                 return;
                 return;
 
 
-            var paint = color.ColorToPaint();
-            Canvas.DrawRect(vector.X, vector.Y, size.Width, size.Height, paint);
+            var position = new SkRect(vector.X, vector.Y, vector.X + size.Width, vector.Y + size.Height);
+            Canvas.DrawFilledRectangle(position, color);
         }
         }
+        
+        public void DrawStrokeRectangle(Position vector, Size size, float strokeWidth, Color color)
+        {
+            if (size.Width < Size.Epsilon || size.Height < Size.Epsilon)
+                return;
 
 
-        public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style)
+            var position = new SkRect(vector.X, vector.Y, vector.X + size.Width, vector.Y + size.Height);
+            Canvas.DrawStrokeRectangle(position, strokeWidth, color);
+        }
+
+        public void DrawParagraph(SkParagraph paragraph)
         {
         {
-            Canvas.DrawText(skTextBlob, position.X, position.Y, style.ToPaint());
+            Canvas.DrawParagraph(paragraph);
         }
         }
 
 
-        public void DrawImage(SKImage image, Position vector, Size size)
+        public void DrawImage(SkImage image, Size size)
         {
         {
-            Canvas.DrawImage(image, new SKRect(vector.X, vector.Y, size.Width, size.Height));
+            Canvas.DrawImage(image, size.Width, size.Height);
         }
         }
 
 
+        public void DrawPicture(SkPicture picture)
+        {
+            Canvas.DrawPicture(picture);
+        }
+
+        public void DrawSvgPath(string path, Color color)
+        {
+            Canvas.DrawSvgPath(path, color);
+        }
+
+        public void DrawSvg(SkSvgImage svgImage, Size size)
+        {
+            Canvas.DrawSvg(svgImage, size.Width, size.Height);
+        }
+        
+        public void DrawOverflowArea(SkRect area)
+        {
+            Canvas.DrawOverflowArea(area);
+        }
+    
+        public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace)
+        {
+            Canvas.ClipOverflowArea(availableSpace, requiredSpace);
+        }
+    
+        public void ClipRectangle(SkRect clipArea)
+        {
+            Canvas.ClipRectangle(clipArea);
+        }
+        
         public void DrawHyperlink(string url, Size size)
         public void DrawHyperlink(string url, Size size)
         {
         {
-            Canvas.DrawUrlAnnotation(new SKRect(0, 0, size.Width, size.Height), url);
+            Canvas.AnnotateUrl(size.Width, size.Height, url);
         }
         }
         
         
         public void DrawSectionLink(string sectionName, Size size)
         public void DrawSectionLink(string sectionName, Size size)
         {
         {
-            Canvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), sectionName);
+            Canvas.AnnotateDestinationLink(size.Width, size.Height, sectionName);
         }
         }
 
 
         public void DrawSection(string sectionName)
         public void DrawSection(string sectionName)
         {
         {
-            Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), sectionName);
+            Canvas.AnnotateDestination(sectionName);
         }
         }
 
 
         public void Rotate(float angle)
         public void Rotate(float angle)
         {
         {
-            Canvas.RotateDegrees(angle);
+            Canvas.Rotate(angle);
         }
         }
 
 
         public void Scale(float scaleX, float scaleY)
         public void Scale(float scaleX, float scaleY)

+ 3 - 3
Source/QuestPDF/Drawing/SkiaDocumentCanvasBase.cs

@@ -1,13 +1,13 @@
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
     internal class SkiaDocumentCanvasBase : SkiaCanvasBase
     internal class SkiaDocumentCanvasBase : SkiaCanvasBase
     {
     {
-        private SKDocument? Document { get; }
+        private SkDocument? Document { get; }
 
 
-        protected SkiaDocumentCanvasBase(SKDocument document)
+        protected SkiaDocumentCanvasBase(SkDocument document)
         {
         {
             Document = document;
             Document = document;
         }
         }

+ 1 - 2
Source/QuestPDF/Drawing/SpacePlan.cs

@@ -1,5 +1,4 @@
-using System.Collections.Generic;
-using QuestPDF.Infrastructure;
+using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {

+ 0 - 224
Source/QuestPDF/Drawing/TextShaper.cs

@@ -1,224 +0,0 @@
-using System;
-using System.Linq;
-using HarfBuzzSharp;
-using QuestPDF.Infrastructure;
-using SkiaSharp;
-using Buffer = HarfBuzzSharp.Buffer;
-
-namespace QuestPDF.Drawing
-{
-    internal sealed class TextShaper
-    {
-        public const int FontShapingScale = 512;
-        
-        private TextStyle TextStyle { get; }
-
-        private SKFont Font => TextStyle.ToFont();
-        private Font ShaperFont => TextStyle.ToShaperFont();
-        private SKPaint Paint => TextStyle.ToPaint();
-
-        public TextShaper(TextStyle textStyle)
-        {
-            TextStyle = textStyle;
-        }
-
-        public TextShapingResult Shape(string text)
-        {
-            using var buffer = new Buffer();
-            
-            PopulateBufferWithText(buffer, text);
-            buffer.GuessSegmentProperties();
-
-            if (TextStyle.Direction == TextDirection.LeftToRight)
-                buffer.Direction = Direction.LeftToRight;
-            
-            if (TextStyle.Direction == TextDirection.RightToLeft)
-                buffer.Direction = Direction.RightToLeft;
-            
-            ShaperFont.Shape(buffer);
-            
-            var length = buffer.Length;
-            var glyphInfos = buffer.GlyphInfos;
-            var glyphPositions = buffer.GlyphPositions;
-            
-            var scaleY = Paint.TextSize / FontShapingScale;
-            var scaleX = scaleY * Paint.TextScaleX;
-            
-            var xOffset = 0f;
-            var yOffset = 0f;
-            
-            // used for letter spacing calculation
-            var lastCluster = glyphInfos.LastOrDefault().Cluster;
-            var letterSpacing = (TextStyle.LetterSpacing ?? 0) * (TextStyle.Size ?? 16); 
-            
-            var glyphs = new ShapedGlyph[length];
-
-            for (var i = 0; i < length; i++)
-            {
-                // letter spacing should be applied between glyph clusters, not between individual glyphs,
-                // different cluster id indicates the end of the glyph cluster
-                if (lastCluster != glyphInfos[i].Cluster)
-                {
-                    lastCluster = glyphInfos[i].Cluster;
-                    xOffset += letterSpacing;
-                }
-
-                glyphs[i] = new ShapedGlyph
-                {
-                    Codepoint = (ushort)glyphInfos[i].Codepoint,
-                    Position = new SKPoint(xOffset + glyphPositions[i].XOffset * scaleX, yOffset - glyphPositions[i].YOffset * scaleY),
-                    Width = glyphPositions[i].XAdvance * scaleX
-                };                
-                
-                xOffset += glyphPositions[i].XAdvance * scaleX;
-                yOffset += glyphPositions[i].YAdvance * scaleY;
-            }
-            
-            return new TextShapingResult(buffer.Direction, glyphs);
-        }
-        
-        void PopulateBufferWithText(Buffer buffer, string text)
-        {
-            var encoding = Paint.TextEncoding;
-
-            if (encoding == SKTextEncoding.Utf8)
-                buffer.AddUtf8(text);
-                
-            else if (encoding == SKTextEncoding.Utf16)
-                buffer.AddUtf16(text);
-
-            else if (encoding == SKTextEncoding.Utf32)
-                buffer.AddUtf32(text);
-
-            else
-                throw new NotSupportedException("TextEncoding of type GlyphId is not supported.");
-        }
-    }
-    
-    internal struct ShapedGlyph
-    {
-        public ushort Codepoint;
-        public SKPoint Position;
-        public float Width;
-    }
-
-    internal struct DrawTextCommand
-    {
-        public SKTextBlob SkTextBlob;
-        public float TextOffsetX;
-    }
-
-    internal sealed class TextShapingResult
-    {
-        private Direction Direction { get; }
-        private ShapedGlyph[] Glyphs { get; }
-
-        public int Length => Glyphs.Length;
-        
-        public ShapedGlyph this[int index] =>
-            Direction == Direction.LeftToRight 
-                ? Glyphs[index] 
-                : Glyphs[Glyphs.Length - 1 - index];
-
-        public TextShapingResult(Direction direction, ShapedGlyph[] glyphs)
-        {
-            Direction = direction;
-            Glyphs = glyphs;
-        }
-
-        public int BreakText(int startIndex, float maxWidth)
-        {
-            return Direction switch
-            {
-                Direction.LeftToRight => BreakTextLeftToRight(),
-                Direction.RightToLeft => BreakTextRightToLeft(),
-                _ => throw new ArgumentOutOfRangeException()
-            };
-
-            int BreakTextLeftToRight()
-            {
-                var index = startIndex;
-                maxWidth += Glyphs[startIndex].Position.X;
-
-                while (index < Glyphs.Length)
-                {
-                    var glyph = Glyphs[index];
-                
-                    if (glyph.Position.X + glyph.Width > maxWidth + Size.Epsilon)
-                        break;
-                
-                    index++;
-                }
-
-                return index - 1;
-            }
-            
-            int BreakTextRightToLeft()
-            {
-                var index = startIndex;
-
-                var startOffset = this[startIndex].Position.X + this[startIndex].Width;
-
-                while (index < Glyphs.Length)
-                {
-                    if (startOffset - this[index].Position.X > maxWidth + Size.Epsilon)
-                        break;
-                
-                    index++;
-                }
-
-                return index - 1;
-            }
-        }
-        
-        public float MeasureWidth(int startIndex, int endIndex)
-        {
-            if (Glyphs.Length == 0)
-                return 0;
-            
-            var start = this[startIndex];
-            var end = this[endIndex];
-
-            return Direction switch
-            {
-                Direction.LeftToRight => end.Position.X - start.Position.X + end.Width,
-                Direction.RightToLeft => start.Position.X - end.Position.X + start.Width,
-                _ => throw new NotSupportedException()
-            };
-        }
-        
-        public DrawTextCommand? PositionText(int startIndex, int endIndex, TextStyle textStyle)
-        {
-            if (Glyphs.Length == 0)
-                return null;
-
-            if (startIndex > endIndex)
-                return null;
-            
-            using var skTextBlobBuilder = new SKTextBlobBuilder();
-            
-            var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(textStyle.ToFont(), endIndex - startIndex + 1);
-            var glyphSpan = positionedRunBuffer.GetGlyphSpan();
-            var positionSpan = positionedRunBuffer.GetPositionSpan();
-                
-            for (var sourceIndex = startIndex; sourceIndex <= endIndex; sourceIndex++)
-            {
-                var runIndex = sourceIndex - startIndex;
-                
-                glyphSpan[runIndex] = this[sourceIndex].Codepoint;
-                positionSpan[runIndex] = this[sourceIndex].Position;
-            }
-
-            var firstVisualCharacterIndex = Direction == Direction.LeftToRight
-                ? startIndex
-                : endIndex;
-            
-            return new DrawTextCommand
-            {
-                SkTextBlob = skTextBlobBuilder.Build(),
-                TextOffsetX = -this[firstVisualCharacterIndex].Position.X
-            };
-        }
-    }
-    
-}

+ 4 - 5
Source/QuestPDF/Drawing/XpsCanvas.cs

@@ -2,23 +2,22 @@
 using System.IO;
 using System.IO;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
     internal sealed class XpsCanvas : SkiaDocumentCanvasBase
     internal sealed class XpsCanvas : SkiaDocumentCanvasBase
     {
     {
-        public XpsCanvas(Stream stream, DocumentSettings documentSettings) 
-            : base(CreateXps(stream, documentSettings))
+        public XpsCanvas(SkWriteStream stream, DocumentSettings documentSettings) : base(CreateXps(stream, documentSettings))
         {
         {
             
             
         }
         }
         
         
-        private static SKDocument CreateXps(Stream stream, DocumentSettings documentSettings)
+        private static SkDocument CreateXps(SkWriteStream stream, DocumentSettings documentSettings)
         {
         {
             try
             try
             {
             {
-                return SKDocument.CreateXps(stream, documentSettings.ImageRasterDpi);
+                return SkXpsDocument.Create(stream, documentSettings.ImageRasterDpi);
             }
             }
             catch (TypeInitializationException exception)
             catch (TypeInitializationException exception)
             {
             {

+ 1 - 1
Source/QuestPDF/Elements/AspectRatio.cs

@@ -16,7 +16,7 @@ namespace QuestPDF.Elements
             if (Ratio == 0)
             if (Ratio == 0)
                 return SpacePlan.FullRender(0, 0);
                 return SpacePlan.FullRender(0, 0);
             
             
-            if(Child == null)
+            if (Child == null)
                 return SpacePlan.FullRender(0, 0);
                 return SpacePlan.FullRender(0, 0);
             
             
             var targetSize = GetTargetSize(availableSpace);
             var targetSize = GetTargetSize(availableSpace);

+ 2 - 2
Source/QuestPDF/Elements/Background.cs

@@ -5,11 +5,11 @@ namespace QuestPDF.Elements
 {
 {
     internal sealed class Background : ContainerElement
     internal sealed class Background : ContainerElement
     {
     {
-        public string Color { get; set; } = Colors.Black;
+        public Color Color { get; set; } = Colors.Black;
         
         
         internal override void Draw(Size availableSpace)
         internal override void Draw(Size availableSpace)
         {
         {
-            Canvas.DrawRectangle(Position.Zero, availableSpace, Color);
+            Canvas.DrawFilledRectangle(Position.Zero, availableSpace, Color);
             base.Draw(availableSpace);
             base.Draw(availableSpace);
         }
         }
     }
     }

+ 5 - 5
Source/QuestPDF/Elements/Border.cs

@@ -5,7 +5,7 @@ namespace QuestPDF.Elements
 {
 {
     internal sealed class Border : ContainerElement
     internal sealed class Border : ContainerElement
     {
     {
-        public string Color { get; set; } = Colors.Black;
+        public Color Color { get; set; } = Colors.Black;
 
 
         public float Top { get; set; }
         public float Top { get; set; }
         public float Right { get; set; }
         public float Right { get; set; }
@@ -16,22 +16,22 @@ namespace QuestPDF.Elements
         {
         {
             base.Draw(availableSpace);
             base.Draw(availableSpace);
             
             
-            Canvas.DrawRectangle(
+            Canvas.DrawFilledRectangle(
                 new Position(-Left/2, -Top/2), 
                 new Position(-Left/2, -Top/2), 
                 new Size(availableSpace.Width + Left/2 + Right/2, Top), 
                 new Size(availableSpace.Width + Left/2 + Right/2, Top), 
                 Color);
                 Color);
             
             
-            Canvas.DrawRectangle(
+            Canvas.DrawFilledRectangle(
                 new Position(-Left/2, -Top/2), 
                 new Position(-Left/2, -Top/2), 
                 new Size(Left, availableSpace.Height + Top/2 + Bottom/2), 
                 new Size(Left, availableSpace.Height + Top/2 + Bottom/2), 
                 Color);
                 Color);
             
             
-            Canvas.DrawRectangle(
+            Canvas.DrawFilledRectangle(
                 new Position(-Left/2, availableSpace.Height-Bottom/2), 
                 new Position(-Left/2, availableSpace.Height-Bottom/2), 
                 new Size(availableSpace.Width + Left/2 + Right/2, Bottom), 
                 new Size(availableSpace.Width + Left/2 + Right/2, Bottom), 
                 Color);
                 Color);
             
             
-            Canvas.DrawRectangle(
+            Canvas.DrawFilledRectangle(
                 new Position(availableSpace.Width-Right/2, -Top/2), 
                 new Position(availableSpace.Width-Right/2, -Top/2), 
                 new Size(Right, availableSpace.Height + Top/2 + Bottom/2), 
                 new Size(Right, availableSpace.Height + Top/2 + Bottom/2), 
                 Color);
                 Color);

+ 0 - 38
Source/QuestPDF/Elements/Canvas.cs

@@ -1,38 +0,0 @@
-using QuestPDF.Drawing;
-using QuestPDF.Helpers;
-using QuestPDF.Infrastructure;
-using SkiaSharp;
-
-namespace QuestPDF.Elements
-{
-    public delegate void DrawOnCanvas(SKCanvas canvas, Size availableSpace);
-
-    internal sealed class Canvas : Element, ICacheable
-    {
-        public DrawOnCanvas Handler { get; set; }
-        
-        internal override SpacePlan Measure(Size availableSpace)
-        {
-            return availableSpace.IsNegative() 
-                ? SpacePlan.Wrap() 
-                : SpacePlan.FullRender(availableSpace);
-        }
-
-        internal override void Draw(Size availableSpace)
-        {
-            var skiaCanvas = (Canvas as Drawing.SkiaCanvasBase)?.Canvas;
-            
-            if (Handler == null || skiaCanvas == null)
-                return;
-
-            var originalMatrix = skiaCanvas.TotalMatrix;
-            skiaCanvas.Save();
-            
-            skiaCanvas.ClipRect(new SKRect(0, 0, availableSpace.Width, availableSpace.Height));
-            Handler.Invoke(skiaCanvas, availableSpace);
-            
-            skiaCanvas.Restore();
-            skiaCanvas.SetMatrix(originalMatrix);
-        }
-    }
-}

+ 3 - 3
Source/QuestPDF/Elements/DebugArea.cs

@@ -1,7 +1,7 @@
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Elements
 namespace QuestPDF.Elements
 {
 {
@@ -10,10 +10,10 @@ namespace QuestPDF.Elements
         public IElement? Child { get; set; }
         public IElement? Child { get; set; }
         
         
         public string Text { get; set; }
         public string Text { get; set; }
-        public string Color { get; set; } = Colors.Red.Medium;
+        public Color Color { get; set; } = Colors.Red.Medium;
         public void Compose(IContainer container)
         public void Compose(IContainer container)
         {
         {
-            var backgroundColor = SKColor.Parse(Color).WithAlpha(50).ToString();
+            var backgroundColor = Color.WithAlpha(64);
             
             
             container
             container
                 .Border(1)
                 .Border(1)

+ 27 - 10
Source/QuestPDF/Elements/DynamicImage.cs

@@ -1,17 +1,25 @@
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Elements
 namespace QuestPDF.Elements
 {
 {
+    public class GenerateDynamicImageDelegatePayload
+    {
+        public Size AvailableSpace { get; set; }
+        public ImageSize ImageSize { get; set; }
+        public int Dpi { get; set; }
+    }
+    
     /// <summary>
     /// <summary>
     /// Generates an image based on the given resolution.
     /// Generates an image based on the given resolution.
     /// </summary>
     /// </summary>
     /// <param name="size">Desired resolution of the image in pixels.</param>
     /// <param name="size">Desired resolution of the image in pixels.</param>
+    /// <param name="dpi">Desired resolution of the image in dots per inch.</param>
     /// <returns>An image in PNG, JPEG, or WEBP image format returned as byte array.</returns>
     /// <returns>An image in PNG, JPEG, or WEBP image format returned as byte array.</returns>
-    public delegate byte[]? GenerateDynamicImageDelegate(ImageSize size);
-
+    public delegate byte[]? GenerateDynamicImageDelegate(GenerateDynamicImageDelegatePayload payload);
+    
     internal sealed class DynamicImage : Element
     internal sealed class DynamicImage : Element
     {
     {
         internal int? TargetDpi { get; set; }
         internal int? TargetDpi { get; set; }
@@ -28,24 +36,33 @@ namespace QuestPDF.Elements
 
 
         internal override void Draw(Size availableSpace)
         internal override void Draw(Size availableSpace)
         {
         {
-            var targetResolution = GetTargetResolution(availableSpace, TargetDpi.Value);
-            var imageData = Source?.Invoke(targetResolution);
+            var dpi = TargetDpi ?? DocumentSettings.DefaultRasterDpi;
             
             
-            if (imageData == null)
+            var sourcePayload = new GenerateDynamicImageDelegatePayload
+            {
+                AvailableSpace = availableSpace,
+                ImageSize = GetTargetResolution(availableSpace, dpi),
+                Dpi = dpi
+            };
+            
+            var imageBytes = Source?.Invoke(sourcePayload);
+            
+            if (imageBytes == null)
                 return;
                 return;
 
 
-            using var originalImage = SKImage.FromEncodedData(imageData);
+            using var imageData = SkData.FromBinary(imageBytes);
+            using var originalImage = SkImage.FromData(imageData);
             
             
             if (UseOriginalImage)
             if (UseOriginalImage)
-            {
-                Canvas.DrawImage(originalImage, Position.Zero, availableSpace);
+            { 
+                Canvas.DrawImage(originalImage, availableSpace);
                 return;
                 return;
             }
             }
             
             
             using var compressedImage = originalImage.CompressImage(CompressionQuality.Value);
             using var compressedImage = originalImage.CompressImage(CompressionQuality.Value);
 
 
             var targetImage = Helpers.Helpers.GetImageWithSmallerSize(originalImage, compressedImage);
             var targetImage = Helpers.Helpers.GetImageWithSmallerSize(originalImage, compressedImage);
-            Canvas.DrawImage(targetImage, Position.Zero, availableSpace);
+            Canvas.DrawImage(targetImage, availableSpace);
         }
         }
 
 
         private static ImageSize GetTargetResolution(Size availableSize, int targetDpi)
         private static ImageSize GetTargetResolution(Size availableSize, int targetDpi)

+ 30 - 0
Source/QuestPDF/Elements/DynamicSvgImage.cs

@@ -0,0 +1,30 @@
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+
+namespace QuestPDF.Elements;
+
+internal class DynamicSvgImage : Element
+{
+    public GenerateDynamicSvgDelegate SvgSource { get; set; }
+
+    internal override SpacePlan Measure(Size availableSpace)
+    {
+        return availableSpace.IsNegative() 
+            ? SpacePlan.Wrap() 
+            : SpacePlan.FullRender(Size.Zero);
+    }
+
+    internal override void Draw(Size availableSpace)
+    {
+        var svg = SvgSource?.Invoke(availableSpace);
+     
+        if (svg == null)
+            return;
+
+        using var svgImage = new SkSvgImage(svg, FontManager.CurrentFontManager);
+        Canvas.DrawSvg(svgImage, availableSpace);
+    }
+}

+ 3 - 3
Source/QuestPDF/Elements/Image.cs

@@ -1,7 +1,7 @@
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Elements
 namespace QuestPDF.Elements
 {
 {
@@ -26,10 +26,10 @@ namespace QuestPDF.Elements
                 return;
                 return;
 
 
             var image = GetImageToDraw(availableSpace);
             var image = GetImageToDraw(availableSpace);
-            Canvas.DrawImage(image, Position.Zero, availableSpace);
+            Canvas.DrawImage(image, availableSpace);
         }
         }
 
 
-        private SKImage GetImageToDraw(Size availableSpace)
+        private SkImage GetImageToDraw(Size availableSpace)
         {
         {
             var originalImage = DocumentImage.SkImage;
             var originalImage = DocumentImage.SkImage;
             
             

+ 10 - 79
Source/QuestPDF/Elements/LayoutOverflowVisualization.cs

@@ -1,21 +1,17 @@
 using System;
 using System;
-using System.Collections.Generic;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing.Proxy;
 using QuestPDF.Drawing.Proxy;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Elements;
 namespace QuestPDF.Elements;
 
 
 internal class LayoutOverflowVisualization : ContainerElement, IContentDirectionAware
 internal class LayoutOverflowVisualization : ContainerElement, IContentDirectionAware
 {
 {
     private const float BorderThickness = 1.5f;
     private const float BorderThickness = 1.5f;
-    private const float StripeThickness = 1.5f;
-    private const float StripeScale = 6f;
-    private const string LineColor = Colors.Red.Medium;
-    private const string AvailableAreaColor = Colors.Green.Medium;
-    private const string OverflowAreaColor = Colors.Red.Medium;
+    private readonly Color LineColor = Colors.Red.Medium;
+    private readonly Color AvailableAreaColor = Colors.Green.Medium;
     private const byte AreaOpacity = 64;
     private const byte AreaOpacity = 64;
 
 
     public ContentDirection ContentDirection { get; set; }
     public ContentDirection ContentDirection { get; set; }
@@ -75,79 +71,14 @@ internal class LayoutOverflowVisualization : ContainerElement, IContentDirection
 
 
     private void DrawOverflowArea(Size availableSpace, Size contentSize)
     private void DrawOverflowArea(Size availableSpace, Size contentSize)
     {
     {
-        if (Canvas is not SkiaCanvasBase canvasBase)
-            return;
-        
-        var skiaCanvas = canvasBase.Canvas;
-
-        DrawAvailableSpaceBackground();
-
-        skiaCanvas.Save();
-        ClipOverflowAreaVisibility();
-        DrawOverflowArea();
-        DrawCheckerboardPattern();
-        skiaCanvas.Restore();
-
-        DrawContentAreaBorder();
-
-        void DrawAvailableSpaceBackground()
-        {
-            using var paint = new SKPaint
-            {
-                Color = SKColor.Parse(AvailableAreaColor).WithAlpha(AreaOpacity)
-            };
-        
-            skiaCanvas.DrawRect(0, 0, availableSpace.Width, availableSpace.Height, paint);
-        }
-        
-        void DrawContentAreaBorder()
-        {
-            using var borderPaint = new SKPaint
-            {
-                Color = SKColor.Parse(LineColor),
-                IsStroke = true,
-                StrokeWidth = BorderThickness
-            };
-
-            skiaCanvas.DrawRect(0, 0, contentSize.Width, contentSize.Height, borderPaint);
-        }
-        
-        void DrawOverflowArea()
-        {
-            using var areaPaint = new SKPaint
-            {
-                Color = SKColor.Parse(OverflowAreaColor).WithAlpha(AreaOpacity)
-            };
-
-            skiaCanvas.DrawRect(0, 0, contentSize.Width, contentSize.Height, areaPaint);
-        }
-        
-        void DrawCheckerboardPattern()
-        {
-            var matrix = SKMatrix.CreateScale(StripeScale, StripeScale).PostConcat(SKMatrix.CreateRotation((float)(Math.PI / 4)));
-
-            using var paint = new SKPaint
-            {
-                Color = SKColor.Parse(LineColor),
-                PathEffect = SKPathEffect.Create2DLine(StripeThickness, matrix),
-                IsAntialias = true
-            };
-            
-            var targetArea = new SKRect(0,0,contentSize.Width, contentSize.Height);
-            targetArea.Inflate(StripeScale * 2, StripeScale * 2);
-            
-            skiaCanvas.DrawRect(targetArea, paint);
-        }
+        var availableSpaceColor = AvailableAreaColor.WithAlpha(AreaOpacity);
+        Canvas.DrawFilledRectangle(Position.Zero, availableSpace, availableSpaceColor);
 
 
-        void ClipOverflowAreaVisibility()
-        {
-            var path = new SKPath();
-
-            path.AddRect(new SKRect(0, 0, contentSize.Width, contentSize.Height), SKPathDirection.Clockwise);
-            path.AddRect(new SKRect(0, 0, Math.Min(availableSpace.Width, contentSize.Width), Math.Min(availableSpace.Height, contentSize.Height)), SKPathDirection.CounterClockwise);
+        Canvas.Save();
+        Canvas.ClipOverflowArea(new SkRect(0, 0, availableSpace.Width, availableSpace.Height), new SkRect(0, 0, contentSize.Width, contentSize.Height));
+        Canvas.DrawOverflowArea(new SkRect(0, 0, contentSize.Width, contentSize.Height));
+        Canvas.Restore();
 
 
-            skiaCanvas.Save();
-            skiaCanvas.ClipPath(path);
-        }
+        Canvas.DrawStrokeRectangle(Position.Zero, contentSize, BorderThickness, LineColor);
     }
     }
 }
 }

+ 3 - 3
Source/QuestPDF/Elements/Line.cs

@@ -18,7 +18,7 @@ namespace QuestPDF.Elements
     internal sealed class Line : Element, ILine, ICacheable
     internal sealed class Line : Element, ILine, ICacheable
     {
     {
         public LineType Type { get; set; } = LineType.Vertical;
         public LineType Type { get; set; } = LineType.Vertical;
-        public string Color { get; set; } = Colors.Black;
+        public Color Color { get; set; } = Colors.Black;
         public float Size { get; set; } = 1;
         public float Size { get; set; } = 1;
         
         
         internal override SpacePlan Measure(Size availableSpace)
         internal override SpacePlan Measure(Size availableSpace)
@@ -38,11 +38,11 @@ namespace QuestPDF.Elements
         {
         {
             if (Type == LineType.Vertical)
             if (Type == LineType.Vertical)
             {
             {
-                Canvas.DrawRectangle(new Position(-Size/2, 0), new Size(Size, availableSpace.Height), Color);
+                Canvas.DrawFilledRectangle(new Position(-Size/2, 0), new Size(Size, availableSpace.Height), Color);
             }
             }
             else if (Type == LineType.Horizontal)
             else if (Type == LineType.Horizontal)
             {
             {
-                Canvas.DrawRectangle(new Position(0, -Size/2), new Size(availableSpace.Width, Size), Color);
+                Canvas.DrawFilledRectangle(new Position(0, -Size/2), new Size(availableSpace.Width, Size), Color);
             }
             }
         }
         }
     }
     }

+ 1 - 1
Source/QuestPDF/Elements/Page.cs

@@ -18,7 +18,7 @@ namespace QuestPDF.Elements
         public float MarginTop { get; set; }
         public float MarginTop { get; set; }
         public float MarginBottom { get; set; }
         public float MarginBottom { get; set; }
 
 
-        public string BackgroundColor { get; set; } = Colors.White;
+        public Color BackgroundColor { get; set; } = Colors.White;
         
         
         public Element Background { get; set; } = Empty.Instance;
         public Element Background { get; set; } = Empty.Instance;
         public Element Foreground { get; set; } = Empty.Instance;
         public Element Foreground { get; set; } = Empty.Instance;

+ 4 - 7
Source/QuestPDF/Elements/Placeholder.cs

@@ -7,15 +7,12 @@ namespace QuestPDF.Elements
     internal sealed class Placeholder : IComponent
     internal sealed class Placeholder : IComponent
     {
     {
         public string Text { get; set; }
         public string Text { get; set; }
-        private static readonly byte[] ImageData;
-
-        static Placeholder()
-        {
-            ImageData = Helpers.Helpers.LoadEmbeddedResource("QuestPDF.Resources.ImagePlaceholder.png");
-        }
 
 
         public void Compose(IContainer container)
         public void Compose(IContainer container)
         {
         {
+            const float imageSvgSize = 24f;
+            const string imageSvgPath = "M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z";
+            
             container
             container
                 .Background(Colors.Grey.Lighten2)
                 .Background(Colors.Grey.Lighten2)
                 .Padding(5)
                 .Padding(5)
@@ -24,7 +21,7 @@ namespace QuestPDF.Elements
                 .Element(x =>
                 .Element(x =>
                 {
                 {
                     if (string.IsNullOrWhiteSpace(Text))
                     if (string.IsNullOrWhiteSpace(Text))
-                        x.MaxHeight(32).Image(ImageData).FitArea();
+                        x.Height(imageSvgSize).Width(imageSvgSize).SvgPath(imageSvgPath, Colors.White);
                     
                     
                     else
                     else
                         x.Text(Text).FontSize(14);
                         x.Text(Text).FontSize(14);

+ 0 - 1
Source/QuestPDF/Elements/Shrink.cs

@@ -1,4 +1,3 @@
-using QuestPDF.Drawing;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Elements
 namespace QuestPDF.Elements

+ 22 - 0
Source/QuestPDF/Elements/SvgImage.cs

@@ -0,0 +1,22 @@
+using QuestPDF.Drawing;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements;
+
+internal class SvgImage : Element
+{
+    public Infrastructure.SvgImage Image { get; set; }
+    
+    internal override SpacePlan Measure(Size availableSpace)
+    {
+        return availableSpace.IsNegative() 
+            ? SpacePlan.Wrap() 
+            : SpacePlan.FullRender(Size.Zero);
+    }
+
+    internal override void Draw(Size availableSpace)
+    {
+        Canvas.DrawSvg(Image.SkSvgImage, availableSpace);
+    }
+}

+ 24 - 0
Source/QuestPDF/Elements/SvgPath.cs

@@ -0,0 +1,24 @@
+using QuestPDF.Drawing;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+
+namespace QuestPDF.Elements;
+
+internal class SvgPath : Element
+{
+    public string Path { get; set; } = string.Empty;
+    public Color FillColor { get; set; } = Colors.Black;
+    
+    internal override SpacePlan Measure(Size availableSpace)
+    {
+        return availableSpace.IsNegative() 
+            ? SpacePlan.Wrap() 
+            : SpacePlan.FullRender(Size.Zero);
+    }
+
+    internal override void Draw(Size availableSpace)
+    {
+        Canvas.DrawSvgPath(Path, FillColor);
+    }
+}

+ 0 - 1
Source/QuestPDF/Elements/Table/TableLayoutPlanner.cs

@@ -1,7 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
-using QuestPDF.Fluent;
 
 
 namespace QuestPDF.Elements.Table
 namespace QuestPDF.Elements.Table
 {
 {

+ 0 - 3
Source/QuestPDF/Elements/Table/TableLayoutValidator.cs

@@ -1,8 +1,5 @@
-using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Linq;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Exceptions;
-using QuestPDF.Fluent;
 
 
 namespace QuestPDF.Elements.Table
 namespace QuestPDF.Elements.Table
 {
 {

+ 0 - 16
Source/QuestPDF/Elements/Text/Calculation/TextDrawingRequest.cs

@@ -1,16 +0,0 @@
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.Elements.Text.Calculation
-{
-    internal sealed class TextDrawingRequest
-    {
-        public ICanvas Canvas { get; set; }
-        public IPageContext PageContext { get; set; }
-        
-        public int StartIndex { get; set; }
-        public int EndIndex { get; set; }
-        
-        public float TotalAscent { get; set; }
-        public Size TextSize { get; set; }
-    }
-}

+ 0 - 45
Source/QuestPDF/Elements/Text/Calculation/TextLine.cs

@@ -1,45 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-
-namespace QuestPDF.Elements.Text.Calculation
-{
-    internal sealed class TextLine
-    {
-        public ICollection<TextLineElement> Elements { get; private set; }
-
-        public float TextHeight { get; private set; }
-        public float LineHeight { get; private set; }
-        
-        public float Ascent { get; private set; }
-        public float Descent { get; private set; }
-
-        public float Width { get; private set; }
-        
-        public static TextLine From(ICollection<TextLineElement> elements)
-        {
-            if (elements.Count == 0)
-            {
-                return new TextLine
-                {
-                    Elements = elements
-                };
-            }
-            
-            var textHeight = elements.Max(x => x.Measurement.Height);
-            var lineHeight = elements.Max(x => x.Measurement.LineHeight * x.Measurement.Height);
-            
-            return new TextLine
-            {
-                Elements = elements,
-                
-                TextHeight = textHeight,
-                LineHeight = lineHeight,
-                
-                Ascent = elements.Min(x => x.Measurement.Ascent) - (lineHeight - textHeight) / 2,
-                Descent = elements.Max(x => x.Measurement.Descent) + (lineHeight - textHeight) / 2,
-                
-                Width = elements.Sum(x => x.Measurement.Width)
-            };
-        }
-    }
-}

+ 0 - 10
Source/QuestPDF/Elements/Text/Calculation/TextLineElement.cs

@@ -1,10 +0,0 @@
-using QuestPDF.Elements.Text.Items;
-
-namespace QuestPDF.Elements.Text.Calculation
-{
-    internal sealed class TextLineElement
-    {
-        public ITextBlockItem Item { get; set; }
-        public TextMeasurementResult Measurement { get; set; }
-    }
-}

+ 0 - 16
Source/QuestPDF/Elements/Text/Calculation/TextMeasurementRequest.cs

@@ -1,16 +0,0 @@
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.Elements.Text.Calculation
-{
-    internal sealed class TextMeasurementRequest
-    {
-        public ICanvas Canvas { get; set; }
-        public IPageContext PageContext { get; set; }
-        
-        public int StartIndex { get; set; }
-        public float AvailableWidth { get; set; }
-        
-        public bool IsFirstElementInBlock { get; set; }
-        public bool IsFirstElementInLine { get; set; }
-    }
-}

+ 0 - 22
Source/QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs

@@ -1,22 +0,0 @@
-using System;
-
-namespace QuestPDF.Elements.Text.Calculation
-{
-    internal sealed class TextMeasurementResult
-    {
-        public float Width { get; set; }
-        public float Height => Math.Abs(Descent) + Math.Abs(Ascent);
-
-        public float Ascent { get; set; }
-        public float Descent { get; set; }
-
-        public float LineHeight { get; set; }
-        
-        public int StartIndex { get; set; }
-        public int EndIndex { get; set; }
-        public int NextIndex { get; set; }
-        public int TotalIndex { get; set; }
-
-        public bool IsLast => EndIndex == TotalIndex;
-    }
-}

+ 0 - 165
Source/QuestPDF/Elements/Text/FontFallback.cs

@@ -1,165 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using QuestPDF.Drawing;
-using QuestPDF.Drawing.Exceptions;
-using QuestPDF.Elements.Text.Items;
-using QuestPDF.Fluent;
-using QuestPDF.Infrastructure;
-using SkiaSharp;
-
-namespace QuestPDF.Elements.Text
-{
-    internal static class FontFallback
-    {
-        public struct TextRun
-        {
-            public string Content { get; set; }
-            public TextStyle Style { get; set; }
-        }
-
-        public class FallbackOption
-        {
-            public TextStyle Style { get; set; }
-            public SKFont Font { get; set; }
-            public SKTypeface Typeface { get; set; }
-        }
-
-        private static SKFontManager FontManager => SKFontManager.Default;
-
-        public static IEnumerable<TextRun> SplitWithFontFallback(this string text, TextStyle textStyle)
-        {
-            var fallbackOptions = GetFallbackOptions(textStyle).ToArray();
-            
-            var spanStartIndex = 0;
-            var spanFallbackOption = fallbackOptions[0];
-            
-            for (var i = 0; i < text.Length; i += char.IsSurrogatePair(text, i) ? 2 : 1)
-            {
-                var codepoint = char.ConvertToUtf32(text, i);
-                var newFallbackOption = MatchFallbackOption(fallbackOptions, codepoint);
-
-                if (newFallbackOption == spanFallbackOption)
-                    continue;
-
-                yield return new TextRun
-                {
-                    Content = text.Substring(spanStartIndex, i - spanStartIndex),
-                    Style = spanFallbackOption.Style
-                };
-
-                spanStartIndex = i;
-                spanFallbackOption = newFallbackOption;
-            }
-            
-            if (spanStartIndex > text.Length)
-                yield break;
-            
-            yield return new TextRun
-            {
-                Content = text.Substring(spanStartIndex, text.Length - spanStartIndex),
-                Style = spanFallbackOption.Style
-            };
-
-            static IEnumerable<FallbackOption> GetFallbackOptions(TextStyle? textStyle)
-            {
-                while (textStyle != null)
-                {
-                    var font = textStyle.ToFont();
-                    
-                    yield return new FallbackOption
-                    {
-                        Style = textStyle,
-                        Font = font,
-                        Typeface = font.Typeface
-                    };
-
-                    textStyle = textStyle.Fallback;
-                }
-            }
-
-            static FallbackOption MatchFallbackOption(ICollection<FallbackOption> fallbackOptions, int codepoint)
-            {
-                foreach (var fallbackOption in fallbackOptions)
-                {
-                    if (fallbackOption.Font.ContainsGlyph(codepoint))
-                        return fallbackOption;
-                }
-
-                if (Settings.CheckIfAllTextGlyphsAreAvailable)
-                    throw CreateNotMatchingFontException(codepoint);
-
-                return fallbackOptions.First();
-            }
-
-            static Exception CreateNotMatchingFontException(int codepoint)
-            {
-                var character = char.ConvertFromUtf32(codepoint);
-                var unicode = $"U-{codepoint:X4}";
-
-                var proposedFonts = FindFontsContainingGlyph(codepoint).ToArray();
-                var proposedFontsFormatted = proposedFonts.Any() ? string.Join(", ", proposedFonts) : "no fonts available";
-                
-                return new DocumentDrawingException(
-                    $"Could not find an appropriate font fallback for glyph: {unicode} '{character}'. " +
-                    $"Font families available on current environment that contain this glyph: {proposedFontsFormatted}. " +
-                    $"Possible solutions: " +
-                    $"1) Use one of the listed fonts as the primary font in your document. " +
-                    $"2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. " +
-                    $"You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. " +
-                    $"However, this may result with text glyphs being incorrectly rendered without any warning.");
-            }
-            
-            static IEnumerable<string> FindFontsContainingGlyph(int codepoint)
-            {
-                var fontManager = SKFontManager.Default;
-
-                return fontManager
-                    .GetFontFamilies()
-                    .Select(fontManager.MatchFamily)
-                    .Where(x => x.ContainsGlyph(codepoint))
-                    .Select(x => x.FamilyName);
-            }
-        }
-
-        public static IEnumerable<ITextBlockItem> ApplyFontFallback(this ICollection<ITextBlockItem> textBlockItems)
-        {
-            foreach (var textBlockItem in textBlockItems)
-            {
-                if (textBlockItem is TextBlockPageNumber or TextBlockElement)
-                {
-                    yield return textBlockItem;
-                }
-                else if (textBlockItem is TextBlockSpan textBlockSpan)
-                {
-                    if (!Settings.CheckIfAllTextGlyphsAreAvailable && textBlockSpan.Style.Fallback == null)
-                    {
-                        yield return textBlockSpan;
-                        continue;
-                    }
-                    
-                    var textRuns = textBlockSpan.Text.SplitWithFontFallback(textBlockSpan.Style);
-                    
-                    foreach (var textRun in textRuns)
-                    {
-                        var newElement = textBlockSpan switch
-                        {
-                            TextBlockHyperlink hyperlink => new TextBlockHyperlink { Url = hyperlink.Url },
-                            TextBlockSectionLink sectionLink => new TextBlockSectionLink { SectionName = sectionLink.SectionName },
-                            TextBlockSpan => new TextBlockSpan()
-                        };
-
-                        newElement.Text = textRun.Content;
-                        newElement.Style = textRun.Style;
-
-                        yield return newElement;
-                    }
-                }
-                else
-                {
-                    throw new NotSupportedException();
-                }
-            }
-        }
-    }
-}

+ 2 - 6
Source/QuestPDF/Elements/Text/Items/ITextBlockItem.cs

@@ -1,11 +1,7 @@
-using QuestPDF.Elements.Text.Calculation;
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.Elements.Text.Items
+namespace QuestPDF.Elements.Text.Items
 {
 {
     internal interface ITextBlockItem
     internal interface ITextBlockItem
     {
     {
-        TextMeasurementResult? Measure(TextMeasurementRequest request);
-        void Draw(TextDrawingRequest request);
+        
     }
     }
 }
 }

+ 8 - 31
Source/QuestPDF/Elements/Text/Items/TextBlockElement.cs

@@ -1,5 +1,4 @@
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
-using QuestPDF.Elements.Text.Calculation;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
@@ -8,40 +7,18 @@ namespace QuestPDF.Elements.Text.Items
     internal sealed class TextBlockElement : ITextBlockItem
     internal sealed class TextBlockElement : ITextBlockItem
     {
     {
         public Element Element { get; set; } = Empty.Instance;
         public Element Element { get; set; } = Empty.Instance;
-        
-        public TextMeasurementResult? Measure(TextMeasurementRequest request)
+        public Size ElementSize { get; set; } = Size.Zero;
+        public TextInjectedElementAlignment Alignment { get; set; } = TextInjectedElementAlignment.AboveBaseline;
+
+        public void ConfigureElement(IPageContext pageContext, ICanvas canvas)
         {
         {
             Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
             Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
-            Element.InjectDependencies(request.PageContext, request.Canvas);
-
-            var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height));
-
-            if (measurement.Type != SpacePlanType.FullRender)
-                return null;
-            
-            return new TextMeasurementResult
-            {
-                Width = measurement.Width,
-                
-                Ascent = -measurement.Height,
-                Descent = 0,
-                
-                LineHeight = 1,
-                
-                StartIndex = 0,
-                EndIndex = 0,
-                TotalIndex = 0
-            };
+            Element.InjectDependencies(pageContext, canvas);
         }
         }
-
-        public void Draw(TextDrawingRequest request)
+        
+        public void UpdateElementSize()
         {
         {
-            Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
-            Element.InjectDependencies(request.PageContext, request.Canvas);
-            
-            request.Canvas.Translate(new Position(0, request.TotalAscent));
-            Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
-            request.Canvas.Translate(new Position(0, -request.TotalAscent));
+            ElementSize = Element.Measure(Size.Max);
         }
         }
     }
     }
 }
 }

+ 2 - 18
Source/QuestPDF/Elements/Text/Items/TextBlockHyperlink.cs

@@ -1,24 +1,8 @@
-using QuestPDF.Elements.Text.Calculation;
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.Elements.Text.Items
+namespace QuestPDF.Elements.Text.Items
 {
 {
     internal sealed class TextBlockHyperlink : TextBlockSpan
     internal sealed class TextBlockHyperlink : TextBlockSpan
     {
     {
         public string Url { get; set; }
         public string Url { get; set; }
-        
-        public override TextMeasurementResult? Measure(TextMeasurementRequest request)
-        {
-            return MeasureWithoutCache(request);
-        }
-
-        public override void Draw(TextDrawingRequest request)
-        {
-            request.Canvas.Translate(new Position(0, request.TotalAscent));
-            request.Canvas.DrawHyperlink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
-            request.Canvas.Translate(new Position(0, -request.TotalAscent));
-            
-            base.Draw(request);
-        }
+        public int ParagraphBeginIndex { get; set; }
     }
     }
 }
 }

+ 2 - 16
Source/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs

@@ -1,5 +1,4 @@
 using System;
 using System;
-using QuestPDF.Elements.Text.Calculation;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Elements.Text.Items
 namespace QuestPDF.Elements.Text.Items
@@ -8,23 +7,10 @@ namespace QuestPDF.Elements.Text.Items
     {
     {
         public const string PageNumberPlaceholder = "123";
         public const string PageNumberPlaceholder = "123";
         public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
         public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
-        protected override bool EnableTextCache => false;
-        
-        public override TextMeasurementResult? Measure(TextMeasurementRequest request)
-        {
-            UpdatePageNumberText(request.PageContext);
-            return MeasureWithoutCache(request);
-        }
-
-        public override void Draw(TextDrawingRequest request)
-        {
-            UpdatePageNumberText(request.PageContext);
-            base.Draw(request);
-        }
 
 
-        private void UpdatePageNumberText(IPageContext context)
+        public void UpdatePageNumberText(IPageContext context)
         {
         {
-            Text = Source(context) ?? string.Empty;
+            Text = Source(context) ?? PageNumberPlaceholder;
         }
         }
     }
     }
 }
 }

+ 2 - 19
Source/QuestPDF/Elements/Text/Items/TextBlockSectionLink.cs

@@ -1,27 +1,10 @@
-using QuestPDF.Elements.Text.Calculation;
-using QuestPDF.Infrastructure;
+using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Elements.Text.Items
 namespace QuestPDF.Elements.Text.Items
 {
 {
     internal sealed class TextBlockSectionLink : TextBlockSpan
     internal sealed class TextBlockSectionLink : TextBlockSpan
     {
     {
-        public IPageContext PageContext { get; set; }
         public string SectionName { get; set; }
         public string SectionName { get; set; }
-        
-        public override TextMeasurementResult? Measure(TextMeasurementRequest request)
-        {
-            return MeasureWithoutCache(request);
-        }
-
-        public override void Draw(TextDrawingRequest request)
-        {
-            var targetName = PageContext.GetDocumentLocationName(SectionName);
-            
-            request.Canvas.Translate(new Position(0, request.TotalAscent));
-            request.Canvas.DrawSectionLink(targetName, new Size(request.TextSize.Width, request.TextSize.Height));
-            request.Canvas.Translate(new Position(0, -request.TotalAscent));
-            
-            base.Draw(request);
-        }
+        public int ParagraphBeginIndex { get; set; }
     }
     }
 }
 }

+ 1 - 190
Source/QuestPDF/Elements/Text/Items/TextBlockSpan.cs

@@ -1,10 +1,4 @@
-using System;
-using System.Collections.Generic;
-using QuestPDF.Drawing;
-using QuestPDF.Elements.Text.Calculation;
-using QuestPDF.Helpers;
-using QuestPDF.Infrastructure;
-using Size = QuestPDF.Infrastructure.Size;
+using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Elements.Text.Items
 namespace QuestPDF.Elements.Text.Items
 {
 {
@@ -12,188 +6,5 @@ namespace QuestPDF.Elements.Text.Items
     {
     {
         public string Text { get; set; }
         public string Text { get; set; }
         public TextStyle Style { get; set; } = TextStyle.Default;
         public TextStyle Style { get; set; } = TextStyle.Default;
-        private TextShapingResult? TextShapingResult { get; set; }
-        private ushort? SpaceCodepoint { get; set; }
-        
-        private Dictionary<MeasurementCacheKey, TextMeasurementResult?> MeasureCache = new ();
-        protected virtual bool EnableTextCache => true;
-
-        private record struct MeasurementCacheKey
-        {
-            public int StartIndex { get; set; }
-            public float AvailableWidth { get; set; }
-        
-            public bool IsFirstElementInBlock { get; set; }
-            public bool IsFirstElementInLine { get; set; }
-        }
-        
-        public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
-        {
-            var cacheKey = new MeasurementCacheKey
-            {
-                StartIndex = request.StartIndex,
-                AvailableWidth = request.AvailableWidth,
-                IsFirstElementInBlock = request.IsFirstElementInBlock,
-                IsFirstElementInLine = request.IsFirstElementInLine
-            };
-             
-            if (!MeasureCache.ContainsKey(cacheKey))
-                MeasureCache[cacheKey] = MeasureWithoutCache(request);
-            
-            return MeasureCache[cacheKey];
-        }
-
-        internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
-        {
-            if (!EnableTextCache)
-                TextShapingResult = null;
-            
-            TextShapingResult ??= Style.ToTextShaper().Shape(Text);
-
-            var paint = Style.ToPaint();
-            var fontMetrics = Style.ToFontMetrics();
-            SpaceCodepoint ??= paint.ToFont().Typeface.GetGlyphs(" ")[0];
-
-            var startIndex = request.StartIndex;
-            
-            // if the element is the first one within the line,
-            // ignore leading spaces
-            if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
-            {
-                while (startIndex < TextShapingResult.Length && Text[startIndex] == SpaceCodepoint)
-                    startIndex++;
-            }
-
-            if (TextShapingResult.Length == 0 || startIndex == TextShapingResult.Length)
-            {
-                return new TextMeasurementResult
-                {
-                    Width = 0,
-                    
-                    LineHeight = Style.LineHeight ?? 1,
-                    Ascent = fontMetrics.Ascent,
-                    Descent = fontMetrics.Descent
-                };
-            }
-            
-            // start breaking text from requested position
-            var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth);
-
-            if (endIndex < startIndex)
-                return null;
-  
-            // break text only on spaces
-            var wrappedText = WrapText(startIndex, endIndex, request.IsFirstElementInLine);
-
-            if (wrappedText == null)
-                return null;
-            
-            // measure final text
-            var width = TextShapingResult.MeasureWidth(startIndex, wrappedText.Value.endIndex);
-            
-            return new TextMeasurementResult
-            {
-                Width = width,
-                
-                Ascent = fontMetrics.Ascent,
-                Descent = fontMetrics.Descent,
-     
-                LineHeight = Style.LineHeight ?? 1,
-                
-                StartIndex = startIndex,
-                EndIndex = wrappedText.Value.endIndex,
-                NextIndex = wrappedText.Value.nextIndex,
-                TotalIndex = TextShapingResult.Length - 1
-            };
-        }
-        
-        // TODO: consider introducing text wrapping abstraction (basic, english-like, asian-like)
-        private (int endIndex, int nextIndex)? WrapText(int startIndex, int endIndex, 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 (endIndex == TextShapingResult.Length - 1)
-                return (endIndex, endIndex);
-
-            // breaking anywhere
-            if (Style.WrapAnywhere ?? false)
-                return (endIndex, endIndex + 1);
-                
-            // current line ends at word, next character is space, perfect place to wrap
-            if (TextShapingResult[endIndex].Codepoint != SpaceCodepoint && TextShapingResult[endIndex + 1].Codepoint == SpaceCodepoint)
-                return (endIndex, endIndex + 2);
-                
-            // find last space within the available text to wrap
-            var lastSpaceIndex = endIndex;
-
-            while (lastSpaceIndex >= startIndex)
-            {
-                if (TextShapingResult[lastSpaceIndex].Codepoint == SpaceCodepoint)
-                    break;
-
-                lastSpaceIndex--;
-            }
-
-            // text contains space that can be used to wrap
-            if (lastSpaceIndex > 1 && lastSpaceIndex >= startIndex)
-                return (lastSpaceIndex - 1, 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 ? (endIndex, endIndex + 1) : null;
-        }
-        
-        public virtual void Draw(TextDrawingRequest request)
-        {
-            var fontMetrics = Style.ToFontMetrics();
-
-            var glyphOffsetY = GetGlyphOffset();
-            
-            var textDrawingCommand = TextShapingResult.PositionText(request.StartIndex, request.EndIndex, Style);
-
-            if (Style.BackgroundColor != Colors.Transparent)
-                request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
-            
-            if (textDrawingCommand.HasValue)
-                request.Canvas.DrawText(textDrawingCommand.Value.SkTextBlob, new Position(textDrawingCommand.Value.TextOffsetX, glyphOffsetY), Style);
-
-            // draw underline
-            if (Style.HasUnderline ?? false)
-            {
-                var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffsetY;
-                DrawLine(fontMetrics.UnderlinePosition + underlineOffset, fontMetrics.UnderlineThickness);
-            }
-            
-            // draw stroke
-            if (Style.HasStrikethrough ?? false)
-            {
-                var strikeoutThickness = fontMetrics.StrikeoutThickness;
-                strikeoutThickness *= Style.FontPosition == FontPosition.Normal ? 1f : 0.625f;
-                
-                DrawLine(fontMetrics.StrikeoutPosition + glyphOffsetY, strikeoutThickness);
-            }
-            
-            void DrawLine(float offset, float thickness)
-            {
-                request.Canvas.DrawRectangle(new Position(0, offset), new Size(request.TextSize.Width, thickness), Style.Color);
-            }
-
-            float GetGlyphOffset()
-            {
-                var fontSize = Style.Size ?? 12f;
-
-                var offsetFactor = Style.FontPosition switch
-                {
-                    FontPosition.Normal => 0,
-                    FontPosition.Subscript => 0.1f,
-                    FontPosition.Superscript => -0.35f,
-                    _ => throw new ArgumentOutOfRangeException()
-                };
-
-                return fontSize * offsetFactor;
-            }
-        }
     }
     }
 }
 }

+ 34 - 0
Source/QuestPDF/Elements/Text/SkParagraphBuilderPoolManager.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Concurrent;
+using QuestPDF.Drawing;
+using QuestPDF.Skia.Text;
+
+namespace QuestPDF.Elements.Text;
+
+internal static class SkParagraphBuilderPoolManager
+{
+    private static ConcurrentDictionary<ParagraphStyleConfiguration, ConcurrentBag<SkParagraphBuilder>> ObjectPool { get; } = new();
+
+    public static SkParagraphBuilder Get(ParagraphStyleConfiguration configuration)
+    {
+        var specificPool = GetPool(configuration);
+        
+        if (specificPool.TryTake(out var builder))
+            return builder;
+        
+        return SkParagraphBuilder.Create(configuration, FontManager.CurrentFontCollection);
+    }
+
+    public static void Return(SkParagraphBuilder builder)
+    {
+        builder.Reset();
+        
+        var specificPool = GetPool(builder.Configuration);
+        specificPool.Add(builder);
+    }
+
+    private static ConcurrentBag<SkParagraphBuilder> GetPool(ParagraphStyleConfiguration configuration)
+    {
+        return ObjectPool.GetOrAdd(configuration, _ => new ConcurrentBag<SkParagraphBuilder>());
+    }
+}

+ 310 - 169
Source/QuestPDF/Elements/Text/TextBlock.cs

@@ -1,9 +1,13 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
-using QuestPDF.Elements.Text.Calculation;
+using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Elements.Text.Items;
 using QuestPDF.Elements.Text.Items;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+using QuestPDF.Skia.Text;
 
 
 namespace QuestPDF.Elements.Text
 namespace QuestPDF.Elements.Text
 {
 {
@@ -11,232 +15,369 @@ namespace QuestPDF.Elements.Text
     {
     {
         public ContentDirection ContentDirection { get; set; }
         public ContentDirection ContentDirection { get; set; }
         
         
-        public HorizontalAlignment? Alignment { get; set; }
-        public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
+        public TextHorizontalAlignment? Alignment { get; set; }
+        public int? LineClamp { get; set; }
+        public List<ITextBlockItem> Items { get; set; } = new();
 
 
+        private SkParagraph Paragraph { get; set; }
+        
+        private bool RebuildParagraphForEveryPage { get; set; }
+        private bool AreParagraphMetricsValid { get; set; }
+        
+        private SkSize[] LineMetrics { get; set; }
+        private float WidthForLineMetricsCalculation { get; set; }
+        private SkRect[] PlaceholderPositions { get; set; }
+        private float MaximumWidth { get; set; }
+        
+        private int CurrentLineIndex { get; set; }
+        private float CurrentTopOffset { get; set; }
+        
         public string Text => string.Join(" ", Items.OfType<TextBlockSpan>().Select(x => x.Text));
         public string Text => string.Join(" ", Items.OfType<TextBlockSpan>().Select(x => x.Text));
 
 
-        private Queue<ITextBlockItem> RenderingQueue { get; set; }
-        private int CurrentElementIndex { get; set; }
-
-        private bool FontFallbackApplied { get; set; } = false;
-
-        public void ResetState()
+        ~TextBlock()
         {
         {
-            ApplyFontFallback();
-            ApplyPageContextToSectionLinks();
-            InitializeQueue();
-            CurrentElementIndex = 0;
-
-            void InitializeQueue()
-            {
-                // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
-                if (RenderingQueue == null)
-                {
-                    RenderingQueue = new Queue<ITextBlockItem>(Items);
-                    return;
-                }
-                
-                RenderingQueue.Clear();
-            
-                foreach (var item in Items)
-                    RenderingQueue.Enqueue(item);
-            }
-
-            void ApplyFontFallback()
-            {
-                if (FontFallbackApplied)
-                    return;
-
-                Items = Items.ApplyFontFallback().ToList();
-                FontFallbackApplied = true;
-            }
-
-            void ApplyPageContextToSectionLinks()
-            {
-                foreach (var sectionLink in Items.OfType<TextBlockSectionLink>())
-                    sectionLink.PageContext = PageContext;
-            }
+            Paragraph?.Dispose();
         }
         }
         
         
-        void SetDefaultAlignment()
+        public void ResetState()
         {
         {
-            if (Alignment.HasValue)
-                return;
-
-            Alignment = ContentDirection == ContentDirection.LeftToRight
-                ? HorizontalAlignment.Left
-                : HorizontalAlignment.Right;
+            CurrentLineIndex = 0;
+            CurrentTopOffset = 0;
         }
         }
-
+        
         internal override SpacePlan Measure(Size availableSpace)
         internal override SpacePlan Measure(Size availableSpace)
         {
         {
-            SetDefaultAlignment();
-            
-            if (!RenderingQueue.Any())
+            if (Items.Count == 0)
                 return SpacePlan.FullRender(Size.Zero);
                 return SpacePlan.FullRender(Size.Zero);
             
             
-            var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
+            Initialize();
+            CalculateParagraphMetrics(availableSpace);
 
 
-            if (!lines.Any())
-                return SpacePlan.Wrap();
+            if (MaximumWidth == 0)
+                return SpacePlan.FullRender(Size.Zero);
+            
+            if (CurrentLineIndex > LineMetrics.Length)
+                return SpacePlan.FullRender(Size.Zero);
+            
+            var totalHeight = 0f;
+            var totalLines = 0;
             
             
-            var width = lines.Max(x => x.Width);
-            var height = lines.Sum(x => x.LineHeight);
+            for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
+            {
+                var lineMetric = LineMetrics[lineIndex];
+                var newTotalHeight = totalHeight + lineMetric.Height;
+                
+                if (newTotalHeight > availableSpace.Height + Size.Epsilon)
+                    break;
+                
+                totalHeight = newTotalHeight;
+                totalLines++;
+            }
 
 
-            if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
+            if (totalLines == 0)
                 return SpacePlan.Wrap();
                 return SpacePlan.Wrap();
 
 
-            var fullyRenderedItemsCount = lines
-                .SelectMany(x => x.Elements)
-                .GroupBy(x => x.Item)
-                .Count(x => x.Any(y => y.Measurement.IsLast));
+            var requiredArea = new Size(
+                Math.Min(MaximumWidth, availableSpace.Width),
+                Math.Min(totalHeight, availableSpace.Height));
             
             
-            if (fullyRenderedItemsCount == RenderingQueue.Count)
-                return SpacePlan.FullRender(width, height);
-            
-            return SpacePlan.PartialRender(width, height);
+            if (CurrentLineIndex + totalLines < LineMetrics.Length)
+                return SpacePlan.PartialRender(requiredArea);
+
+            return SpacePlan.FullRender(requiredArea);
         }
         }
 
 
         internal override void Draw(Size availableSpace)
         internal override void Draw(Size availableSpace)
         {
         {
-            SetDefaultAlignment();
-            
-            var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
+            if (Items.Count == 0)
+                return;
             
             
-            if (!lines.Any())
+            CalculateParagraphMetrics(availableSpace);
+
+            if (MaximumWidth == 0)
                 return;
                 return;
             
             
-            var topOffset = 0f;
+            var (linesToDraw, takenHeight) = DetermineLinesToDraw();
+            DrawParagraph();
+            
+            CurrentLineIndex += linesToDraw;
+            CurrentTopOffset += takenHeight;
 
 
-            foreach (var line in lines)
-            {
-                var leftOffset = GetAlignmentOffset(line.Width);
+            if (CurrentLineIndex == LineMetrics.Length)
+                ResetState();
+            
+            return;
 
 
-                foreach (var item in line.Elements.Where(x => x.Measurement.Width > 0.0))
+            (int linesToDraw, float takenHeight) DetermineLinesToDraw()
+            {
+                var linesToDraw = 0;
+                var takenHeight = 0f;
+                
+                for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
                 {
                 {
-                    var textDrawingRequest = new TextDrawingRequest
-                    {
-                        Canvas = Canvas,
-                        PageContext = PageContext,
-                        
-                        StartIndex = item.Measurement.StartIndex,
-                        EndIndex = item.Measurement.EndIndex,
-                        
-                        TextSize = new Size(item.Measurement.Width, line.LineHeight),
-                        TotalAscent = line.Ascent
-                    };
+                    var lineMetric = LineMetrics[lineIndex];
                 
                 
-                    var canvasOffset = ContentDirection == ContentDirection.LeftToRight
-                        ? new Position(leftOffset, topOffset - line.Ascent)
-                        : new Position(availableSpace.Width - leftOffset - item.Measurement.Width, topOffset - line.Ascent);
-                    
-                    Canvas.Translate(canvasOffset);
-                    item.Item.Draw(textDrawingRequest);
-                    Canvas.Translate(canvasOffset.Reverse());
+                    var newTotalHeight = takenHeight + lineMetric.Height;
+
+                    if (newTotalHeight > availableSpace.Height + Size.Epsilon)
+                        break;
                     
                     
-                    leftOffset += item.Measurement.Width;
+                    takenHeight = newTotalHeight;
+                    linesToDraw++;
+                }
+
+                return (linesToDraw, takenHeight);
+            }
+            
+            void DrawParagraph()
+            {
+                var takesMultiplePages = linesToDraw != LineMetrics.Length;
+                
+                if (takesMultiplePages)
+                {
+                    Canvas.Save();
+                    Canvas.ClipRectangle(new SkRect(0, 0, availableSpace.Width, takenHeight));
+                    Canvas.Translate(new Position(0, -CurrentTopOffset));
                 }
                 }
                 
                 
-                topOffset += line.LineHeight;
+                Canvas.DrawParagraph(Paragraph);
+                DrawInjectedElements();
+                DrawHyperlinks();
+                DrawSectionLinks();
+                
+                if (takesMultiplePages)
+                    Canvas.Restore();
             }
             }
 
 
-            lines
-                .SelectMany(x => x.Elements)
-                .GroupBy(x => x.Item)
-                .Where(x => x.Any(y => y.Measurement.IsLast))
-                .Select(x => x.Key)
-                .ToList()
-                .ForEach(x => RenderingQueue.Dequeue());
+            void DrawInjectedElements()
+            {
+                var elementItems = Items.OfType<TextBlockElement>().ToArray();
+                
+                for (var placeholderIndex = 0; placeholderIndex < PlaceholderPositions.Length; placeholderIndex++)
+                {
+                    var placeholder = PlaceholderPositions[placeholderIndex];
+                    var associatedElement = elementItems[placeholderIndex];
+                    
+                    associatedElement.ConfigureElement(PageContext, Canvas);
 
 
-            var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
-            CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
+                    var offset = new Position(placeholder.Left, placeholder.Top);
+                    
+                    if (!IsPositionVisible(offset))
+                        continue;
+                    
+                    Canvas.Translate(offset);
+                    associatedElement.Element.Draw(new Size(placeholder.Width, placeholder.Height));
+                    Canvas.Translate(offset.Reverse());
+                }
+            }
             
             
-            if (!RenderingQueue.Any())
-                ResetState();
+            void DrawHyperlinks()
+            {
+                foreach (var hyperlink in Items.OfType<TextBlockHyperlink>())
+                {
+                    var positions = Paragraph.GetTextRangePositions(hyperlink.ParagraphBeginIndex, hyperlink.ParagraphBeginIndex + hyperlink.Text.Length);
+                    
+                    foreach (var position in positions)
+                    {
+                        var offset = new Position(position.Left, position.Top);
+                        
+                        if (!IsPositionVisible(offset))
+                            continue;
+                        
+                        Canvas.Translate(offset);
+                        Canvas.DrawHyperlink(hyperlink.Url, new Size(position.Width, position.Height));
+                        Canvas.Translate(offset.Reverse());
+                    }
+                }
+            }
             
             
-            float GetAlignmentOffset(float lineWidth)
+            void DrawSectionLinks()
             {
             {
-                var emptySpace = availableSpace.Width - lineWidth;
-
-                return Alignment switch
+                foreach (var sectionLink in Items.OfType<TextBlockSectionLink>())
                 {
                 {
-                    HorizontalAlignment.Left => ContentDirection == ContentDirection.LeftToRight ? 0 : emptySpace,
-                    HorizontalAlignment.Center => emptySpace / 2,
-                    HorizontalAlignment.Right => ContentDirection == ContentDirection.LeftToRight ? emptySpace : 0,
-                    _ => 0
-                };
+                    var positions = Paragraph.GetTextRangePositions(sectionLink.ParagraphBeginIndex, sectionLink.ParagraphBeginIndex + sectionLink.Text.Length);
+                    var targetName = PageContext.GetDocumentLocationName(sectionLink.SectionName);
+                    
+                    foreach (var position in positions)
+                    {
+                        var offset = new Position(position.Left, position.Top);
+                        
+                        if (!IsPositionVisible(offset))
+                            continue;
+                        
+                        Canvas.Translate(offset);
+                        Canvas.DrawSectionLink(targetName, new Size(position.Width, position.Height));
+                        Canvas.Translate(offset.Reverse());
+                    }
+                }
             }
             }
-        }
 
 
-        public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
+            bool IsPositionVisible(Position position)
+            {
+                return CurrentTopOffset <= position.Y || position.Y <= CurrentTopOffset + takenHeight;
+            }
+        }
+        
+        private void Initialize()
         {
         {
-            var queue = new Queue<ITextBlockItem>(RenderingQueue);
-            var currentItemIndex = CurrentElementIndex;
-            var currentHeight = 0f;
+            if (Paragraph != null && !RebuildParagraphForEveryPage)
+                return;
+            
+            RebuildParagraphForEveryPage = Items.Any(x => x is TextBlockPageNumber);
+            BuildParagraph();
+            
+            AreParagraphMetricsValid = false;
+        }
 
 
-            while (queue.Any())
+        private void BuildParagraph()
+        {
+            var paragraphStyle = new ParagraphStyleConfiguration
             {
             {
-                var line = GetNextLine();
-                
-                if (!line.Elements.Any())
-                    yield break;
-                
-                if (currentHeight + line.LineHeight > availableHeight + Size.Epsilon)
-                    yield break;
+                Alignment = MapAlignment(Alignment ?? TextHorizontalAlignment.Start),
+                Direction = MapDirection(ContentDirection),
+                MaxLinesVisible = LineClamp ?? 1_000_000
+            };
+            
+            var builder = SkParagraphBuilderPoolManager.Get(paragraphStyle);
 
 
-                currentHeight += line.LineHeight;
-                yield return line;
+            try
+            {
+                Paragraph = CreateParagraph(builder);
+            }
+            finally
+            {
+                SkParagraphBuilderPoolManager.Return(builder);
             }
             }
 
 
-            TextLine GetNextLine()
+            static ParagraphStyleConfiguration.TextAlign MapAlignment(TextHorizontalAlignment alignment)
             {
             {
-                var currentWidth = 0f;
+                return alignment switch
+                {
+                    TextHorizontalAlignment.Left => ParagraphStyleConfiguration.TextAlign.Left,
+                    TextHorizontalAlignment.Center => ParagraphStyleConfiguration.TextAlign.Center,
+                    TextHorizontalAlignment.Right => ParagraphStyleConfiguration.TextAlign.Right,
+                    TextHorizontalAlignment.Justify => ParagraphStyleConfiguration.TextAlign.Justify,
+                    TextHorizontalAlignment.Start => ParagraphStyleConfiguration.TextAlign.Start,
+                    TextHorizontalAlignment.End => ParagraphStyleConfiguration.TextAlign.End,
+                    _ => throw new Exception()
+                };
+            }
 
 
-                var currentLineElements = new List<TextLineElement>();
+            static ParagraphStyleConfiguration.TextDirection MapDirection(ContentDirection direction)
+            {
+                return direction switch
+                {
+                    ContentDirection.LeftToRight => ParagraphStyleConfiguration.TextDirection.Ltr,
+                    ContentDirection.RightToLeft => ParagraphStyleConfiguration.TextDirection.Rtl,
+                    _ => throw new Exception()
+                };
+            }
             
             
-                while (true)
+            static SkPlaceholderStyle.PlaceholderAlignment MapInjectedTextAlignment(TextInjectedElementAlignment alignment)
+            {
+                return alignment switch
                 {
                 {
-                    if (!queue.Any())
-                        break;
+                    TextInjectedElementAlignment.AboveBaseline => SkPlaceholderStyle.PlaceholderAlignment.AboveBaseline,
+                    TextInjectedElementAlignment.BelowBaseline => SkPlaceholderStyle.PlaceholderAlignment.BelowBaseline,
+                    TextInjectedElementAlignment.Top => SkPlaceholderStyle.PlaceholderAlignment.Top,
+                    TextInjectedElementAlignment.Bottom => SkPlaceholderStyle.PlaceholderAlignment.Bottom,
+                    TextInjectedElementAlignment.Middle => SkPlaceholderStyle.PlaceholderAlignment.Middle,
+                    _ => throw new Exception()
+                };
+            }
 
 
-                    var currentElement = queue.Peek();
-                    
-                    var measurementRequest = new TextMeasurementRequest
+            SkParagraph CreateParagraph(SkParagraphBuilder builder)
+            {
+                var currentTextIndex = 0;
+            
+                foreach (var textBlockItem in Items)
+                {
+                    if (textBlockItem is TextBlockSpan textBlockSpan)
                     {
                     {
-                        Canvas = Canvas,
-                        PageContext = PageContext,
-                        
-                        StartIndex = currentItemIndex,
-                        AvailableWidth = availableWidth - currentWidth,
-                        
-                        IsFirstElementInBlock = currentElement == Items.First(),
-                        IsFirstElementInLine = !currentLineElements.Any()
-                    };
-                
-                    var measurementResponse = currentElement.Measure(measurementRequest);
+                        if (textBlockItem is TextBlockSectionLink textBlockSectionLink)
+                            textBlockSectionLink.ParagraphBeginIndex = currentTextIndex;
+            
+                        else if (textBlockItem is TextBlockHyperlink textBlockHyperlink)
+                            textBlockHyperlink.ParagraphBeginIndex = currentTextIndex;
+            
+                        else if (textBlockItem is TextBlockPageNumber textBlockPageNumber)
+                            textBlockPageNumber.UpdatePageNumberText(PageContext);
                 
                 
-                    if (measurementResponse == null)
-                        break;
-                    
-                    currentLineElements.Add(new TextLineElement
+                        var textStyle = textBlockSpan.Style.GetSkTextStyle();
+                        builder.AddText(textBlockSpan.Text, textStyle);
+                        currentTextIndex += textBlockSpan.Text.Length;
+                    }
+                    else if (textBlockItem is TextBlockElement textBlockElement)
                     {
                     {
-                        Item = currentElement,
-                        Measurement = measurementResponse
-                    });
-
-                    currentWidth += measurementResponse.Width;
-                    currentItemIndex = measurementResponse.NextIndex;
+                        textBlockElement.ConfigureElement(PageContext, Canvas);
+                        textBlockElement.UpdateElementSize();
                     
                     
-                    if (!measurementResponse.IsLast)
-                        break;
-
-                    currentItemIndex = 0;
-                    queue.Dequeue();
+                        builder.AddPlaceholder(new SkPlaceholderStyle
+                        {
+                            Width = textBlockElement.ElementSize.Width,
+                            Height = textBlockElement.ElementSize.Height,
+                            Alignment = MapInjectedTextAlignment(textBlockElement.Alignment),
+                            Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
+                            BaselineOffset = 0
+                        });
+                    }
                 }
                 }
 
 
-                return TextLine.From(currentLineElements);
+                return builder.CreateParagraph();
             }
             }
         }
         }
+        
+        private void CalculateParagraphMetrics(Size availableSpace)
+        {
+            // SkParagraph seems to require a bigger space buffer to calculate metrics correctly
+            const float epsilon = 1f;
+            
+            if (Math.Abs(WidthForLineMetricsCalculation - availableSpace.Width) > epsilon)
+                AreParagraphMetricsValid = false;
+            
+            if (AreParagraphMetricsValid) 
+                return;
+            
+            WidthForLineMetricsCalculation = availableSpace.Width;
+                
+            Paragraph.PlanLayout(availableSpace.Width + epsilon);
+            CheckUnresolvedGlyphs();
+                
+            LineMetrics = Paragraph.GetLineMetrics();
+            PlaceholderPositions = Paragraph.GetPlaceholderPositions();
+            MaximumWidth = LineMetrics.Any() ? LineMetrics.Max(x => x.Width) : 0;
+            
+            AreParagraphMetricsValid = true;
+        }
+        
+        private void CheckUnresolvedGlyphs()
+        {
+            if (!Settings.CheckIfAllTextGlyphsAreAvailable)
+                return;
+                
+            var unsupportedGlyphs = Paragraph.GetUnresolvedCodepoints();
+                   
+            if (!unsupportedGlyphs.Any())
+                return;
+                
+            var formattedGlyphs = unsupportedGlyphs    
+                .Select(codepoint =>
+                {
+                    var character = char.ConvertFromUtf32(codepoint);
+                    return $"U-{codepoint:X4} '{character}'";
+                });
+                
+            var glyphs = string.Join("\n", formattedGlyphs);
+
+            throw new DocumentDrawingException(
+                $"Could not find an appropriate font fallback for the following glyphs: \n" +
+                $"${glyphs} \n\n" +
+                $"Possible solutions: \n" +
+                $"1) Install fonts that contain missing glyphs in your runtime environment. \n" +
+                $"2) Configure the fallback TextStyle using the 'TextStyle.FontFamilyFallback' method. \n" +
+                $"3) Register additional application specific fonts using the 'FontManager.RegisterFont' method. \n\n" +
+                $"You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. \n" +
+                $"However, this may result with text glyphs being incorrectly rendered without any warning.");
+        }
     }
     }
 }
 }

+ 1 - 2
Source/QuestPDF/Fluent/AlignmentExtensions.cs

@@ -1,5 +1,4 @@
-using System;
-using QuestPDF.Elements;
+using QuestPDF.Elements;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Fluent
 namespace QuestPDF.Fluent

+ 2 - 5
Source/QuestPDF/Fluent/BorderExtensions.cs

@@ -1,5 +1,4 @@
-using System;
-using QuestPDF.Elements;
+using QuestPDF.Elements;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
@@ -94,10 +93,8 @@ namespace QuestPDF.Fluent
         /// <a href="https://www.questpdf.com/api-reference/border.html">Learn more</a>
         /// <a href="https://www.questpdf.com/api-reference/border.html">Learn more</a>
         /// </summary>
         /// </summary>
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="colorParam"]/*' />
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="colorParam"]/*' />
-        public static IContainer BorderColor(this IContainer element, string color)
+        public static IContainer BorderColor(this IContainer element, Color color)
         {
         {
-            ColorValidator.Validate(color);
-            
             var border = element as Border ?? new Border();
             var border = element as Border ?? new Border();
             border.Color = color;
             border.Color = color;
             return element.Element(border);
             return element.Element(border);

+ 1 - 2
Source/QuestPDF/Fluent/ConstrainedExtensions.cs

@@ -1,5 +1,4 @@
-using System;
-using QuestPDF.Elements;
+using QuestPDF.Elements;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Fluent
 namespace QuestPDF.Fluent

+ 2 - 4
Source/QuestPDF/Fluent/DebugExtensions.cs

@@ -15,17 +15,15 @@ namespace QuestPDF.Fluent
         /// </summary>
         /// </summary>
         /// <param name="text">Optional label displayed within the box.</param>
         /// <param name="text">Optional label displayed within the box.</param>
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="colorParam"]/*' />
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="colorParam"]/*' />
-        public static IContainer DebugArea(this IContainer parent, string? text = null, string color = Colors.Red.Medium)
+        public static IContainer DebugArea(this IContainer parent, string? text = null, Color? color = null)
         {
         {
-            ColorValidator.Validate(color);
-            
             var container = new Container();
             var container = new Container();
 
 
             parent.Component(new DebugArea
             parent.Component(new DebugArea
             {
             {
                 Child = container,
                 Child = container,
                 Text = text ?? string.Empty,
                 Text = text ?? string.Empty,
-                Color = color
+                Color = color ?? Colors.Red.Medium
             });
             });
 
 
             return container;
             return container;

+ 16 - 19
Source/QuestPDF/Fluent/ElementExtensions.cs

@@ -89,10 +89,8 @@ namespace QuestPDF.Fluent
         /// <a href="https://www.questpdf.com/api-reference/background.html">Learn more</a>
         /// <a href="https://www.questpdf.com/api-reference/background.html">Learn more</a>
         /// </summary>
         /// </summary>
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="colorParam"]/*' />
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="colorParam"]/*' />
-        public static IContainer Background(this IContainer element, string color)
+        public static IContainer Background(this IContainer element, Color color)
         {
         {
-            ColorValidator.Validate(color);
-            
             return element.Element(new Background
             return element.Element(new Background
             {
             {
                 Color = color
                 Color = color
@@ -298,22 +296,6 @@ namespace QuestPDF.Fluent
             });
             });
         }
         }
         
         
-        /// <summary>
-        /// Provides direct access to the low-level SkiaSharp API.
-        /// <a href="https://www.questpdf.com/api-reference/canvas.html">Learn more</a>
-        /// </summary>
-        /// <example>
-        /// <para>Use this element when needing to render advanced shapes not directly available in the QuestPDF API.</para>
-        /// <para>It's also ideal for integrating with other SkiaSharp-based libraries, such as charting tools, to produce pixel-perfect vector graphics.</para>
-        /// </example>
-        public static void Canvas(this IContainer element, DrawOnCanvas handler)
-        {
-            element.Element(new Canvas
-            {
-                Handler = handler
-            });
-        }
-        
         /// <summary>
         /// <summary>
         /// Removes size constraints and grants its content virtually unlimited space.
         /// Removes size constraints and grants its content virtually unlimited space.
         /// <a href="https://www.questpdf.com/api-reference/unconstrained.html">Learn more</a>
         /// <a href="https://www.questpdf.com/api-reference/unconstrained.html">Learn more</a>
@@ -376,5 +358,20 @@ namespace QuestPDF.Fluent
         {
         {
             return element.Element(new ScaleToFit());
             return element.Element(new ScaleToFit());
         }
         }
+
+        #region Canvas [Obsolete]
+
+        private const string CanvasDeprecatedMessage = "The Canvas API has been deprecated since version 2024.3.0. Please use the .Svg(stringContent) API to provide custom content, and consult documentation webpage regarding integrating SkiaSharp with QuestPDF.";
+        
+        [Obsolete(CanvasDeprecatedMessage)]
+        public delegate void DrawOnCanvas(object canvas, Size availableSpace);
+        
+        [Obsolete(CanvasDeprecatedMessage)]
+        public static void Canvas(this IContainer element, DrawOnCanvas handler)
+        {
+            throw new NotImplementedException(CanvasDeprecatedMessage);
+        }
+
+        #endregion
     }
     }
 }
 }

Some files were not shown because too many files changed in this diff