Browse Source

Z-index feature (#1224)

* Initial implementation of z-index (caching is not working)

* Refactor canvas interfaces: replace IRenderingCanvas with IDocumentCanvas

* Removed usage of deprecated skia API

* Refactoring: IDocumentCanvas and IDrawingCanvas

* Improve QuestPDF.Drawing folder structure

* Fixed IDisposable pattern and calls

* Initial implementation of z-index (caching is not working)

* Refactor canvas interfaces: replace IRenderingCanvas with IDocumentCanvas

* Removed usage of deprecated skia API

* Refactoring: IDocumentCanvas and IDrawingCanvas

* Improve QuestPDF.Drawing folder structure

* Fixed IDisposable pattern and calls

* Fix rendering bugs for ZIndex

* Improve XML documentation for ZIndex

* Add documentation example for ZIndex
Marcin Ziąbek 8 months ago
parent
commit
3a1f74fe5f
42 changed files with 1091 additions and 566 deletions
  1. 70 0
      Source/QuestPDF.DocumentationExamples/ZIndexExamples.cs
  2. 3 4
      Source/QuestPDF.LayoutTests/TestEngine/LayoutTestExecutor.cs
  3. 1 4
      Source/QuestPDF.LayoutTests/TestEngine/MockChild.cs
  4. 15 0
      Source/QuestPDF.ReportSample/Tests.cs
  5. 12 2
      Source/QuestPDF.UnitTests/TestEngine/MockCanvas.cs
  6. 12 2
      Source/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs
  7. 2 2
      Source/QuestPDF.UnitTests/TestEngine/TestPlan.cs
  8. 1 0
      Source/QuestPDF/Companion/CompanionService.cs
  9. 49 31
      Source/QuestPDF/Drawing/DocumentCanvases/CompanionDocumentCanvas.cs
  10. 34 0
      Source/QuestPDF/Drawing/DocumentCanvases/FreeDocumentCanvas.cs
  11. 45 18
      Source/QuestPDF/Drawing/DocumentCanvases/ImageDocumentCanvas.cs
  12. 122 0
      Source/QuestPDF/Drawing/DocumentCanvases/PdfDocumentCanvas.cs
  13. 86 0
      Source/QuestPDF/Drawing/DocumentCanvases/SvgDocumentCanvas.cs
  14. 94 0
      Source/QuestPDF/Drawing/DocumentCanvases/XpsDocumentCanvas.cs
  15. 24 26
      Source/QuestPDF/Drawing/DocumentGenerator.cs
  16. 38 0
      Source/QuestPDF/Drawing/DocumentPageSnapshot.cs
  17. 25 30
      Source/QuestPDF/Drawing/DrawingCanvases/FreeDrawingCanvas.cs
  18. 64 13
      Source/QuestPDF/Drawing/DrawingCanvases/ProxyDrawingCanvas.cs
  19. 221 0
      Source/QuestPDF/Drawing/DrawingCanvases/SkiaDrawingCanvas.cs
  20. 0 58
      Source/QuestPDF/Drawing/PdfCanvas.cs
  21. 1 4
      Source/QuestPDF/Drawing/Proxy/LayoutOverflowVisualization.cs
  22. 9 10
      Source/QuestPDF/Drawing/Proxy/LayoutProxy.cs
  23. 21 20
      Source/QuestPDF/Drawing/Proxy/SnapshotCacheRecorderProxy.cs
  24. 0 156
      Source/QuestPDF/Drawing/SkiaCanvasBase.cs
  25. 0 57
      Source/QuestPDF/Drawing/SkiaDocumentCanvasBase.cs
  26. 0 14
      Source/QuestPDF/Drawing/SnapshotRecorderCanvas.cs
  27. 0 57
      Source/QuestPDF/Drawing/SvgCanvas.cs
  28. 0 28
      Source/QuestPDF/Drawing/XpsCanvas.cs
  29. 1 1
      Source/QuestPDF/Elements/Dynamic.cs
  30. 17 3
      Source/QuestPDF/Elements/MultiColumn.cs
  31. 2 1
      Source/QuestPDF/Elements/Page.cs
  32. 1 1
      Source/QuestPDF/Elements/Text/Items/TextBlockElement.cs
  33. 23 0
      Source/QuestPDF/Elements/ZIndex.cs
  34. 15 0
      Source/QuestPDF/Fluent/ElementExtensions.cs
  35. 1 1
      Source/QuestPDF/Infrastructure/Element.cs
  36. 2 4
      Source/QuestPDF/Infrastructure/IDocumentCanvas.cs
  37. 13 4
      Source/QuestPDF/Infrastructure/IDrawingCanvas.cs
  38. BIN
      Source/QuestPDF/Runtimes/osx-arm64/native/libQuestPdfSkia.dylib
  39. 43 12
      Source/QuestPDF/Skia/SkCanvas.cs
  40. 1 1
      Source/QuestPDF/Skia/SkNativeDependencyCompatibilityChecker.cs
  41. 2 2
      Source/QuestPDF/Skia/Text/SkParagraphBuilder.cs
  42. 21 0
      Source/QuestPDF/Skia/Text/SkUnicode.cs

+ 70 - 0
Source/QuestPDF.DocumentationExamples/ZIndexExamples.cs

@@ -0,0 +1,70 @@
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.DocumentationExamples;
+
+public class ZIndexExamples
+{
+    [Test]
+    public void Example()
+    {
+        Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.MinSize(new PageSize(650, 0));
+                    page.MaxSize(new PageSize(650, 1000));
+                    page.DefaultTextStyle(x => x.FontSize(20));
+                    page.Margin(25);
+
+                    page.Content()
+                        .PaddingVertical(15)
+                        .Border(2)
+                        .Row(row =>
+                        {
+                            row.RelativeItem()
+                                .Background(Colors.Grey.Lighten3)
+                                .Element(c => AddPricingItem(c, "Community", "Free"));
+                            
+                            row.RelativeItem()
+                                .ZIndex(1) // -1 or 0 or 1
+                                .Padding(-15)
+                                .Border(1)
+                                .Background(Colors.Grey.Lighten1)
+                                .PaddingTop(15)
+                                .Element(c => AddPricingItem(c, "Professional", "$699"));
+                            
+                            row.RelativeItem()
+                                .Background(Colors.Grey.Lighten3)
+                                .Element(c => AddPricingItem(c, "Enterprise", "$1999")); 
+
+                            void AddPricingItem(IContainer container, string name, string formattedPrice)
+                            {
+                                container
+                                    .Padding(25)
+                                    .Column(column =>
+                                    {
+                                        column.Item().AlignCenter().Text(name).FontSize(24).Black();
+                                        column.Item().AlignCenter().Text(formattedPrice).FontSize(20).SemiBold();
+                                        
+                                        column.Item().PaddingHorizontal(-25).PaddingVertical(10).LineHorizontal(1);
+                                        
+                                        foreach (var i in Enumerable.Range(1, 4))
+                                        {
+                                            column.Item()
+                                                .PaddingTop(10)
+                                                .AlignCenter()
+                                                .Text(Placeholders.Label())
+                                                .FontSize(16)
+                                                .Light();
+                                        }
+                                    });
+                            }
+                        });
+                });
+            })
+            .GenerateImages(x => "zindex-positive.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 });
+    }
+}

+ 3 - 4
Source/QuestPDF.LayoutTests/TestEngine/LayoutTestExecutor.cs

@@ -1,4 +1,5 @@
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
+using QuestPDF.Drawing.DocumentCanvases;
 using QuestPDF.Drawing.Proxy;
 using QuestPDF.Drawing.Proxy;
 using QuestPDF.Elements;
 using QuestPDF.Elements;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
@@ -25,9 +26,9 @@ internal static class LayoutTestExecutor
             var pageContext = new PageContext();
             var pageContext = new PageContext();
             pageContext.ProceedToNextRenderingPhase();
             pageContext.ProceedToNextRenderingPhase();
 
 
-            using var canvas = new CompanionCanvas();
+            using var canvas = new CompanionDocumentCanvas();
         
         
-            container.InjectDependencies(pageContext, canvas);
+            container.InjectDependencies(pageContext, canvas.GetDrawingCanvas());
         
         
             // distribute global state
             // distribute global state
             container.ApplyInheritedAndGlobalTexStyle(TextStyle.Default);
             container.ApplyInheritedAndGlobalTexStyle(TextStyle.Default);
@@ -36,8 +37,6 @@ internal static class LayoutTestExecutor
         
         
             // render
             // render
             container.VisitChildren(x => (x as IStateful)?.ResetState());
             container.VisitChildren(x => (x as IStateful)?.ResetState());
-        
-            canvas.BeginDocument();
             
             
             var pageSizes = new List<Size>();
             var pageSizes = new List<Size>();
         
         

+ 1 - 4
Source/QuestPDF.LayoutTests/TestEngine/MockChild.cs

@@ -51,10 +51,7 @@ internal class ElementMock : Element
         
         
         Canvas.DrawFilledRectangle(Position.Zero, size, Colors.Grey.Medium);
         Canvas.DrawFilledRectangle(Position.Zero, size, Colors.Grey.Medium);
         
         
-        if (Canvas is not SkiaCanvasBase canvasBase)
-            return;
-
-        var matrix = canvasBase.Canvas.GetCurrentTotalMatrix();
+        var matrix = Canvas.GetCurrentMatrix();
         
         
         DrawingCommands.Add(new MockDrawingCommand
         DrawingCommands.Add(new MockDrawingCommand
         {
         {

+ 15 - 0
Source/QuestPDF.ReportSample/Tests.cs

@@ -56,5 +56,20 @@ namespace QuestPDF.ReportSample
                 report.GeneratePdf();
                 report.GeneratePdf();
             });
             });
         }
         }
+        
+        [Test]
+        public async Task CheckFinalizersStability()
+        {
+            Settings.EnableCaching = true;
+
+            Report.GeneratePdf();
+            Report.GenerateImages();
+            Report.GenerateSvg();
+
+            await Task.Delay(1000);
+            GC.Collect();
+            GC.WaitForPendingFinalizers();
+            await Task.Delay(1000);
+        }
     }
     }
 }
 }

+ 12 - 2
Source/QuestPDF.UnitTests/TestEngine/MockCanvas.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using QuestPDF.Drawing;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.Skia;
 using QuestPDF.Skia;
 using QuestPDF.Skia.Text;
 using QuestPDF.Skia.Text;
@@ -6,7 +7,7 @@ using SkiaSharp;
 
 
 namespace QuestPDF.UnitTests.TestEngine
 namespace QuestPDF.UnitTests.TestEngine
 {
 {
-    internal sealed class MockCanvas : ICanvas
+    internal sealed class MockCanvas : IDrawingCanvas
     {
     {
         public Action<Position> TranslateFunc { get; set; }
         public Action<Position> TranslateFunc { get; set; }
         public Action<float> RotateFunc { get; set; }
         public Action<float> RotateFunc { get; set; }
@@ -14,12 +15,21 @@ namespace QuestPDF.UnitTests.TestEngine
         public Action<SkImage, Position, Size> DrawImageFunc { get; set; }
         public Action<SkImage, Position, Size> DrawImageFunc { get; set; }
         public Action<Position, Size, Color> DrawRectFunc { get; set; }
         public Action<Position, Size, Color> DrawRectFunc { get; set; }
 
 
+        public DocumentPageSnapshot GetSnapshot() => throw new NotImplementedException();
+        public void DrawSnapshot(DocumentPageSnapshot snapshot) => throw new NotImplementedException();
+
         public void Save() => throw new NotImplementedException();
         public void Save() => throw new NotImplementedException();
         public void Restore() => throw new NotImplementedException();
         public void Restore() => throw new NotImplementedException();
         
         
+        public void SetZIndex(int index) => throw new NotImplementedException();
+        public int GetZIndex() => throw new NotImplementedException();
+        
+        public SkCanvasMatrix GetCurrentMatrix() => throw new NotImplementedException();
+        public void SetMatrix(SkCanvasMatrix matrix) => 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 Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);
         public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);
+        public void Rotate(float angle) => RotateFunc(angle);
 
 
         public void DrawFilledRectangle(Position vector, Size size, Color color) => DrawRectFunc(vector, size, color);
         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 DrawStrokeRectangle(Position vector, Size size, float strokeWidth, Color color) => throw new NotImplementedException();

+ 12 - 2
Source/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using QuestPDF.Drawing;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.Skia;
 using QuestPDF.Skia;
 using QuestPDF.Skia.Text;
 using QuestPDF.Skia.Text;
@@ -7,16 +8,25 @@ using QuestPDF.UnitTests.TestEngine.Operations;
 
 
 namespace QuestPDF.UnitTests.TestEngine
 namespace QuestPDF.UnitTests.TestEngine
 {
 {
-    internal sealed class OperationRecordingCanvas : ICanvas
+    internal sealed class OperationRecordingCanvas : IDrawingCanvas
     {
     {
         public ICollection<OperationBase> Operations { get; } = new List<OperationBase>();
         public ICollection<OperationBase> Operations { get; } = new List<OperationBase>();
         
         
+        public DocumentPageSnapshot GetSnapshot() => throw new NotImplementedException();
+        public void DrawSnapshot(DocumentPageSnapshot snapshot) => throw new NotImplementedException();
+        
         public void Save() => throw new NotImplementedException();
         public void Save() => throw new NotImplementedException();
         public void Restore() => throw new NotImplementedException();
         public void Restore() => throw new NotImplementedException();
         
         
+        public void SetZIndex(int index) => throw new NotImplementedException();
+        public int GetZIndex() => throw new NotImplementedException();
+        
+        public SkCanvasMatrix GetCurrentMatrix() => throw new NotImplementedException();
+        public void SetMatrix(SkCanvasMatrix matrix) => 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 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 Rotate(float angle) => Operations.Add(new CanvasRotateOperation(angle));
 
 
         public void DrawFilledRectangle(Position vector, Size size, Color color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
         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 DrawStrokeRectangle(Position vector, Size size, float strokeWidth, Color color) => throw new NotImplementedException();

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

@@ -20,7 +20,7 @@ namespace QuestPDF.UnitTests.TestEngine
         private static Random Random { get; } = new Random();
         private static Random Random { get; } = new Random();
         
         
         private Element Element { get; set; }
         private Element Element { get; set; }
-        private ICanvas Canvas { get; }
+        private IDrawingCanvas Canvas { get; }
         
         
         private Size OperationInput { get; set; }
         private Size OperationInput { get; set; }
         private Queue<OperationBase> Operations { get; } = new Queue<OperationBase>();
         private Queue<OperationBase> Operations { get; } = new Queue<OperationBase>();
@@ -48,7 +48,7 @@ namespace QuestPDF.UnitTests.TestEngine
             return null;
             return null;
         }
         }
         
         
-        private ICanvas CreateCanvas()
+        private IDrawingCanvas CreateCanvas()
         {
         {
             return new MockCanvas
             return new MockCanvas
             {
             {

+ 1 - 0
Source/QuestPDF/Companion/CompanionService.cs

@@ -11,6 +11,7 @@ using System.Text.Json.Serialization;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
+using QuestPDF.Drawing.DocumentCanvases;
 
 
 namespace QuestPDF.Companion
 namespace QuestPDF.Companion
 {
 {

+ 49 - 31
Source/QuestPDF/Drawing/CompanionCanvas.cs → Source/QuestPDF/Drawing/DocumentCanvases/CompanionDocumentCanvas.cs

@@ -1,10 +1,13 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
 using QuestPDF.Companion;
 using QuestPDF.Companion;
+using QuestPDF.Drawing.DrawingCanvases;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.Skia;
 using QuestPDF.Skia;
 
 
-namespace QuestPDF.Drawing
+namespace QuestPDF.Drawing.DocumentCanvases
 {
 {
     internal sealed class CompanionPageSnapshot
     internal sealed class CompanionPageSnapshot
     {
     {
@@ -32,69 +35,84 @@ namespace QuestPDF.Drawing
     internal sealed class CompanionDocumentSnapshot
     internal sealed class CompanionDocumentSnapshot
     {
     {
         public ICollection<CompanionPageSnapshot> Pictures { get; set; }
         public ICollection<CompanionPageSnapshot> Pictures { get; set; }
-        public bool DocumentContentHasLayoutOverflowIssues { get; set; }
         public CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement Hierarchy { get; set; }
         public CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement Hierarchy { get; set; }
     }
     }
     
     
-    internal sealed class CompanionCanvas : SkiaCanvasBase, IDisposable
+    internal sealed class CompanionDocumentCanvas : IDocumentCanvas, IDisposable
     {
     {
-        private SkPictureRecorder? PictureRecorder { get; set; }
-        private Size? CurrentPageSize { get; set; }
+        private ProxyDrawingCanvas DrawingCanvas { get; } = new();
+        private Size CurrentPageSize { get; set; } = Size.Zero;
 
 
         private ICollection<CompanionPageSnapshot> PageSnapshots { get; } = new List<CompanionPageSnapshot>();
         private ICollection<CompanionPageSnapshot> PageSnapshots { get; } = new List<CompanionPageSnapshot>();
         
         
         internal CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement Hierarchy { get; set; }
         internal CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement Hierarchy { get; set; }
+
+        public CompanionDocumentSnapshot GetContent()
+        {
+            return new CompanionDocumentSnapshot
+            {
+                Pictures = PageSnapshots,
+                Hierarchy = Hierarchy
+            };
+        }
+
+        #region IDisposable
         
         
-        ~CompanionCanvas()
+        ~CompanionDocumentCanvas()
         {
         {
             this.WarnThatFinalizerIsReached();
             this.WarnThatFinalizerIsReached();
             Dispose();
             Dispose();
         }
         }
-
+        
         public void Dispose()
         public void Dispose()
         {
         {
-            Canvas?.Dispose();
-            PictureRecorder?.Dispose();
-            // do not dispose PageSnapshots, they are used by the CompanionDocumentSnapshot
+            DrawingCanvas.Dispose();
             GC.SuppressFinalize(this);
             GC.SuppressFinalize(this);
         }
         }
         
         
-        public override void BeginDocument()
+        #endregion
+        
+        #region IDocumentCanvas
+        
+        public void BeginDocument()
         {
         {
             PageSnapshots.Clear();
             PageSnapshots.Clear();
         }
         }
 
 
-        public override void BeginPage(Size size)
+        public void EndDocument()
         {
         {
-            CurrentPageSize = size;
-            PictureRecorder = new SkPictureRecorder();
+            
+        }
 
 
-            Canvas = PictureRecorder.BeginRecording(size.Width, size.Height);
+        public void BeginPage(Size size)
+        {
+            CurrentPageSize = size;
+            
+            DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
+            DrawingCanvas.SetZIndex(0);
         }
         }
 
 
-        public override void EndPage()
+        public void EndPage()
         {
         {
-            var picture = PictureRecorder?.EndRecording();
+            Debug.Assert(!CurrentPageSize.IsCloseToZero());
             
             
-            if (picture != null && CurrentPageSize.HasValue)
-                PageSnapshots.Add(new CompanionPageSnapshot(picture, CurrentPageSize.Value));
+            using var pictureRecorder = new SkPictureRecorder();
+            using var canvas = pictureRecorder.BeginRecording(CurrentPageSize.Width, CurrentPageSize.Height);
 
 
-            PictureRecorder?.Dispose();
-            PictureRecorder = null;
+            using var snapshot = DrawingCanvas.GetSnapshot();
+            snapshot.DrawOnSkCanvas(canvas);
+            canvas.Save();
             
             
-            Canvas?.Dispose();
+            var picture = pictureRecorder.EndRecording();
+            PageSnapshots.Add(new CompanionPageSnapshot(picture, CurrentPageSize));
         }
         }
 
 
-        public override void EndDocument() { }
-
-        public CompanionDocumentSnapshot GetContent()
+        
+        public IDrawingCanvas GetDrawingCanvas()
         {
         {
-            return new CompanionDocumentSnapshot
-            {
-                Pictures = PageSnapshots,
-                DocumentContentHasLayoutOverflowIssues = DocumentContentHasLayoutOverflowIssues,
-                Hierarchy = Hierarchy
-            };
+            return DrawingCanvas;
         }
         }
+        
+        #endregion
     }
     }
 }
 }

+ 34 - 0
Source/QuestPDF/Drawing/DocumentCanvases/FreeDocumentCanvas.cs

@@ -0,0 +1,34 @@
+using QuestPDF.Drawing.DrawingCanvases;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Drawing.DocumentCanvases;
+
+internal sealed class FreeDocumentCanvas : IDocumentCanvas
+{
+    private FreeDrawingCanvas DrawingCanvas { get; } = new();
+        
+    public void BeginDocument()
+    {
+            
+    }
+
+    public void EndDocument()
+    {
+            
+    }
+
+    public void BeginPage(Size size)
+    {
+            
+    }
+
+    public void EndPage()
+    {
+            
+    }
+
+    public IDrawingCanvas GetDrawingCanvas()
+    {
+        return DrawingCanvas;
+    }
+}

+ 45 - 18
Source/QuestPDF/Drawing/ImageCanvas.cs → Source/QuestPDF/Drawing/DocumentCanvases/ImageDocumentCanvas.cs

@@ -1,63 +1,83 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
+using QuestPDF.Drawing.DrawingCanvases;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.Skia;
 using QuestPDF.Skia;
 
 
-namespace QuestPDF.Drawing
+namespace QuestPDF.Drawing.DocumentCanvases
 {
 {
-    internal sealed class ImageCanvas : SkiaCanvasBase, IDisposable
+    internal sealed class ImageDocumentCanvas : IDocumentCanvas, IDisposable
     {
     {
         private ImageGenerationSettings Settings { get; }
         private ImageGenerationSettings Settings { get; }
         private SkBitmap Bitmap { 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
+        
+        private SkCanvas? CurrentPageCanvas { get; set; }
+        private ProxyDrawingCanvas DrawingCanvas { get; } = new();
+        
         internal ICollection<byte[]> Images { get; } = new List<byte[]>();
         internal ICollection<byte[]> Images { get; } = new List<byte[]>();
         
         
-        public ImageCanvas(ImageGenerationSettings settings)
+        public ImageDocumentCanvas(ImageGenerationSettings settings)
         {
         {
             Settings = settings;
             Settings = settings;
         }
         }
+
+        #region IDisposable
         
         
-        ~ImageCanvas()
+        ~ImageDocumentCanvas()
         {
         {
             this.WarnThatFinalizerIsReached();
             this.WarnThatFinalizerIsReached();
             Dispose();
             Dispose();
         }
         }
-
+        
         public void Dispose()
         public void Dispose()
         {
         {
-            Canvas?.Dispose();
+            CurrentPageCanvas?.Dispose();
             Bitmap?.Dispose();
             Bitmap?.Dispose();
+            DrawingCanvas?.Dispose();
+            
             GC.SuppressFinalize(this);
             GC.SuppressFinalize(this);
         }
         }
         
         
-        public override void BeginDocument()
+        #endregion
+        
+        #region IDocumentCanvas
+        
+        public void BeginDocument()
         {
         {
             
             
         }
         }
 
 
-        public override void EndDocument()
+        public void EndDocument()
         {
         {
-            Canvas?.Dispose();
+            CurrentPageCanvas?.Dispose();
             Bitmap?.Dispose();
             Bitmap?.Dispose();
         }
         }
 
 
-        public override void BeginPage(Size size)
+        public void BeginPage(Size size)
         {
         {
             var scalingFactor = Settings.RasterDpi / (float) PageSizes.PointsPerInch;
             var scalingFactor = Settings.RasterDpi / (float) PageSizes.PointsPerInch;
 
 
             Bitmap = new SkBitmap((int) (size.Width * scalingFactor), (int) (size.Height * scalingFactor));
             Bitmap = new SkBitmap((int) (size.Width * scalingFactor), (int) (size.Height * scalingFactor));
-            Canvas = SkCanvas.CreateFromBitmap(Bitmap);
+            CurrentPageCanvas = SkCanvas.CreateFromBitmap(Bitmap);
+            
+            CurrentPageCanvas.Scale(scalingFactor, scalingFactor);
             
             
-            Canvas.Scale(scalingFactor, scalingFactor);
+            DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
+            DrawingCanvas.SetZIndex(0);
         }
         }
 
 
-        public override void EndPage()
+        public void EndPage()
         {
         {
-            Canvas.Save();
-            Canvas.Dispose();
+            Debug.Assert(CurrentPageCanvas != null);
+            
+            using var documentPageSnapshot = DrawingCanvas.GetSnapshot();
+            documentPageSnapshot.DrawOnSkCanvas(CurrentPageCanvas);
+            
+            CurrentPageCanvas.Save();
+            CurrentPageCanvas.Dispose();
+            CurrentPageCanvas = null;
             
             
             using var imageData = EncodeBitmap();
             using var imageData = EncodeBitmap();
             var imageBytes = imageData.ToBytes();
             var imageBytes = imageData.ToBytes();
@@ -76,5 +96,12 @@ namespace QuestPDF.Drawing
                 };
                 };
             }
             }
         }
         }
+        
+        public IDrawingCanvas GetDrawingCanvas()
+        {
+            return DrawingCanvas;
+        }
+        
+        #endregion
     }
     }
 }
 }

+ 122 - 0
Source/QuestPDF/Drawing/DocumentCanvases/PdfDocumentCanvas.cs

@@ -0,0 +1,122 @@
+using System;
+using System.Diagnostics;
+using QuestPDF.Drawing.DrawingCanvases;
+using QuestPDF.Drawing.Exceptions;
+using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+
+namespace QuestPDF.Drawing.DocumentCanvases
+{
+    internal sealed class PdfDocumentCanvas : IDocumentCanvas, IDisposable
+    {
+        private SkDocument Document { get; }
+        private SkCanvas? CurrentPageCanvas { get; set; }
+        private ProxyDrawingCanvas DrawingCanvas { get; } = new();
+        
+        public PdfDocumentCanvas(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings)
+        {
+            Document = CreatePdf(stream, documentMetadata, 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);
+            using var language = new SkText(documentMetadata.Language);
+            
+            var internalMetadata = new SkPdfDocumentMetadata
+            {
+                Title = title,
+                Author = author,
+                Subject = subject,
+                Keywords = keywords,
+                Creator = creator,
+                Producer = producer,
+                Language = language,
+                
+                CreationDate = new SkDateTime(documentMetadata.CreationDate),
+                ModificationDate = new SkDateTime(documentMetadata.ModifiedDate),
+                
+                RasterDPI = documentSettings.ImageRasterDpi,
+                SupportPDFA = documentSettings.PdfA,
+                CompressDocument = documentSettings.CompressDocument
+            };
+            
+            try
+            {
+                return SkPdfDocument.Create(stream, internalMetadata);
+            }
+            catch (TypeInitializationException exception)
+            {
+                throw new InitializationException("PDF", exception);
+            }
+        }
+        
+        #region IDisposable
+        
+        ~PdfDocumentCanvas()
+        {
+            this.WarnThatFinalizerIsReached();
+            Dispose();
+        }
+        
+        public void Dispose()
+        {
+            Document?.Dispose();
+            CurrentPageCanvas?.Dispose();
+            DrawingCanvas?.Dispose();
+            
+            GC.SuppressFinalize(this);
+        }
+        
+        #endregion
+        
+        #region IDocumentCanvas
+        
+        public void BeginDocument()
+        {
+            
+        }
+
+        public void EndDocument()
+        {
+            Document?.Close();
+            Document?.Dispose();
+        }
+
+        public void BeginPage(Size size)
+        {
+            CurrentPageCanvas = Document?.BeginPage(size.Width, size.Height);
+            
+            DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
+            DrawingCanvas.SetZIndex(0);
+        }
+
+        public void EndPage()
+        {
+            Debug.Assert(CurrentPageCanvas != null);
+            
+            using var documentPageSnapshot = DrawingCanvas.GetSnapshot();
+            documentPageSnapshot.DrawOnSkCanvas(CurrentPageCanvas);
+            
+            CurrentPageCanvas.Save();
+            CurrentPageCanvas.Dispose();
+            CurrentPageCanvas = null;
+            
+            Document.EndPage();
+        }
+        
+        public IDrawingCanvas GetDrawingCanvas()
+        {
+            return DrawingCanvas;
+        }
+        
+        #endregion
+    }
+}

+ 86 - 0
Source/QuestPDF/Drawing/DocumentCanvases/SvgDocumentCanvas.cs

@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using QuestPDF.Drawing.DrawingCanvases;
+using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+
+namespace QuestPDF.Drawing.DocumentCanvases
+{
+    internal sealed class SvgDocumentCanvas : IDocumentCanvas, IDisposable
+    {
+        private SkCanvas? CurrentPageCanvas { get; set; }
+        private ProxyDrawingCanvas DrawingCanvas { get; } = new();
+        
+        private SkWriteStream WriteStream { get; set; }
+        internal ICollection<string> Images { get; } = new List<string>();
+        
+        #region IDisposable
+        
+        ~SvgDocumentCanvas()
+        {
+            this.WarnThatFinalizerIsReached();
+            Dispose();
+        }
+        
+        public void Dispose()
+        {
+            CurrentPageCanvas?.Dispose();
+            WriteStream?.Dispose();
+            DrawingCanvas?.Dispose();
+            
+            GC.SuppressFinalize(this);
+        }
+        
+        #endregion
+        
+        #region IDocumentCanvas
+        
+        public void BeginDocument()
+        {
+            
+        }
+
+        public void EndDocument()
+        {
+            CurrentPageCanvas?.Dispose();
+            WriteStream?.Dispose();
+        }
+
+        public void BeginPage(Size size)
+        {
+            WriteStream?.Dispose();
+            WriteStream = new SkWriteStream();
+            CurrentPageCanvas = SkSvgCanvas.CreateSvg(size.Width, size.Height, WriteStream);
+            
+            DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
+            DrawingCanvas.SetZIndex(0);
+        }
+
+        public void EndPage()
+        {
+            Debug.Assert(CurrentPageCanvas != null);
+
+            using var documentPageSnapshot = DrawingCanvas.GetSnapshot();
+            documentPageSnapshot.DrawOnSkCanvas(CurrentPageCanvas);
+            
+            CurrentPageCanvas.Save();
+            CurrentPageCanvas.Dispose();
+            CurrentPageCanvas = null;
+            
+            using var data = WriteStream.DetachData();
+            var svgImage = Encoding.UTF8.GetString(data.ToBytes());
+            Images.Add(svgImage);
+            
+            WriteStream.Dispose();
+        }
+        
+        public IDrawingCanvas GetDrawingCanvas()
+        {
+            return DrawingCanvas;
+        }
+        
+        #endregion
+    }
+}

+ 94 - 0
Source/QuestPDF/Drawing/DocumentCanvases/XpsDocumentCanvas.cs

@@ -0,0 +1,94 @@
+using System;
+using System.Diagnostics;
+using QuestPDF.Drawing.DrawingCanvases;
+using QuestPDF.Drawing.Exceptions;
+using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+
+namespace QuestPDF.Drawing.DocumentCanvases
+{
+    internal sealed class XpsDocumentCanvas : IDocumentCanvas, IDisposable
+    {
+        private SkDocument Document { get; }
+        private SkCanvas? CurrentPageCanvas { get; set; }
+        private ProxyDrawingCanvas DrawingCanvas { get; } = new();
+        
+        public XpsDocumentCanvas(SkWriteStream stream, DocumentSettings documentSettings)
+        {
+            Document = CreateXps(stream, documentSettings);
+        }
+        
+        private static SkDocument CreateXps(SkWriteStream stream, DocumentSettings documentSettings)
+        {
+            try
+            {
+                return SkXpsDocument.Create(stream, documentSettings.ImageRasterDpi);
+            }
+            catch (TypeInitializationException exception)
+            {
+                throw new InitializationException("XPS", exception);
+            }
+        }
+        
+        #region IDisposable
+        
+        ~XpsDocumentCanvas()
+        {
+            this.WarnThatFinalizerIsReached();
+            Dispose();
+        }
+        
+        public void Dispose()
+        {
+            Document?.Dispose();
+            CurrentPageCanvas?.Dispose();
+            DrawingCanvas?.Dispose();
+            
+            GC.SuppressFinalize(this);
+        }
+        
+        #endregion
+        
+        #region IDocumentCanvas
+        
+        public void BeginDocument()
+        {
+            
+        }
+
+        public void EndDocument()
+        {
+            Document?.Close();
+            Document?.Dispose();
+        }
+
+        public void BeginPage(Size size)
+        {
+            CurrentPageCanvas = Document?.BeginPage(size.Width, size.Height);
+            
+            DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
+            DrawingCanvas.SetZIndex(0);
+        }
+
+        public void EndPage()
+        {
+            Debug.Assert(CurrentPageCanvas != null);
+            
+            using var documentPageSnapshot = DrawingCanvas.GetSnapshot();
+            documentPageSnapshot.DrawOnSkCanvas(CurrentPageCanvas);
+            
+            CurrentPageCanvas.Save();
+            CurrentPageCanvas.Dispose();
+            CurrentPageCanvas = null;
+            
+            Document.EndPage();
+        }
+        
+        public IDrawingCanvas GetDrawingCanvas()
+        {
+            return DrawingCanvas;
+        }
+        
+        #endregion
+    }
+}

+ 24 - 26
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using QuestPDF.Companion;
 using QuestPDF.Companion;
+using QuestPDF.Drawing.DocumentCanvases;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Proxy;
 using QuestPDF.Drawing.Proxy;
 using QuestPDF.Elements;
 using QuestPDF.Elements;
@@ -27,7 +28,7 @@ namespace QuestPDF.Drawing
             
             
             var metadata = document.GetMetadata();
             var metadata = document.GetMetadata();
             var settings = document.GetSettings();
             var settings = document.GetSettings();
-            using var canvas = new PdfCanvas(stream, metadata, settings);
+            using var canvas = new PdfDocumentCanvas(stream, metadata, settings);
             RenderDocument(canvas, document, settings);
             RenderDocument(canvas, document, settings);
         }
         }
         
         
@@ -36,7 +37,7 @@ namespace QuestPDF.Drawing
             ValidateLicense();
             ValidateLicense();
             
             
             var settings = document.GetSettings();
             var settings = document.GetSettings();
-            using var canvas = new XpsCanvas(stream, settings);
+            using var canvas = new XpsDocumentCanvas(stream, settings);
             RenderDocument(canvas, document, settings);
             RenderDocument(canvas, document, settings);
         }
         }
         
         
@@ -47,7 +48,7 @@ namespace QuestPDF.Drawing
             var documentSettings = document.GetSettings();
             var documentSettings = document.GetSettings();
             documentSettings.ImageRasterDpi = imageGenerationSettings.RasterDpi;
             documentSettings.ImageRasterDpi = imageGenerationSettings.RasterDpi;
             
             
-            using var canvas = new ImageCanvas(imageGenerationSettings);
+            using var canvas = new ImageDocumentCanvas(imageGenerationSettings);
             RenderDocument(canvas, document, documentSettings);
             RenderDocument(canvas, document, documentSettings);
 
 
             return canvas.Images;
             return canvas.Images;
@@ -57,7 +58,7 @@ namespace QuestPDF.Drawing
         {
         {
             ValidateLicense();
             ValidateLicense();
             
             
-            using var canvas = new SvgCanvas();
+            using var canvas = new SvgDocumentCanvas();
             RenderDocument(canvas, document, document.GetSettings());
             RenderDocument(canvas, document, document.GetSettings());
 
 
             return canvas.Images;
             return canvas.Images;
@@ -89,7 +90,7 @@ namespace QuestPDF.Drawing
 
 
         internal static CompanionDocumentSnapshot GenerateCompanionContent(IDocument document)
         internal static CompanionDocumentSnapshot GenerateCompanionContent(IDocument document)
         {
         {
-            using var canvas = new CompanionCanvas();
+            using var canvas = new CompanionDocumentCanvas();
             RenderDocument(canvas, document, DocumentSettings.Default);
             RenderDocument(canvas, document, DocumentSettings.Default);
             return canvas.GetContent();
             return canvas.GetContent();
         }
         }
@@ -121,7 +122,7 @@ namespace QuestPDF.Drawing
         /// </summary>
         /// </summary>
         private static readonly SemaphoreSlim RenderDocumentSemaphore = new(4);
         private static readonly SemaphoreSlim RenderDocumentSemaphore = new(4);
         
         
-        private static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document, DocumentSettings settings) where TCanvas : ICanvas, IRenderingCanvas
+        private static void RenderDocument(IDocumentCanvas canvas, IDocument document, DocumentSettings settings)
         {
         {
             RenderDocumentSemaphore.Wait();
             RenderDocumentSemaphore.Wait();
 
 
@@ -143,24 +144,23 @@ namespace QuestPDF.Drawing
             }
             }
         }
         }
 
 
-        private static void RenderSingleDocument<TCanvas>(TCanvas canvas, IDocument document, DocumentSettings settings)
-            where TCanvas : ICanvas, IRenderingCanvas
+        private static void RenderSingleDocument(IDocumentCanvas canvas, IDocument document, DocumentSettings settings)
         {
         {
-            var useOriginalImages = canvas is ImageCanvas;
+            var useOriginalImages = canvas is ImageDocumentCanvas;
 
 
             var content = ConfigureContent(document, settings, useOriginalImages);
             var content = ConfigureContent(document, settings, useOriginalImages);
             
             
-            if (canvas is CompanionCanvas)
+            if (canvas is CompanionDocumentCanvas)
                 content.VisitChildren(x => x.CreateProxy(y => new LayoutProxy(y)));
                 content.VisitChildren(x => x.CreateProxy(y => new LayoutProxy(y)));
             
             
             try
             try
             {
             {
                 var pageContext = new PageContext();
                 var pageContext = new PageContext();
-                RenderPass(pageContext, new FreeCanvas(), content);
+                RenderPass(pageContext, new FreeDocumentCanvas(), content);
                 pageContext.ProceedToNextRenderingPhase();
                 pageContext.ProceedToNextRenderingPhase();
                 RenderPass(pageContext, canvas, content);
                 RenderPass(pageContext, canvas, content);
             
             
-                if (canvas is CompanionCanvas companionCanvas)
+                if (canvas is CompanionDocumentCanvas companionCanvas)
                     companionCanvas.Hierarchy = content.ExtractHierarchy();
                     companionCanvas.Hierarchy = content.ExtractHierarchy();
             }
             }
             finally
             finally
@@ -169,10 +169,9 @@ namespace QuestPDF.Drawing
             }
             }
         }
         }
         
         
-        private static void RenderMergedDocument<TCanvas>(TCanvas canvas, MergedDocument document, DocumentSettings settings)
-            where TCanvas : ICanvas, IRenderingCanvas
+        private static void RenderMergedDocument(IDocumentCanvas canvas, MergedDocument document, DocumentSettings settings)
         {
         {
-            var useOriginalImages = canvas is ImageCanvas;
+            var useOriginalImages = canvas is ImageDocumentCanvas;
             
             
             var documentParts = Enumerable
             var documentParts = Enumerable
                 .Range(0, document.Documents.Count)
                 .Range(0, document.Documents.Count)
@@ -192,7 +191,7 @@ namespace QuestPDF.Drawing
                     foreach (var documentPart in documentParts)
                     foreach (var documentPart in documentParts)
                     {
                     {
                         documentPageContext.SetDocumentId(documentPart.DocumentId);
                         documentPageContext.SetDocumentId(documentPart.DocumentId);
-                        RenderPass(documentPageContext, new FreeCanvas(), documentPart.Content);
+                        RenderPass(documentPageContext, new FreeDocumentCanvas(), documentPart.Content);
                     }
                     }
                 
                 
                     documentPageContext.ProceedToNextRenderingPhase();
                     documentPageContext.ProceedToNextRenderingPhase();
@@ -211,7 +210,7 @@ namespace QuestPDF.Drawing
                         var pageContext = new PageContext();
                         var pageContext = new PageContext();
                         pageContext.SetDocumentId(documentPart.DocumentId);
                         pageContext.SetDocumentId(documentPart.DocumentId);
                     
                     
-                        RenderPass(pageContext, new FreeCanvas(), documentPart.Content);
+                        RenderPass(pageContext, new FreeDocumentCanvas(), documentPart.Content);
                         pageContext.ProceedToNextRenderingPhase();
                         pageContext.ProceedToNextRenderingPhase();
                         RenderPass(pageContext, canvas, documentPart.Content);
                         RenderPass(pageContext, canvas, documentPart.Content);
                     
                     
@@ -243,10 +242,9 @@ namespace QuestPDF.Drawing
             return content;
             return content;
         }
         }
 
 
-        private static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, ContainerElement content)
-            where TCanvas : ICanvas, IRenderingCanvas
+        private static void RenderPass(PageContext pageContext, IDocumentCanvas canvas, ContainerElement content)
         {
         {
-            content.InjectDependencies(pageContext, canvas);
+            content.InjectDependencies(pageContext, canvas.GetDrawingCanvas());
             content.VisitChildren(x => (x as IStateful)?.ResetState(hardReset: true));
             content.VisitChildren(x => (x as IStateful)?.ResetState(hardReset: true));
 
 
             while(true)
             while(true)
@@ -288,8 +286,8 @@ namespace QuestPDF.Drawing
             
             
             void ApplyLayoutDebugging()
             void ApplyLayoutDebugging()
             {
             {
-                content.VisitChildren(x => (x as SnapshotRecorder)?.Dispose());
-                content.RemoveExistingProxiesOfType<SnapshotRecorder>();
+                content.VisitChildren(x => (x as SnapshotCacheRecorderProxy)?.Dispose());
+                content.RemoveExistingProxiesOfType<SnapshotCacheRecorderProxy>();
 
 
                 content.ApplyLayoutOverflowDetection();
                 content.ApplyLayoutOverflowDetection();
                 content.Measure(Size.Max);
                 content.Measure(Size.Max);
@@ -299,7 +297,7 @@ namespace QuestPDF.Drawing
                 overflowState.TryToFixTheLayoutOverflowIssue();
                 overflowState.TryToFixTheLayoutOverflowIssue();
                 
                 
                 content.ApplyContentDirection();
                 content.ApplyContentDirection();
-                content.InjectDependencies(pageContext, canvas);
+                content.InjectDependencies(pageContext, canvas.GetDrawingCanvas());
 
 
                 content.VisitChildren(x => (x as LayoutProxy)?.CaptureLayoutErrorMeasurement());
                 content.VisitChildren(x => (x as LayoutProxy)?.CaptureLayoutErrorMeasurement());
                 content.RemoveExistingProxiesOfType<OverflowDebuggingProxy>();
                 content.RemoveExistingProxiesOfType<OverflowDebuggingProxy>();
@@ -368,7 +366,7 @@ namespace QuestPDF.Drawing
             }
             }
         }
         }
 
 
-        internal static void InjectDependencies(this Element content, IPageContext pageContext, ICanvas canvas)
+        internal static void InjectDependencies(this Element content, IPageContext pageContext, IDrawingCanvas canvas)
         {
         {
             content.VisitChildren(x =>
             content.VisitChildren(x =>
             {
             {
@@ -385,7 +383,7 @@ namespace QuestPDF.Drawing
             var canApplyCaching = Traverse(content);
             var canApplyCaching = Traverse(content);
             
             
             if (canApplyCaching)
             if (canApplyCaching)
-                content?.CreateProxy(x => new SnapshotRecorder(x));
+                content?.CreateProxy(x => new SnapshotCacheRecorderProxy(x));
 
 
             // returns true if can apply caching
             // returns true if can apply caching
             bool Traverse(Element? content)
             bool Traverse(Element? content)
@@ -438,7 +436,7 @@ namespace QuestPDF.Drawing
                     var canApplyCaching = canApplyCachingPerChild[childIndex];
                     var canApplyCaching = canApplyCachingPerChild[childIndex];
                     childIndex++;
                     childIndex++;
 
 
-                    return canApplyCaching ? new SnapshotRecorder(x) : x;
+                    return canApplyCaching ? new SnapshotCacheRecorderProxy(x) : x;
                 });
                 });
                 
                 
                 return false;
                 return false;

+ 38 - 0
Source/QuestPDF/Drawing/DocumentPageSnapshot.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using QuestPDF.Skia;
+
+namespace QuestPDF.Drawing;
+
+internal class DocumentPageSnapshot : IDisposable
+{
+    public List<LayerSnapshot> Layers { get; init; }
+    
+    ~DocumentPageSnapshot()
+    {
+        this.WarnThatFinalizerIsReached();
+        Dispose();
+    }
+    
+    public void Dispose()
+    {
+        foreach (var layer in Layers)
+            layer.Picture.Dispose();
+
+        Layers.Clear();
+        GC.SuppressFinalize(this);
+    }
+    
+    public class LayerSnapshot
+    {
+        public int ZIndex { get; init; }
+        public SkPicture Picture { get; init; }
+    }
+
+    public void DrawOnSkCanvas(SkCanvas canvas)
+    {
+        foreach (var layerSnapshot in Layers.OrderBy(x => x.ZIndex))
+            canvas.DrawPicture(layerSnapshot.Picture);
+    }
+}

+ 25 - 30
Source/QuestPDF/Drawing/FreeCanvas.cs → Source/QuestPDF/Drawing/DrawingCanvases/FreeDrawingCanvas.cs

@@ -2,54 +2,61 @@
 using QuestPDF.Skia;
 using QuestPDF.Skia;
 using QuestPDF.Skia.Text;
 using QuestPDF.Skia.Text;
 
 
-namespace QuestPDF.Drawing
+namespace QuestPDF.Drawing.DrawingCanvases
 {
 {
-    internal sealed class FreeCanvas : ICanvas, IRenderingCanvas
+    internal sealed class FreeDrawingCanvas : IDrawingCanvas
     {
     {
-        #region IRenderingCanvas
-
-        public bool DocumentContentHasLayoutOverflowIssues { get; set; }
-        
-        public void BeginDocument()
+        public DocumentPageSnapshot GetSnapshot()
         {
         {
-            
+            return new DocumentPageSnapshot();
         }
         }
 
 
-        public void EndDocument()
+        public void DrawSnapshot(DocumentPageSnapshot snapshot)
         {
         {
             
             
         }
         }
 
 
-        public void BeginPage(Size size)
+        public void Save()
         {
         {
             
             
         }
         }
 
 
-        public void EndPage()
+        public void Restore()
         {
         {
             
             
         }
         }
 
 
-        public void MarkCurrentPageAsHavingLayoutIssues()
+        public void SetZIndex(int index)
         {
         {
             
             
         }
         }
 
 
-        #endregion
-
-        #region ICanvas
+        public int GetZIndex()
+        {
+            return 0;
+        }
+        
+        public SkCanvasMatrix GetCurrentMatrix()
+        {
+            return default;
+        }
 
 
-        public void Save()
+        public void SetMatrix(SkCanvasMatrix matrix)
         {
         {
             
             
         }
         }
 
 
-        public void Restore()
+        public void Translate(Position vector)
         {
         {
             
             
         }
         }
         
         
-        public void Translate(Position vector)
+        public void Scale(float scaleX, float scaleY)
+        {
+            
+        }
+        
+        public void Rotate(float angle)
         {
         {
             
             
         }
         }
@@ -118,17 +125,5 @@ namespace QuestPDF.Drawing
         {
         {
             
             
         }
         }
-
-        public void Rotate(float angle)
-        {
-            
-        }
-
-        public void Scale(float scaleX, float scaleY)
-        {
-            
-        }
-
-        #endregion
     }
     }
 }
 }

+ 64 - 13
Source/QuestPDF/Drawing/ProxyCanvas.cs → Source/QuestPDF/Drawing/DrawingCanvases/ProxyDrawingCanvas.cs

@@ -1,12 +1,41 @@
+using System;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.Skia;
 using QuestPDF.Skia;
 using QuestPDF.Skia.Text;
 using QuestPDF.Skia.Text;
 
 
-namespace QuestPDF.Drawing;
+namespace QuestPDF.Drawing.DrawingCanvases;
 
 
-internal sealed class ProxyCanvas : ICanvas
+internal sealed class ProxyDrawingCanvas : IDrawingCanvas, IDisposable
 {
 {
-    public ICanvas Target { get; set; }
+    public IDrawingCanvas Target { get; set; }
+
+    #region IDisposable
+    
+    ~ProxyDrawingCanvas()
+    {
+        this.WarnThatFinalizerIsReached();
+        Dispose();
+    }
+    
+    public void Dispose()
+    {
+        (Target as IDisposable)?.Dispose();
+        GC.SuppressFinalize(this);
+    }
+    
+    #endregion
+    
+    #region IDrawingCanvas
+    
+    public DocumentPageSnapshot GetSnapshot()
+    {
+        return Target.GetSnapshot();
+    }
+
+    public void DrawSnapshot(DocumentPageSnapshot snapshot)
+    {
+        Target.DrawSnapshot(snapshot);
+    }
 
 
     public void Save()
     public void Save()
     {
     {
@@ -18,10 +47,40 @@ internal sealed class ProxyCanvas : ICanvas
         Target.Restore();
         Target.Restore();
     }
     }
 
 
+    public void SetZIndex(int index)
+    {
+        Target.SetZIndex(index);
+    }
+
+    public int GetZIndex()
+    {
+        return Target.GetZIndex();
+    }
+    
+    public SkCanvasMatrix GetCurrentMatrix()
+    {
+        return Target.GetCurrentMatrix();
+    }
+
+    public void SetMatrix(SkCanvasMatrix matrix)
+    {
+        Target.SetMatrix(matrix);
+    }
+    
     public void Translate(Position vector)
     public void Translate(Position vector)
     {
     {
         Target.Translate(vector);
         Target.Translate(vector);
     }
     }
+    
+    public void Scale(float scaleX, float scaleY)
+    {
+        Target.Scale(scaleX, scaleY);
+    }
+    
+    public void Rotate(float angle)
+    {
+        Target.Rotate(angle);
+    }
 
 
     public void DrawFilledRectangle(Position vector, Size size, Color color)
     public void DrawFilledRectangle(Position vector, Size size, Color color)
     {
     {
@@ -87,14 +146,6 @@ internal sealed class ProxyCanvas : ICanvas
     {
     {
         Target.DrawSection(sectionName);
         Target.DrawSection(sectionName);
     }
     }
-
-    public void Rotate(float angle)
-    {
-        Target.Rotate(angle);
-    }
-
-    public void Scale(float scaleX, float scaleY)
-    {
-        Target.Scale(scaleX, scaleY);
-    }
+    
+    #endregion
 }
 }

+ 221 - 0
Source/QuestPDF/Drawing/DrawingCanvases/SkiaDrawingCanvas.cs

@@ -0,0 +1,221 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+using QuestPDF.Skia.Text;
+
+namespace QuestPDF.Drawing.DrawingCanvases
+{
+    internal sealed class SkiaDrawingCanvas : IDrawingCanvas, IDisposable
+    {
+        public float Width { get; }
+        public float Height { get; }
+        
+        public SkiaDrawingCanvas(float width, float height)
+        {
+            Width = width;
+            Height = height;
+        }
+        
+        ~SkiaDrawingCanvas()
+        {
+            Dispose();
+        }
+        
+        public void Dispose()
+        {
+            CurrentCanvas?.Dispose();
+            CurrentCanvas = null;
+
+            foreach (var layer in ZIndexCanvases.Values)
+            {
+                layer.Canvas.Dispose();
+                layer.PictureRecorder.Dispose();
+            }
+            
+            ZIndexCanvases.Clear();
+            
+            GC.SuppressFinalize(this);
+        }
+        
+        #region ZIndex
+        
+        private SkCanvas CurrentCanvas { get; set; }
+        
+        private int CurrentZIndex { get; set; } = 0;
+        private IDictionary<int, (SkPictureRecorder PictureRecorder, SkCanvas Canvas)> ZIndexCanvases { get; } = new Dictionary<int, (SkPictureRecorder, SkCanvas)>();
+
+        private SkCanvas GetCanvasForZIndex(int zIndex)
+        {
+            if (ZIndexCanvases.TryGetValue(zIndex, out var value))
+                return value.Canvas;
+            
+            var pictureRecorder = new SkPictureRecorder();
+            var canvas = pictureRecorder.BeginRecording(Width, Height);
+            
+            ZIndexCanvases.Add(zIndex, (pictureRecorder, canvas));
+            return canvas;
+        }
+        
+        #endregion
+        
+        #region ICanvas
+
+        public DocumentPageSnapshot GetSnapshot()
+        { 
+            return new DocumentPageSnapshot
+            {
+                Layers = ZIndexCanvases
+                    .Select(zindex =>
+                    {
+                        using var pictureRecorder = zindex.Value.PictureRecorder;
+                        var picture = pictureRecorder.EndRecording();
+                        
+                        zindex.Value.Canvas.Dispose();
+                        
+                        return new DocumentPageSnapshot.LayerSnapshot
+                        {
+                            ZIndex = zindex.Key,
+                            Picture = picture
+                        };
+                    })
+                    .ToList()
+            };
+        }
+
+        public void DrawSnapshot(DocumentPageSnapshot snapshot)
+        {
+            foreach (var snapshotLayer in snapshot.Layers.OrderBy(x => x.ZIndex))
+            {
+                var canvas = GetCanvasForZIndex(snapshotLayer.ZIndex);
+
+                canvas.Save();
+                canvas.SetCurrentMatrix(SkCanvasMatrix.Identity);
+                canvas.DrawPicture(snapshotLayer.Picture);
+                canvas.Restore();
+            }
+        }
+
+        public void Save()
+        {
+            CurrentCanvas.Save();
+        }
+
+        public void Restore()
+        {
+            CurrentCanvas.Restore();
+        }
+        
+        public void SetZIndex(int index)
+        {
+            CurrentZIndex = index;
+            CurrentCanvas = GetCanvasForZIndex(CurrentZIndex);
+        }
+
+        public int GetZIndex()
+        {
+            return CurrentZIndex;
+        }
+
+        public SkCanvasMatrix GetCurrentMatrix()
+        {
+            return CurrentCanvas.GetCurrentMatrix();
+        }
+
+        public void SetMatrix(SkCanvasMatrix matrix)
+        {
+            CurrentCanvas.SetCurrentMatrix(matrix);
+        }
+        
+        public void Translate(Position vector)
+        {
+            CurrentCanvas.Translate(vector.X, vector.Y);
+        }
+        
+        public void Scale(float scaleX, float scaleY)
+        {
+            CurrentCanvas.Scale(scaleX, scaleY);
+        }
+        
+        public void Rotate(float angle)
+        {
+            CurrentCanvas.Rotate(angle);
+        }
+
+        public void DrawFilledRectangle(Position vector, Size size, Color color)
+        {
+            if (size.Width < Size.Epsilon || size.Height < Size.Epsilon)
+                return;
+
+            var position = new SkRect(vector.X, vector.Y, vector.X + size.Width, vector.Y + size.Height);
+            CurrentCanvas.DrawFilledRectangle(position, color);
+        }
+        
+        public void DrawStrokeRectangle(Position vector, Size size, float strokeWidth, Color color)
+        {
+            if (size.Width < Size.Epsilon || size.Height < Size.Epsilon)
+                return;
+
+            var position = new SkRect(vector.X, vector.Y, vector.X + size.Width, vector.Y + size.Height);
+            CurrentCanvas.DrawStrokeRectangle(position, strokeWidth, color);
+        }
+
+        public void DrawParagraph(SkParagraph paragraph, int lineFrom, int lineTo)
+        {
+            CurrentCanvas.DrawParagraph(paragraph, lineFrom, lineTo);
+        }
+
+        public void DrawImage(SkImage image, Size size)
+        {
+            CurrentCanvas.DrawImage(image, size.Width, size.Height);
+        }
+
+        public void DrawPicture(SkPicture picture)
+        {
+            CurrentCanvas.DrawPicture(picture);
+        }
+
+        public void DrawSvgPath(string path, Color color)
+        {
+            CurrentCanvas.DrawSvgPath(path, color);
+        }
+
+        public void DrawSvg(SkSvgImage svgImage, Size size)
+        {
+            CurrentCanvas.DrawSvg(svgImage, size.Width, size.Height);
+        }
+        
+        public void DrawOverflowArea(SkRect area)
+        {
+            CurrentCanvas.DrawOverflowArea(area);
+        }
+    
+        public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace)
+        {
+            CurrentCanvas.ClipOverflowArea(availableSpace, requiredSpace);
+        }
+    
+        public void ClipRectangle(SkRect clipArea)
+        {
+            CurrentCanvas.ClipRectangle(clipArea);
+        }
+        
+        public void DrawHyperlink(string url, Size size)
+        {
+            CurrentCanvas.AnnotateUrl(size.Width, size.Height, url);
+        }
+        
+        public void DrawSectionLink(string sectionName, Size size)
+        {
+            CurrentCanvas.AnnotateDestinationLink(size.Width, size.Height, sectionName);
+        }
+
+        public void DrawSection(string sectionName)
+        {
+            CurrentCanvas.AnnotateDestination(sectionName);
+        }
+        
+        #endregion
+    }
+}

+ 0 - 58
Source/QuestPDF/Drawing/PdfCanvas.cs

@@ -1,58 +0,0 @@
-using System;
-using System.IO;
-using QuestPDF.Drawing.Exceptions;
-using QuestPDF.Helpers;
-using QuestPDF.Infrastructure;
-using QuestPDF.Skia;
-
-namespace QuestPDF.Drawing
-{
-    internal sealed class PdfCanvas : SkiaDocumentCanvasBase
-    {
-        public PdfCanvas(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings) 
-            : base(CreatePdf(stream, documentMetadata, 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);
-            using var language = new SkText(documentMetadata.Language);
-            
-            var internalMetadata = new SkPdfDocumentMetadata
-            {
-                Title = title,
-                Author = author,
-                Subject = subject,
-                Keywords = keywords,
-                Creator = creator,
-                Producer = producer,
-                Language = language,
-                
-                CreationDate = new SkDateTime(documentMetadata.CreationDate),
-                ModificationDate = new SkDateTime(documentMetadata.ModifiedDate),
-                
-                RasterDPI = documentSettings.ImageRasterDpi,
-                SupportPDFA = documentSettings.PdfA,
-                CompressDocument = documentSettings.CompressDocument
-            };
-            
-            try
-            {
-                return SkPdfDocument.Create(stream, internalMetadata);
-            }
-            catch (TypeInitializationException exception)
-            {
-                throw new InitializationException("PDF", exception);
-            }
-        }
-    }
-}

+ 1 - 4
Source/QuestPDF/Drawing/Proxy/LayoutOverflowVisualization.cs

@@ -47,10 +47,7 @@ internal sealed class LayoutOverflowVisualization : ElementProxy, IContentDirect
         }
         }
 
 
         Canvas = Child.Canvas;
         Canvas = Child.Canvas;
-        
-        if (Canvas is SkiaCanvasBase skiaCanvasBase)
-            skiaCanvasBase.MarkCurrentPageAsHavingLayoutIssues();
-        
+
         // check overflow area
         // check overflow area
         var contentArea = Child.TryMeasureWithOverflow(availableSpace);
         var contentArea = Child.TryMeasureWithOverflow(availableSpace);
 
 

+ 9 - 10
Source/QuestPDF/Drawing/Proxy/LayoutProxy.cs

@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using QuestPDF.Companion;
 using QuestPDF.Companion;
+using QuestPDF.Drawing.DrawingCanvases;
 using QuestPDF.Elements;
 using QuestPDF.Elements;
 using QuestPDF.Elements.Text;
 using QuestPDF.Elements.Text;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
@@ -23,21 +24,19 @@ internal sealed class LayoutProxy : ElementProxy
         var size = ProvideIntrinsicSize() ? Child.Measure(availableSpace) : availableSpace;
         var size = ProvideIntrinsicSize() ? Child.Measure(availableSpace) : availableSpace;
         
         
         base.Draw(availableSpace);
         base.Draw(availableSpace);
-        
-        var canvas = Canvas as SkiaCanvasBase;
-        
-        if (canvas == null)
+
+        if (Canvas is FreeDrawingCanvas)
             return;
             return;
         
         
-        var position = canvas.Canvas.GetCurrentTotalMatrix();
-
+        var matrix = Canvas.GetCurrentMatrix();
+        
         Snapshots.Add(new CompanionCommands.UpdateDocumentStructure.PageLocation
         Snapshots.Add(new CompanionCommands.UpdateDocumentStructure.PageLocation
         {
         {
             PageNumber = PageContext.CurrentPage,
             PageNumber = PageContext.CurrentPage,
-            Left = position.TranslateX,
-            Top = position.TranslateY,
-            Right = position.TranslateX + size.Width,
-            Bottom = position.TranslateY + size.Height
+            Left = matrix.TranslateX,
+            Top = matrix.TranslateY,
+            Right = matrix.TranslateX + size.Width,
+            Bottom = matrix.TranslateY + size.Height
         });
         });
 
 
         bool ProvideIntrinsicSize()
         bool ProvideIntrinsicSize()

+ 21 - 20
Source/QuestPDF/Drawing/Proxy/SnapshotRecorder.cs → Source/QuestPDF/Drawing/Proxy/SnapshotCacheRecorderProxy.cs

@@ -1,18 +1,19 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using QuestPDF.Drawing.DrawingCanvases;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.Skia;
 using QuestPDF.Skia;
 
 
 namespace QuestPDF.Drawing.Proxy;
 namespace QuestPDF.Drawing.Proxy;
 
 
-internal sealed class SnapshotRecorder : ElementProxy, IDisposable
+internal sealed class SnapshotCacheRecorderProxy : ElementProxy, IDisposable
 {
 {
-    SnapshotRecorderCanvas RecorderCanvas { get; } = new();
-    Dictionary<(int pageNumber, float availableWidth, float availableHeight), SpacePlan> MeasureCache { get; } = new();
-    Dictionary<int, SkPicture> DrawCache { get; } = new();
+    private ProxyDrawingCanvas RecorderCanvas { get; } = new();
+    private Dictionary<(int pageNumber, float availableWidth, float availableHeight), SpacePlan> MeasureCache { get; } = new();
+    private Dictionary<int, DocumentPageSnapshot> DrawCache { get; } = new();
 
 
-    ~SnapshotRecorder()
+    ~SnapshotCacheRecorderProxy()
     {
     {
         this.WarnThatFinalizerIsReached();
         this.WarnThatFinalizerIsReached();
         Dispose();
         Dispose();
@@ -20,20 +21,22 @@ internal sealed class SnapshotRecorder : ElementProxy, IDisposable
 
 
     public void Dispose()
     public void Dispose()
     {
     {
+        RecorderCanvas?.Dispose();
+        
         foreach (var cacheValue in DrawCache.Values)
         foreach (var cacheValue in DrawCache.Values)
             cacheValue.Dispose();
             cacheValue.Dispose();
         
         
         GC.SuppressFinalize(this);
         GC.SuppressFinalize(this);
     }
     }
     
     
-    public SnapshotRecorder(Element child)
+    public SnapshotCacheRecorderProxy(Element child)
     {
     {
         Child = child;
         Child = child;
     }
     }
     
     
     private void Initialize()
     private void Initialize()
     {
     {
-        if (Child.Canvas is SnapshotRecorderCanvas)
+        if (Child.Canvas == RecorderCanvas)
             return;
             return;
         
         
         Child.VisitChildren(x => x.Canvas = RecorderCanvas);
         Child.VisitChildren(x => x.Canvas = RecorderCanvas);
@@ -47,8 +50,11 @@ internal sealed class SnapshotRecorder : ElementProxy, IDisposable
         
         
         if (MeasureCache.TryGetValue(cacheItem, out var measurement))
         if (MeasureCache.TryGetValue(cacheItem, out var measurement))
             return measurement;
             return measurement;
-        
+
+        RecorderCanvas.Target = new FreeDrawingCanvas();
         var result = base.Measure(availableSpace);
         var result = base.Measure(availableSpace);
+        RecorderCanvas.Target = null;
+        
         MeasureCache[cacheItem] = result;
         MeasureCache[cacheItem] = result;
         return result;
         return result;
     }
     }
@@ -57,28 +63,23 @@ internal sealed class SnapshotRecorder : ElementProxy, IDisposable
     {
     {
         // element may overflow the available space
         // element may overflow the available space
         // capture as much as possible around the origin point
         // capture as much as possible around the origin point
-        var cachePictureSize = Size.Max;
-        var cachePictureOffset = new Position(cachePictureSize.Width / 2, cachePictureSize.Height / 2);
         
         
         if (DrawCache.TryGetValue(PageContext.CurrentPage, out var snapshot))
         if (DrawCache.TryGetValue(PageContext.CurrentPage, out var snapshot))
         {
         {
-            Canvas.Translate(cachePictureOffset.Reverse());
-            Canvas.DrawPicture(snapshot);
-            Canvas.Translate(cachePictureOffset);
+            Canvas.DrawSnapshot(snapshot);
             
             
             snapshot.Dispose();
             snapshot.Dispose();
             DrawCache.Remove(PageContext.CurrentPage);
             DrawCache.Remove(PageContext.CurrentPage);
             return;
             return;
         }
         }
         
         
-        using var pictureRecorder = new SkPictureRecorder();
-        using var canvas = pictureRecorder.BeginRecording(Size.Max.Width, Size.Max.Height);
-        RecorderCanvas.Canvas = canvas;
-
-        RecorderCanvas.Translate(cachePictureOffset);
+        using var skiaCanvas = new SkiaDrawingCanvas(Size.Max.Width, Size.Max.Height);
+        RecorderCanvas.Target = skiaCanvas;
+        RecorderCanvas.SetZIndex(0);
+        
         base.Draw(availableSpace);
         base.Draw(availableSpace);
         
         
-        DrawCache[PageContext.CurrentPage] = pictureRecorder.EndRecording();
-        RecorderCanvas.Canvas = null;
+        DrawCache[PageContext.CurrentPage] = skiaCanvas.GetSnapshot();
+        RecorderCanvas.Target = null;
     }
     }
 }
 }

+ 0 - 156
Source/QuestPDF/Drawing/SkiaCanvasBase.cs

@@ -1,156 +0,0 @@
-using QuestPDF.Helpers;
-using QuestPDF.Infrastructure;
-using QuestPDF.Skia;
-using QuestPDF.Skia.Text;
-
-namespace QuestPDF.Drawing
-{
-    internal abstract class SkiaCanvasBase : ICanvas, IRenderingCanvas
-    {
-        internal SkCanvas Canvas { get; set; }
-
-        #region IRenderingCanvas
-        
-        public bool DocumentContentHasLayoutOverflowIssues { get; set; }
-        
-        private Size CurrentPageSize { get; set; } = Size.Zero;
-        private bool CurrentPageHasLayoutIssues { get; set; }
-        
-        public abstract void BeginDocument();
-        public abstract void EndDocument();
-
-        public virtual void BeginPage(Size size)
-        {
-            CurrentPageSize = size;
-            CurrentPageHasLayoutIssues = false;
-        }
-
-        public virtual void EndPage()
-        {
-            if (CurrentPageHasLayoutIssues)
-                DrawLayoutIssuesIndicatorOnCurrentPage();
-        }
-
-        public void MarkCurrentPageAsHavingLayoutIssues()
-        {
-            CurrentPageHasLayoutIssues = true;
-            DocumentContentHasLayoutOverflowIssues = true;
-        }
-        
-        private void DrawLayoutIssuesIndicatorOnCurrentPage()
-        {
-            // visual configuration
-            var lineColor = Colors.Red.Medium;
-            const byte lineOpacity = 64;
-        
-            // implementation
-            var indicatorColor = lineColor.WithAlpha(lineOpacity);
-            var position = new SkRect(0, 0, CurrentPageSize.Width, CurrentPageSize.Height);
-            Canvas.DrawFilledRectangle(position, indicatorColor);
-        }
-        
-        #endregion
-        
-        #region ICanvas
-        
-        public void Save()
-        {
-            Canvas.Save();
-        }
-
-        public void Restore()
-        {
-            Canvas.Restore();
-        }
-        
-        public void Translate(Position vector)
-        {
-            Canvas.Translate(vector.X, vector.Y);
-        }
-
-        public void DrawFilledRectangle(Position vector, Size size, Color color)
-        {
-            if (size.Width < Size.Epsilon || size.Height < Size.Epsilon)
-                return;
-
-            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;
-
-            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, int lineFrom, int lineTo)
-        {
-            Canvas.DrawParagraph(paragraph, lineFrom, lineTo);
-        }
-
-        public void DrawImage(SkImage image, Size size)
-        {
-            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)
-        {
-            Canvas.AnnotateUrl(size.Width, size.Height, url);
-        }
-        
-        public void DrawSectionLink(string sectionName, Size size)
-        {
-            Canvas.AnnotateDestinationLink(size.Width, size.Height, sectionName);
-        }
-
-        public void DrawSection(string sectionName)
-        {
-            Canvas.AnnotateDestination(sectionName);
-        }
-
-        public void Rotate(float angle)
-        {
-            Canvas.Rotate(angle);
-        }
-
-        public void Scale(float scaleX, float scaleY)
-        {
-            Canvas.Scale(scaleX, scaleY);
-        }
-        
-        #endregion
-    }
-}

+ 0 - 57
Source/QuestPDF/Drawing/SkiaDocumentCanvasBase.cs

@@ -1,57 +0,0 @@
-using System;
-using QuestPDF.Infrastructure;
-using QuestPDF.Skia;
-
-namespace QuestPDF.Drawing
-{
-    internal class SkiaDocumentCanvasBase : SkiaCanvasBase, IDisposable
-    {
-        private SkDocument? Document { get; }
-
-        protected SkiaDocumentCanvasBase(SkDocument document)
-        {
-            Document = document;
-        }
-
-        ~SkiaDocumentCanvasBase()
-        {
-            this.WarnThatFinalizerIsReached();
-            Dispose();
-        }
-
-        public void Dispose()
-        {
-            Canvas?.Dispose();
-            Document?.Dispose();
-            GC.SuppressFinalize(this);
-        }
-        
-        public override void BeginDocument()
-        {
-            
-        }
-
-        public override void EndDocument()
-        {
-            Canvas?.Dispose();
-            
-            Document.Close();
-            Document.Dispose();
-        }
-
-        public override void BeginPage(Size size)
-        {
-            Canvas = Document.BeginPage(size.Width, size.Height);
-            
-            base.BeginPage(size);
-        }
-
-        public override void EndPage()
-        {
-            base.EndPage();
-            
-            Document.EndPage();
-            Canvas.Dispose();
-        }
-    }
-}

+ 0 - 14
Source/QuestPDF/Drawing/SnapshotRecorderCanvas.cs

@@ -1,14 +0,0 @@
-namespace QuestPDF.Drawing;
-
-internal sealed class SnapshotRecorderCanvas : SkiaCanvasBase
-{
-    public override void BeginDocument()
-    {
-        
-    }
-
-    public override void EndDocument()
-    {
-        
-    }
-}

+ 0 - 57
Source/QuestPDF/Drawing/SvgCanvas.cs

@@ -1,57 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using QuestPDF.Infrastructure;
-using QuestPDF.Skia;
-
-namespace QuestPDF.Drawing
-{
-    internal sealed class SvgCanvas : SkiaCanvasBase, IDisposable
-    {
-        internal SkWriteStream WriteStream { get; set; }
-        internal ICollection<string> Images { get; } = new List<string>();
-        
-        ~SvgCanvas()
-        {
-            this.WarnThatFinalizerIsReached();
-            Dispose();
-        }
-
-        public void Dispose()
-        {
-            Canvas?.Dispose();
-            WriteStream?.Dispose();
-            GC.SuppressFinalize(this);
-        }
-        
-        public override void BeginDocument()
-        {
-            
-        }
-
-        public override void EndDocument()
-        {
-            Canvas?.Dispose();
-            WriteStream?.Dispose();
-        }
-
-        public override void BeginPage(Size size)
-        {
-            WriteStream?.Dispose();
-            WriteStream = new SkWriteStream();
-            Canvas = SkSvgCanvas.CreateSvg(size.Width, size.Height, WriteStream);
-        }
-
-        public override void EndPage()
-        {
-            Canvas.Save();
-            Canvas.Dispose();
-            
-            using var data = WriteStream.DetachData();
-            var svgImage = Encoding.UTF8.GetString(data.ToBytes());
-            Images.Add(svgImage);
-            
-            WriteStream.Dispose();
-        }
-    }
-}

+ 0 - 28
Source/QuestPDF/Drawing/XpsCanvas.cs

@@ -1,28 +0,0 @@
-using System;
-using System.IO;
-using QuestPDF.Drawing.Exceptions;
-using QuestPDF.Infrastructure;
-using QuestPDF.Skia;
-
-namespace QuestPDF.Drawing
-{
-    internal sealed class XpsCanvas : SkiaDocumentCanvasBase
-    {
-        public XpsCanvas(SkWriteStream stream, DocumentSettings documentSettings) : base(CreateXps(stream, documentSettings))
-        {
-            
-        }
-        
-        private static SkDocument CreateXps(SkWriteStream stream, DocumentSettings documentSettings)
-        {
-            try
-            {
-                return SkXpsDocument.Create(stream, documentSettings.ImageRasterDpi);
-            }
-            catch (TypeInitializationException exception)
-            {
-                throw new InitializationException("XPS", exception);
-            }
-        }
-    }
-}

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

@@ -114,7 +114,7 @@ namespace QuestPDF.Elements
     public sealed class DynamicContext
     public sealed class DynamicContext
     {
     {
         internal IPageContext PageContext { get; set; }
         internal IPageContext PageContext { get; set; }
-        internal ICanvas Canvas { get; set; }
+        internal IDrawingCanvas Canvas { get; set; }
 
 
         internal TextStyle TextStyle { get; set; }
         internal TextStyle TextStyle { get; set; }
         internal ContentDirection ContentDirection { get; set; }
         internal ContentDirection ContentDirection { get; set; }

+ 17 - 3
Source/QuestPDF/Elements/MultiColumn.cs

@@ -3,10 +3,12 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
+using QuestPDF.Drawing.DrawingCanvases;
 using QuestPDF.Drawing.Proxy;
 using QuestPDF.Drawing.Proxy;
 using QuestPDF.Elements.Text;
 using QuestPDF.Elements.Text;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Elements;
 namespace QuestPDF.Elements;
 
 
@@ -42,7 +44,7 @@ internal sealed class MultiColumnChildDrawingObserver : ElementProxy
     }
     }
 }
 }
 
 
-internal sealed class MultiColumn : Element, IContentDirectionAware
+internal sealed class MultiColumn : Element, IContentDirectionAware, IDisposable
 {
 {
     // items
     // items
     internal Element Content { get; set; } = Empty.Instance;
     internal Element Content { get; set; } = Empty.Instance;
@@ -56,9 +58,21 @@ internal sealed class MultiColumn : Element, IContentDirectionAware
     public ContentDirection ContentDirection { get; set; }
     public ContentDirection ContentDirection { get; set; }
 
 
     // cache
     // cache
-    private ProxyCanvas ChildrenCanvas { get; } = new();
+    private ProxyDrawingCanvas ChildrenCanvas { get; } = new();
     private TreeNode<MultiColumnChildDrawingObserver>[] State { get; set; }
     private TreeNode<MultiColumnChildDrawingObserver>[] State { get; set; }
 
 
+    ~MultiColumn()
+    {
+        this.WarnThatFinalizerIsReached();
+        Dispose();
+    }
+    
+    public void Dispose()
+    {
+        ChildrenCanvas?.Dispose();
+        GC.SuppressFinalize(this);
+    }
+    
     internal override void CreateProxy(Func<Element?, Element?> create)
     internal override void CreateProxy(Func<Element?, Element?> create)
     {
     {
         Content = create(Content);
         Content = create(Content);
@@ -92,7 +106,7 @@ internal sealed class MultiColumn : Element, IContentDirectionAware
         if (Content.Canvas != ChildrenCanvas)
         if (Content.Canvas != ChildrenCanvas)
             Content.InjectDependencies(PageContext, ChildrenCanvas);
             Content.InjectDependencies(PageContext, ChildrenCanvas);
         
         
-        ChildrenCanvas.Target = new FreeCanvas();
+        ChildrenCanvas.Target = new FreeDrawingCanvas();
         
         
         return FindPerfectSpace();
         return FindPerfectSpace();
 
 

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

@@ -32,10 +32,11 @@ namespace QuestPDF.Elements
             container
             container
                 .DebugPointer(DebugPointerType.DocumentStructure, DocumentStructureTypes.Page.ToString())
                 .DebugPointer(DebugPointerType.DocumentStructure, DocumentStructureTypes.Page.ToString())
                 .ContentDirection(ContentDirection)
                 .ContentDirection(ContentDirection)
-                .Background(BackgroundColor)
                 .DefaultTextStyle(DefaultTextStyle)
                 .DefaultTextStyle(DefaultTextStyle)
                 .Layers(layers =>
                 .Layers(layers =>
                 {
                 {
+                    layers.Layer().ZIndex(int.MinValue).Background(BackgroundColor);
+                    
                     layers
                     layers
                         .Layer()
                         .Layer()
                         .Repeat()
                         .Repeat()

+ 1 - 1
Source/QuestPDF/Elements/Text/Items/TextBlockElement.cs

@@ -11,7 +11,7 @@ namespace QuestPDF.Elements.Text.Items
         public TextInjectedElementAlignment Alignment { get; set; } = TextInjectedElementAlignment.AboveBaseline;
         public TextInjectedElementAlignment Alignment { get; set; } = TextInjectedElementAlignment.AboveBaseline;
         public int ParagraphBlockIndex { get; set; }
         public int ParagraphBlockIndex { get; set; }
 
 
-        public void ConfigureElement(IPageContext pageContext, ICanvas canvas)
+        public void ConfigureElement(IPageContext pageContext, IDrawingCanvas canvas)
         {
         {
             Element.VisitChildren(x => (x as IStateful)?.ResetState());
             Element.VisitChildren(x => (x as IStateful)?.ResetState());
             Element.InjectDependencies(pageContext, canvas);
             Element.InjectDependencies(pageContext, canvas);

+ 23 - 0
Source/QuestPDF/Elements/ZIndex.cs

@@ -0,0 +1,23 @@
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements
+{
+    internal sealed class ZIndex : ContainerElement
+    {
+        public int Depth { get; set; }
+        
+        internal override void Draw(Size availableSpace)
+        {
+            var previousMatrix = Canvas.GetCurrentMatrix();
+            var previousZIndex = Canvas.GetZIndex();
+            Canvas.SetZIndex(Depth);
+            Canvas.SetMatrix(previousMatrix);
+            
+            base.Draw(availableSpace);
+            
+            var newMatrix = Canvas.GetCurrentMatrix();
+            Canvas.SetZIndex(previousZIndex);
+            Canvas.SetMatrix(newMatrix);
+        }
+    }
+}

+ 15 - 0
Source/QuestPDF/Fluent/ElementExtensions.cs

@@ -485,6 +485,21 @@ namespace QuestPDF.Fluent
             });
             });
         }
         }
         
         
+        /// <summary>
+        /// By default, the library draws content in the order it is defined, which may not always be the desired behavior.
+        /// This element allows you to alter the rendering order, ensuring that the content is displayed in the correct sequence.
+        /// The default z-index is 0, unless a different value is inherited from a parent container.
+        /// <a href="https://www.questpdf.com/api-reference/z-index.html">Learn more</a>
+        /// </summary>
+        /// <param name="indexValue">The z-index value. Higher values are rendered above lower values.</param>
+        public static IContainer ZIndex(this IContainer element, int indexValue)
+        {
+            return element.Element(new ZIndex
+            {
+                Depth = indexValue
+            });
+        }
+        
         #region Canvas [Obsolete]
         #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: https://www.questpdf.com/api-reference/skiasharp-integration.html";
         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: https://www.questpdf.com/api-reference/skiasharp-integration.html";

+ 1 - 1
Source/QuestPDF/Infrastructure/Element.cs

@@ -9,7 +9,7 @@ namespace QuestPDF.Infrastructure
     internal abstract class Element : IElement
     internal abstract class Element : IElement
     {
     {
         internal IPageContext PageContext { get; set; }
         internal IPageContext PageContext { get; set; }
-        internal ICanvas Canvas { get; set; }
+        internal IDrawingCanvas Canvas { get; set; }
         internal SourceCodePath? CodeLocation { get; set; }
         internal SourceCodePath? CodeLocation { get; set; }
         
         
         internal virtual IEnumerable<Element?> GetChildren()
         internal virtual IEnumerable<Element?> GetChildren()

+ 2 - 4
Source/QuestPDF/Infrastructure/IRenderingCanvas.cs → Source/QuestPDF/Infrastructure/IDocumentCanvas.cs

@@ -1,14 +1,12 @@
 namespace QuestPDF.Infrastructure
 namespace QuestPDF.Infrastructure
 {
 {
-    internal interface IRenderingCanvas
+    internal interface IDocumentCanvas
     {
     {
-        bool DocumentContentHasLayoutOverflowIssues { get; set; }
-        void MarkCurrentPageAsHavingLayoutIssues();
-        
         void BeginDocument();
         void BeginDocument();
         void EndDocument();
         void EndDocument();
         
         
         void BeginPage(Size size);
         void BeginPage(Size size);
         void EndPage();
         void EndPage();
+        IDrawingCanvas GetDrawingCanvas();
     }
     }
 }
 }

+ 13 - 4
Source/QuestPDF/Infrastructure/ICanvas.cs → Source/QuestPDF/Infrastructure/IDrawingCanvas.cs

@@ -1,14 +1,26 @@
+using QuestPDF.Drawing;
 using QuestPDF.Skia;
 using QuestPDF.Skia;
 using QuestPDF.Skia.Text;
 using QuestPDF.Skia.Text;
 
 
 namespace QuestPDF.Infrastructure
 namespace QuestPDF.Infrastructure
 {
 {
-    internal interface ICanvas
+    internal interface IDrawingCanvas
     {
     {
+        DocumentPageSnapshot GetSnapshot();
+        void DrawSnapshot(DocumentPageSnapshot snapshot);
+        
         void Save();
         void Save();
         void Restore();
         void Restore();
+
+        void SetZIndex(int index);
+        int GetZIndex();
+        
+        SkCanvasMatrix GetCurrentMatrix();
+        void SetMatrix(SkCanvasMatrix matrix);
         
         
         void Translate(Position vector);
         void Translate(Position vector);
+        void Scale(float scaleX, float scaleY);
+        void Rotate(float angle);
         
         
         void DrawFilledRectangle(Position vector, Size size, Color color);
         void DrawFilledRectangle(Position vector, Size size, Color color);
         void DrawStrokeRectangle(Position vector, Size size, float strokeWidth, Color color);
         void DrawStrokeRectangle(Position vector, Size size, float strokeWidth, Color color);
@@ -25,8 +37,5 @@ namespace QuestPDF.Infrastructure
         void DrawHyperlink(string url, Size size);
         void DrawHyperlink(string url, Size size);
         void DrawSectionLink(string sectionName, Size size);
         void DrawSectionLink(string sectionName, Size size);
         void DrawSection(string sectionName);
         void DrawSection(string sectionName);
-        
-        void Rotate(float angle);
-        void Scale(float scaleX, float scaleY);
     }
     }
 }
 }

BIN
Source/QuestPDF/Runtimes/osx-arm64/native/libQuestPdfSkia.dylib


+ 43 - 12
Source/QuestPDF/Skia/SkCanvas.cs

@@ -4,6 +4,37 @@ using QuestPDF.Skia.Text;
 
 
 namespace QuestPDF.Skia;
 namespace QuestPDF.Skia;
 
 
+[StructLayout(LayoutKind.Sequential)]
+internal struct SkCanvasMatrix
+{
+    public float ScaleX;
+    public float SkewX;
+    public float TranslateX;
+    
+    public float SkewY;
+    public float ScaleY;
+    public float TranslateY;
+
+    public float Perspective1;
+    public float Perspective2;
+    public float Perspective3;
+
+    public static SkCanvasMatrix Identity => new SkCanvasMatrix
+    {
+        ScaleX = 1,
+        SkewX = 0,
+        TranslateX = 0,
+        
+        SkewY = 0,
+        ScaleY = 1,
+        TranslateY = 0,
+        
+        Perspective1 = 0,
+        Perspective2 = 0,
+        Perspective3 = 1
+    };
+}
+
 internal sealed class SkCanvas : IDisposable
 internal sealed class SkCanvas : IDisposable
 {
 {
     public IntPtr Instance { get; private set; }
     public IntPtr Instance { get; private set; }
@@ -116,9 +147,15 @@ internal sealed class SkCanvas : IDisposable
         API.canvas_annotate_destination_link(Instance, width, height, destinationName);
         API.canvas_annotate_destination_link(Instance, width, height, destinationName);
     }
     }
     
     
-    public CanvasMatrix GetCurrentTotalMatrix()
+    public SkCanvasMatrix GetCurrentMatrix()
     {
     {
-        return API.canvas_get_matrix(Instance);
+        API.canvas_get_matrix9(Instance, out var result);
+        return result;
+    }
+    
+    public void SetCurrentMatrix(SkCanvasMatrix matrix)
+    {
+        API.canvas_set_matrix9(Instance, matrix);
     }
     }
     
     
     ~SkCanvas()
     ~SkCanvas()
@@ -139,15 +176,6 @@ internal sealed class SkCanvas : IDisposable
         GC.SuppressFinalize(this);
         GC.SuppressFinalize(this);
     }
     }
     
     
-    public struct CanvasMatrix
-    {
-        public float ScaleX;
-        public float TranslateX;
-
-        public float ScaleY;
-        public float TranslateY;
-    }
-
     private static class API
     private static class API
     {
     {
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
@@ -211,6 +239,9 @@ internal sealed class SkCanvas : IDisposable
         public static extern void canvas_annotate_destination_link(IntPtr canvas, float width, float height, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName);
         public static extern void canvas_annotate_destination_link(IntPtr canvas, float width, float height, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName);
         
         
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
-        public static extern CanvasMatrix canvas_get_matrix(IntPtr canvas);
+        public static extern void canvas_get_matrix9(IntPtr canvas, out SkCanvasMatrix matrix);
+        
+        [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void canvas_set_matrix9(IntPtr canvas, SkCanvasMatrix matrix);
     }
     }
 }
 }

+ 1 - 1
Source/QuestPDF/Skia/SkNativeDependencyCompatibilityChecker.cs

@@ -7,7 +7,7 @@ namespace QuestPDF.Skia;
 
 
 internal static class SkNativeDependencyCompatibilityChecker
 internal static class SkNativeDependencyCompatibilityChecker
 {
 {
-    private const int ExpectedNativeLibraryVersion = 1;
+    private const int ExpectedNativeLibraryVersion = 2;
     
     
     private static NativeDependencyCompatibilityChecker Instance { get; } = new()
     private static NativeDependencyCompatibilityChecker Instance { get; } = new()
     {
     {

+ 2 - 2
Source/QuestPDF/Skia/Text/SkParagraphBuilder.cs

@@ -109,7 +109,7 @@ internal sealed class SkParagraphBuilder : IDisposable
             LineClampEllipsis = clampLinesEllipsis.Instance
             LineClampEllipsis = clampLinesEllipsis.Instance
         };
         };
         
         
-        var instance = API.paragraph_builder_create(paragraphStyleConfiguration, fontCollection.Instance);
+        var instance = API.paragraph_builder_create(paragraphStyleConfiguration, SkUnicode.Global.Instance, fontCollection.Instance);
         SkiaAPI.EnsureNotNull(instance);
         SkiaAPI.EnsureNotNull(instance);
         
         
         return new SkParagraphBuilder
         return new SkParagraphBuilder
@@ -162,7 +162,7 @@ internal sealed class SkParagraphBuilder : IDisposable
     private static class API
     private static class API
     {
     {
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
-        public static extern IntPtr paragraph_builder_create(ParagraphStyleConfiguration paragraphStyleConfiguration, IntPtr fontCollection);
+        public static extern IntPtr paragraph_builder_create(ParagraphStyleConfiguration paragraphStyleConfiguration, IntPtr unicode, IntPtr fontCollection);
         
         
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         public static extern void paragraph_builder_add_text(IntPtr paragraphBuilder, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string text, IntPtr textStyle);
         public static extern void paragraph_builder_add_text(IntPtr paragraphBuilder, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string text, IntPtr textStyle);

+ 21 - 0
Source/QuestPDF/Skia/Text/SkUnicode.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace QuestPDF.Skia.Text;
+
+internal sealed class SkUnicode
+{
+    public IntPtr Instance { get; private set; }
+    public static SkUnicode Global { get; } = new();
+
+    private SkUnicode()
+    {
+        Instance = API.unicode_create();
+    }
+    
+    private static class API
+    {
+        [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern IntPtr unicode_create();
+    }
+}