Browse Source

Initial Skia-custom integration

Marcin Ziąbek 1 year ago
parent
commit
a58812e816
89 changed files with 1268 additions and 2171 deletions
  1. 1 3
      Source/QuestPDF.Examples/CanvasExamples.cs
  2. 1 1
      Source/QuestPDF.Examples/ChartExamples.cs
  3. 1 0
      Source/QuestPDF.Examples/ContinousPage.cs
  4. 3 2
      Source/QuestPDF.Examples/ElementExamples.cs
  5. 1 3
      Source/QuestPDF.Examples/Engine/RenderingTest.cs
  6. 4 4
      Source/QuestPDF.Examples/Engine/SimpleDocument.cs
  7. 5 5
      Source/QuestPDF.Examples/ImageExamples.cs
  8. 25 0
      Source/QuestPDF.Examples/SkiaSharpHelpers.cs
  9. 1 17
      Source/QuestPDF.Examples/SvgImageExample.cs
  10. 16 63
      Source/QuestPDF.Examples/TextExamples.cs
  11. 1 0
      Source/QuestPDF.LayoutTests/QuestPDF.LayoutTests.csproj
  12. 8 3
      Source/QuestPDF.LayoutTests/TestEngine/LayoutTestOutputVisualization.cs
  13. 3 3
      Source/QuestPDF.LayoutTests/TestEngine/MockChild.cs
  14. 34 0
      Source/QuestPDF.ReportSample/Helpers.cs
  15. 1 0
      Source/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs
  16. 2 0
      Source/QuestPDF.ReportSample/Layouts/StandardReport.cs
  17. 16 9
      Source/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs
  18. 7 7
      Source/QuestPDF.UnitTests/DynamicImageTests.cs
  19. 0 146
      Source/QuestPDF.UnitTests/FontStyleSetTests.cs
  20. 18 5
      Source/QuestPDF.UnitTests/TestEngine/MockCanvas.cs
  21. 17 5
      Source/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs
  22. 1 1
      Source/QuestPDF.UnitTests/TestEngine/TestPlan.cs
  23. 3 13
      Source/QuestPDF/Drawing/DocumentGenerator.cs
  24. 10 201
      Source/QuestPDF/Drawing/FontManager.cs
  25. 0 131
      Source/QuestPDF/Drawing/FontStyleSet.cs
  26. 50 3
      Source/QuestPDF/Drawing/FreeCanvas.cs
  27. 26 13
      Source/QuestPDF/Drawing/ImageCanvas.cs
  28. 6 0
      Source/QuestPDF/Drawing/ParagraphStyleManager.cs
  29. 11 10
      Source/QuestPDF/Drawing/PdfCanvas.cs
  30. 6 5
      Source/QuestPDF/Drawing/PreviewerCanvas.cs
  31. 66 21
      Source/QuestPDF/Drawing/SkiaCanvasBase.cs
  32. 3 2
      Source/QuestPDF/Drawing/SkiaDocumentCanvasBase.cs
  33. 0 221
      Source/QuestPDF/Drawing/TextShaper.cs
  34. 6 0
      Source/QuestPDF/Drawing/TextStyleManager.cs
  35. 4 4
      Source/QuestPDF/Drawing/XpsCanvas.cs
  36. 1 1
      Source/QuestPDF/Elements/AspectRatio.cs
  37. 1 1
      Source/QuestPDF/Elements/Background.cs
  38. 4 4
      Source/QuestPDF/Elements/Border.cs
  39. 0 32
      Source/QuestPDF/Elements/Canvas.cs
  40. 2 1
      Source/QuestPDF/Elements/DebugArea.cs
  41. 27 9
      Source/QuestPDF/Elements/DynamicImage.cs
  42. 30 0
      Source/QuestPDF/Elements/DynamicSvgImage.cs
  43. 3 2
      Source/QuestPDF/Elements/Image.cs
  44. 8 75
      Source/QuestPDF/Elements/LayoutOverflowVisualization.cs
  45. 2 2
      Source/QuestPDF/Elements/Line.cs
  46. 23 0
      Source/QuestPDF/Elements/SvgImage.cs
  47. 24 0
      Source/QuestPDF/Elements/SvgPath.cs
  48. 0 16
      Source/QuestPDF/Elements/Text/Calculation/TextDrawingRequest.cs
  49. 0 45
      Source/QuestPDF/Elements/Text/Calculation/TextLine.cs
  50. 0 10
      Source/QuestPDF/Elements/Text/Calculation/TextLineElement.cs
  51. 0 16
      Source/QuestPDF/Elements/Text/Calculation/TextMeasurementRequest.cs
  52. 0 22
      Source/QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs
  53. 0 163
      Source/QuestPDF/Elements/Text/FontFallback.cs
  54. 2 5
      Source/QuestPDF/Elements/Text/Items/ITextBlockItem.cs
  55. 4 31
      Source/QuestPDF/Elements/Text/Items/TextBlockElement.cs
  56. 2 18
      Source/QuestPDF/Elements/Text/Items/TextBlockHyperlink.cs
  57. 2 16
      Source/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs
  58. 2 16
      Source/QuestPDF/Elements/Text/Items/TextBlockSectionLink.cs
  59. 1 190
      Source/QuestPDF/Elements/Text/Items/TextBlockSpan.cs
  60. 209 154
      Source/QuestPDF/Elements/Text/TextBlock.cs
  61. 20 16
      Source/QuestPDF/Fluent/ElementExtensions.cs
  62. 27 12
      Source/QuestPDF/Fluent/GenerateExtensions.cs
  63. 21 0
      Source/QuestPDF/Fluent/ImageExtensions.cs
  64. 128 0
      Source/QuestPDF/Fluent/SvgExtensions.cs
  65. 17 100
      Source/QuestPDF/Fluent/TextExtensions.cs
  66. 1 1
      Source/QuestPDF/Fluent/TextStyleExtensions.cs
  67. 14 0
      Source/QuestPDF/Helpers/ColorParser.cs
  68. 3 15
      Source/QuestPDF/Helpers/ColorValidator.cs
  69. 7 38
      Source/QuestPDF/Helpers/Helpers.cs
  70. 3 115
      Source/QuestPDF/Helpers/NativeDependencyCompatibilityChecker.cs
  71. 7 52
      Source/QuestPDF/Helpers/Placeholders.cs
  72. 17 3
      Source/QuestPDF/Infrastructure/ICanvas.cs
  73. 18 28
      Source/QuestPDF/Infrastructure/Image.cs
  74. 6 0
      Source/QuestPDF/Infrastructure/ParagraphStyle.cs
  75. 51 0
      Source/QuestPDF/Infrastructure/SvgImage.cs
  76. 5 32
      Source/QuestPDF/Infrastructure/TextStyleManager.cs
  77. 9 16
      Source/QuestPDF/Previewer/ExceptionDocument.cs
  78. 7 0
      Source/QuestPDF/QuestPDF.csproj
  79. 15 0
      Source/QuestPDF/Resources/Documentation.xml
  80. 20 3
      Source/QuestPDF/Skia/SkCanvas.cs
  81. 98 0
      Source/QuestPDF/Skia/SkColor.cs
  82. 6 2
      Source/QuestPDF/Skia/SkImage.cs
  83. 3 0
      Source/QuestPDF/Skia/SkRect.cs
  84. 21 0
      Source/QuestPDF/Skia/SkSize.cs
  85. 4 0
      Source/QuestPDF/Skia/SkSvgImage.cs
  86. 34 7
      Source/QuestPDF/Skia/Text/SkParagraph.cs
  87. 1 1
      Source/QuestPDF/Skia/Text/SkParagraphBuilder.cs
  88. 1 0
      Source/QuestPDF/Skia/Text/SkTextStyle.cs
  89. BIN
      Source/QuestPDF/skia_questpdf.dylib

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

@@ -1,9 +1,7 @@
-using Microcharts;
 using NUnit.Framework;
 using NUnit.Framework;
 using QuestPDF.Examples.Engine;
 using QuestPDF.Examples.Engine;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
-using QuestPDF.Infrastructure;
 using SkiaSharp;
 using SkiaSharp;
 
 
 namespace QuestPDF.Examples
 namespace QuestPDF.Examples
@@ -26,7 +24,7 @@ namespace QuestPDF.Examples
                         .MinimalBox()
                         .MinimalBox()
                         .Layers(layers =>
                         .Layers(layers =>
                         {
                         {
-                            layers.Layer().Canvas((canvas, size) =>
+                            layers.Layer().SkiaSharpCanvas((canvas, size) =>
                             {
                             {
                                 DrawRoundedRectangle(Colors.White, false);
                                 DrawRoundedRectangle(Colors.White, false);
                                 DrawRoundedRectangle(Colors.Blue.Darken2, true);
                                 DrawRoundedRectangle(Colors.Blue.Darken2, true);

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

@@ -67,7 +67,7 @@ namespace QuestPDF.Examples
                                 .Border(1)
                                 .Border(1)
                                 .ExtendHorizontal()
                                 .ExtendHorizontal()
                                 .Height(300)
                                 .Height(300)
-                                .Canvas((canvas, size) =>
+                                .SkiaSharpCanvas((canvas, size) =>
                                 {
                                 {
                                     var chart = new BarChart
                                     var chart = new BarChart
                                     {
                                     {

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

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

+ 3 - 2
Source/QuestPDF.Examples/ElementExamples.cs

@@ -63,6 +63,7 @@ namespace QuestPDF.Examples
             RenderingTest
             RenderingTest
                 .Create()
                 .Create()
                 .PageSize(740, 200)
                 .PageSize(740, 200)
+                .ShowResults()
                 .Render(container =>
                 .Render(container =>
                 {
                 {
                     container
                     container
@@ -229,7 +230,7 @@ namespace QuestPDF.Examples
                     container
                     container
                         .Background("#FFF")
                         .Background("#FFF")
                         .Padding(25)
                         .Padding(25)
-                        .Canvas((canvas, size) =>
+                        .SkiaSharpCanvas((canvas, size) =>
                         {
                         {
                             using var paint = new SKPaint
                             using var paint = new SKPaint
                             {
                             {
@@ -450,7 +451,7 @@ namespace QuestPDF.Examples
                                 column.Item().PaddingTop(40).Text("Text 2");
                                 column.Item().PaddingTop(40).Text("Text 2");
                             });
                             });
                     
                     
-                            layers.Layer().Canvas((canvas, size) =>
+                            layers.Layer().SkiaSharpCanvas((canvas, size) =>
                             {
                             {
                                 using var paint = new SKPaint
                                 using var paint = new SKPaint
                                 {
                                 {

+ 1 - 3
Source/QuestPDF.Examples/Engine/RenderingTest.cs

@@ -103,9 +103,7 @@ namespace QuestPDF.Examples.Engine
 
 
         public void RenderDocument(Action<IDocumentContainer> content)
         public void RenderDocument(Action<IDocumentContainer> content)
         {
         {
-            MaxPagesThreshold ??= ResultType == RenderingTestResult.Pdf ? 1000 : 10;
-            var document = new SimpleDocument(content, MaxPagesThreshold.Value, ApplyCaching, ApplyDebugging);
-
+            var document = new SimpleDocument(content, ApplyCaching, ApplyDebugging);
             Render(document);
             Render(document);
         }
         }
         
         

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

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

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

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

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

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

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

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

+ 16 - 63
Source/QuestPDF.Examples/TextExamples.cs

@@ -377,7 +377,6 @@ namespace QuestPDF.Examples
                             text.Span("This is a random image aligned to the baseline: ");
                             text.Span("This is a random image aligned to the baseline: ");
                             
                             
                             text.Element()
                             text.Element()
-                                .PaddingBottom(-6)
                                 .Height(24)
                                 .Height(24)
                                 .Width(48)
                                 .Width(48)
                                 .Image(Placeholders.Image);
                                 .Image(Placeholders.Image);
@@ -913,68 +912,6 @@ namespace QuestPDF.Examples
                 });
                 });
         }
         }
         
         
-        [Test]
-        public void DetectSpanPositionExample()
-        {
-            RenderingTest
-                .Create()
-                .PageSize(new PageSize(650, 800))
-                .ProduceImages()
-                .ShowResults()
-                .Render(container =>
-                {
-                    var fontSize = 20;
-                    
-                    var paint = new SKPaint
-                    {
-                        Color = SKColors.Red,
-                        TextSize = fontSize
-                    };
-                    
-                    var fontMetrics = paint.FontMetrics;
-
-                    var start = 0f;
-                    var end = 0f;
-                    
-                    // corner case: what if text is paged? clamp start and end?
-
-                    container
-                        .Padding(25)
-                        .DefaultTextStyle(x => x.FontSize(fontSize).FontFamily("Calibri"))
-                        .Layers(layers =>
-                        {
-                            layers.PrimaryLayer().Text(text =>
-                            {
-                                text.Span(Placeholders.Paragraph());
-                                text.Span(" - ");
-                                
-                                // record start
-                                text.Element().Width(1).Height(1)
-                                    .Canvas((canvas, size) => start = canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
-                                
-                                text.Span(Placeholders.LoremIpsum()).BackgroundColor(Colors.Red.Lighten4);
-                                
-                                // record end
-                                text.Element().Width(1).Height(1)
-                                    .Canvas((canvas, size) => end = canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
-                            
-                                text.Span(" - ");
-                                text.Span(Placeholders.Paragraph());
-                            });
-                            
-                            layers.Layer().Canvas((canvas, size) =>
-                            {
-                                canvas.Save();
-      
-                                canvas.Translate(-canvas.TotalMatrix.TransX / canvas.TotalMatrix.ScaleX, -canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
-                                canvas.DrawRect(10, start + fontMetrics.Ascent, 5, end - start + (fontMetrics.Bottom - fontMetrics.Ascent), paint);
-                                
-                                canvas.Restore();
-                            });
-                        });
-                });
-        }
-        
         [Test]
         [Test]
         public void InconsistentLineHeightWhenUsingNewLineTest()
         public void InconsistentLineHeightWhenUsingNewLineTest()
         {
         {
@@ -1062,5 +999,21 @@ namespace QuestPDF.Examples
                      });
                      });
                  });
                  });
          }
          }
+         
+         [Test]
+         public void LongPageableText()
+         {
+             var longText = string.Join("\n", Enumerable.Range(0, 100).Select(_ => Placeholders.Paragraph()));
+             
+             RenderingTest
+                 .Create()
+                 .ProducePdf()
+                 .ShowResults()
+                 .PageSize(PageSizes.A4)
+                 .Render(container =>
+                 {
+                     container.Padding(25).Background(Colors.Grey.Lighten3).AlignLeft().Text(longText);
+                 });
+         }
     }
     }
 }
 }

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

@@ -12,6 +12,7 @@
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
         <PackageReference Include="NUnit" Version="4.0.1" />
         <PackageReference Include="NUnit" Version="4.0.1" />
         <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
         <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
+        <PackageReference Include="SkiaSharp" Version="2.88.7" />
     </ItemGroup>
     </ItemGroup>
 
 
     <ItemGroup>
     <ItemGroup>

+ 8 - 3
Source/QuestPDF.LayoutTests/TestEngine/LayoutTestOutputVisualization.cs

@@ -87,9 +87,14 @@ internal static class LayoutTestResultVisualization
             canvas.Translate(Padding, Padding);
             canvas.Translate(Padding, Padding);
             
             
             // draw title
             // draw title
-            using var textPaint = TextStyle.LibraryDefault.FontSize(8).FontColor(Colors.White).Bold().ToPaint().Clone();
-            textPaint.TextAlign = SKTextAlign.Center;
-
+            using var textPaint = new SKPaint
+            {
+                TextSize = 8,
+                Color = Colors.White.ColorToCode(),
+                Typeface = SKTypeface.FromFamilyName("Calibri", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright),
+                TextAlign = SKTextAlign.Center
+            };
+            
             var actualHeaderPosition = new SKPoint(result.PageSize.Width / 2, textPaint.TextSize / 2);
             var actualHeaderPosition = new SKPoint(result.PageSize.Width / 2, textPaint.TextSize / 2);
             canvas.DrawText("ACTUAL", actualHeaderPosition, textPaint);
             canvas.DrawText("ACTUAL", actualHeaderPosition, textPaint);
             
             

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

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

+ 34 - 0
Source/QuestPDF.ReportSample/Helpers.cs

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

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

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

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

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

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

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

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

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

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

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

+ 18 - 5
Source/QuestPDF.UnitTests/TestEngine/MockCanvas.cs

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

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

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

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

@@ -240,7 +240,7 @@ namespace QuestPDF.UnitTests.TestEngine
                 {
                 {
                     CompressionQuality = ImageCompressionQuality.Medium,
                     CompressionQuality = ImageCompressionQuality.Medium,
                     TargetDpi = DocumentSettings.DefaultRasterDpi,
                     TargetDpi = DocumentSettings.DefaultRasterDpi,
-                    Source = Placeholders.Image
+                    Source = paylaod => Placeholders.Image(paylaod.ImageSize)
                 }
                 }
             };
             };
         }
         }

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

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

+ 10 - 201
Source/QuestPDF/Drawing/FontManager.cs

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

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

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

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

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

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

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

+ 6 - 0
Source/QuestPDF/Drawing/ParagraphStyleManager.cs

@@ -0,0 +1,6 @@
+namespace QuestPDF.Drawing;
+
+static class ParagraphStyleManager
+{
+    
+}

+ 11 - 10
Source/QuestPDF/Drawing/PdfCanvas.cs

@@ -3,22 +3,23 @@ using System.IO;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
     internal sealed class PdfCanvas : SkiaDocumentCanvasBase
     internal sealed class PdfCanvas : SkiaDocumentCanvasBase
     {
     {
-        public PdfCanvas(Stream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings) 
+        public PdfCanvas(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings) 
             : base(CreatePdf(stream, documentMetadata, documentSettings))
             : base(CreatePdf(stream, documentMetadata, documentSettings))
         {
         {
             
             
         }
         }
 
 
-        private static SKDocument CreatePdf(Stream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings)
+        private static SkDocument CreatePdf(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings)
         {
         {
             try
             try
             {
             {
-                return SKDocument.CreatePdf(stream, MapMetadata(documentMetadata, documentSettings));
+                return SkPdfDocument.Create(stream, MapMetadata(documentMetadata, documentSettings));
             }
             }
             catch (TypeInitializationException exception)
             catch (TypeInitializationException exception)
             {
             {
@@ -26,9 +27,9 @@ namespace QuestPDF.Drawing
             }
             }
         }
         }
 
 
-        private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata, DocumentSettings documentSettings)
+        private static SkPdfDocumentMetadata MapMetadata(DocumentMetadata metadata, DocumentSettings documentSettings)
         {
         {
-            return new SKDocumentPdfMetadata
+            return new SkPdfDocumentMetadata
             {
             {
                 Title = metadata.Title,
                 Title = metadata.Title,
                 Author = metadata.Author,
                 Author = metadata.Author,
@@ -37,12 +38,12 @@ namespace QuestPDF.Drawing
                 Creator = metadata.Creator,
                 Creator = metadata.Creator,
                 Producer = metadata.Producer,
                 Producer = metadata.Producer,
                 
                 
-                Creation = metadata.CreationDate,
-                Modified = metadata.ModifiedDate,
+                CreationDate = new SkDateTime(metadata.CreationDate),
+                ModificationDate = new SkDateTime(metadata.ModifiedDate),
                 
                 
-                RasterDpi = documentSettings.ImageRasterDpi,
-                EncodingQuality = documentSettings.ImageCompressionQuality.ToQualityValue(),
-                PdfA = documentSettings.PdfA
+                RasterDPI = documentSettings.ImageRasterDpi,
+                ImageEncodingQuality = documentSettings.ImageCompressionQuality.ToQualityValue(),
+                SupportPDFA = documentSettings.PdfA
             };
             };
         }
         }
     }
     }

+ 6 - 5
Source/QuestPDF/Drawing/PreviewerCanvas.cs

@@ -1,14 +1,15 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
     internal class PreviewerPageSnapshot
     internal class PreviewerPageSnapshot
     {
     {
-        public SKPicture Picture { get; set; }
+        public SkPicture Picture { get; set; }
         public Size Size { get; set; }
         public Size Size { get; set; }
 
 
-        public PreviewerPageSnapshot(SKPicture picture, Size size)
+        public PreviewerPageSnapshot(SkPicture picture, Size size)
         {
         {
             Picture = picture;
             Picture = picture;
             Size = size;
             Size = size;
@@ -23,7 +24,7 @@ namespace QuestPDF.Drawing
     
     
     internal class PreviewerCanvas : SkiaCanvasBase
     internal class PreviewerCanvas : SkiaCanvasBase
     {
     {
-        private SKPictureRecorder? PictureRecorder { get; set; }
+        private SkPictureRecorder? PictureRecorder { get; set; }
         private Size? CurrentPageSize { get; set; }
         private Size? CurrentPageSize { get; set; }
 
 
         private ICollection<PreviewerPageSnapshot> PageSnapshots { get; } = new List<PreviewerPageSnapshot>();
         private ICollection<PreviewerPageSnapshot> PageSnapshots { get; } = new List<PreviewerPageSnapshot>();
@@ -36,9 +37,9 @@ namespace QuestPDF.Drawing
         public override void BeginPage(Size size)
         public override void BeginPage(Size size)
         {
         {
             CurrentPageSize = size;
             CurrentPageSize = size;
-            PictureRecorder = new SKPictureRecorder();
+            PictureRecorder = new SkPictureRecorder();
 
 
-            Canvas = PictureRecorder.BeginRecording(new SKRect(0, 0, size.Width, size.Height));
+            Canvas = PictureRecorder.BeginRecording(size.Width, size.Height);
         }
         }
 
 
         public override void EndPage()
         public override void EndPage()

+ 66 - 21
Source/QuestPDF/Drawing/SkiaCanvasBase.cs

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

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

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

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

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

+ 6 - 0
Source/QuestPDF/Drawing/TextStyleManager.cs

@@ -0,0 +1,6 @@
+namespace QuestPDF.Drawing;
+
+static class TextStyleManager
+{
+    
+}

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

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

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

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

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

@@ -9,7 +9,7 @@ namespace QuestPDF.Elements
         
         
         internal override void Draw(Size availableSpace)
         internal override void Draw(Size availableSpace)
         {
         {
-            Canvas.DrawRectangle(Position.Zero, availableSpace, Color);
+            Canvas.DrawFilledRectangle(Position.Zero, availableSpace, Color);
             base.Draw(availableSpace);
             base.Draw(availableSpace);
         }
         }
     }
     }

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

@@ -16,22 +16,22 @@ namespace QuestPDF.Elements
         {
         {
             base.Draw(availableSpace);
             base.Draw(availableSpace);
             
             
-            Canvas.DrawRectangle(
+            Canvas.DrawFilledRectangle(
                 new Position(-Left/2, -Top/2), 
                 new Position(-Left/2, -Top/2), 
                 new Size(availableSpace.Width + Left/2 + Right/2, Top), 
                 new Size(availableSpace.Width + Left/2 + Right/2, Top), 
                 Color);
                 Color);
             
             
-            Canvas.DrawRectangle(
+            Canvas.DrawFilledRectangle(
                 new Position(-Left/2, -Top/2), 
                 new Position(-Left/2, -Top/2), 
                 new Size(Left, availableSpace.Height + Top/2 + Bottom/2), 
                 new Size(Left, availableSpace.Height + Top/2 + Bottom/2), 
                 Color);
                 Color);
             
             
-            Canvas.DrawRectangle(
+            Canvas.DrawFilledRectangle(
                 new Position(-Left/2, availableSpace.Height-Bottom/2), 
                 new Position(-Left/2, availableSpace.Height-Bottom/2), 
                 new Size(availableSpace.Width + Left/2 + Right/2, Bottom), 
                 new Size(availableSpace.Width + Left/2 + Right/2, Bottom), 
                 Color);
                 Color);
             
             
-            Canvas.DrawRectangle(
+            Canvas.DrawFilledRectangle(
                 new Position(availableSpace.Width-Right/2, -Top/2), 
                 new Position(availableSpace.Width-Right/2, -Top/2), 
                 new Size(Right, availableSpace.Height + Top/2 + Bottom/2), 
                 new Size(Right, availableSpace.Height + Top/2 + Bottom/2), 
                 Color);
                 Color);

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

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

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

@@ -1,6 +1,7 @@
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Elements
 namespace QuestPDF.Elements
 {
 {
@@ -12,7 +13,7 @@ namespace QuestPDF.Elements
         public string Color { get; set; } = Colors.Red.Medium;
         public string Color { get; set; } = Colors.Red.Medium;
         public void Compose(IContainer container)
         public void Compose(IContainer container)
         {
         {
-            var backgroundColor = SKColor.Parse(Color).WithAlpha(50).ToString();
+            var backgroundColor = SkColor.Parse(Color).ColorWithAlpha(50).ColorToString();
             
             
             container
             container
                 .Border(1)
                 .Border(1)

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

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

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

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

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

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

+ 8 - 75
Source/QuestPDF/Elements/LayoutOverflowVisualization.cs

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

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

@@ -38,11 +38,11 @@ namespace QuestPDF.Elements
         {
         {
             if (Type == LineType.Vertical)
             if (Type == LineType.Vertical)
             {
             {
-                Canvas.DrawRectangle(new Position(-Size/2, 0), new Size(Size, availableSpace.Height), Color);
+                Canvas.DrawFilledRectangle(new Position(-Size/2, 0), new Size(Size, availableSpace.Height), Color);
             }
             }
             else if (Type == LineType.Horizontal)
             else if (Type == LineType.Horizontal)
             {
             {
-                Canvas.DrawRectangle(new Position(0, -Size/2), new Size(availableSpace.Width, Size), Color);
+                Canvas.DrawFilledRectangle(new Position(0, -Size/2), new Size(availableSpace.Width, Size), Color);
             }
             }
         }
         }
     }
     }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 209 - 154
Source/QuestPDF/Elements/Text/TextBlock.cs

@@ -1,9 +1,12 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
-using QuestPDF.Elements.Text.Calculation;
 using QuestPDF.Elements.Text.Items;
 using QuestPDF.Elements.Text.Items;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+using QuestPDF.Skia.Text;
 
 
 namespace QuestPDF.Elements.Text
 namespace QuestPDF.Elements.Text
 {
 {
@@ -14,42 +17,27 @@ namespace QuestPDF.Elements.Text
         public HorizontalAlignment? Alignment { get; set; }
         public HorizontalAlignment? Alignment { get; set; }
         public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
         public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
 
 
+        private bool RebuildParagraphForEveryPage { get; set; }
+        
+        private SkParagraph Paragraph { get; set; }
+        private SkSize[] LineMetrics { get; set; }
+        private float WidthForLineMetricsCalculation { get; set; }
+        private int CurrentLineIndex { get; set; } = 0;
+        private float CurrentTopOffset { get; set; } = 0f;
+        private SkRect[] PlaceholderPositions { get; set; }
+        private float MaximumWidth { get; set; }
+        
         public string Text => string.Join(" ", Items.OfType<TextBlockSpan>().Select(x => x.Text));
         public string Text => string.Join(" ", Items.OfType<TextBlockSpan>().Select(x => x.Text));
 
 
-        private Queue<ITextBlockItem> RenderingQueue { get; set; }
-        private int CurrentElementIndex { get; set; }
-
-        private bool FontFallbackApplied { get; set; } = false;
-
+        ~TextBlock()
+        {
+            Paragraph?.Dispose();
+        }
+        
         public void ResetState()
         public void ResetState()
         {
         {
-            ApplyFontFallback();
-            InitializeQueue();
-            CurrentElementIndex = 0;
-
-            void InitializeQueue()
-            {
-                // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
-                if (RenderingQueue == null)
-                {
-                    RenderingQueue = new Queue<ITextBlockItem>(Items);
-                    return;
-                }
-                
-                RenderingQueue.Clear();
-            
-                foreach (var item in Items)
-                    RenderingQueue.Enqueue(item);
-            }
-
-            void ApplyFontFallback()
-            {
-                if (FontFallbackApplied)
-                    return;
-
-                Items = Items.ApplyFontFallback().ToList();
-                FontFallbackApplied = true;
-            }
+            CurrentLineIndex = 0;
+            CurrentTopOffset = 0;
         }
         }
         
         
         void SetDefaultAlignment()
         void SetDefaultAlignment()
@@ -64,172 +52,239 @@ namespace QuestPDF.Elements.Text
 
 
         internal override SpacePlan Measure(Size availableSpace)
         internal override SpacePlan Measure(Size availableSpace)
         {
         {
+            if (Items.Count == 0)
+                return SpacePlan.FullRender(Size.Zero);
+            
             SetDefaultAlignment();
             SetDefaultAlignment();
+            Initialize();
+
+            if (Math.Abs(WidthForLineMetricsCalculation - availableSpace.Width) > Size.Epsilon)
+            {
+                WidthForLineMetricsCalculation = availableSpace.Width;
+                Paragraph.PlanLayout(availableSpace.Width);
+                LineMetrics = Paragraph.GetLineMetrics();
+                PlaceholderPositions = Paragraph.GetPlaceholderPositions();
+                MaximumWidth = LineMetrics.Max(x => x.Width);
+            }
             
             
-            if (!RenderingQueue.Any())
+            if (CurrentLineIndex > LineMetrics.Length)
                 return SpacePlan.FullRender(Size.Zero);
                 return SpacePlan.FullRender(Size.Zero);
             
             
-            var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
-
-            if (!lines.Any())
-                return SpacePlan.Wrap();
+            var totalHeight = 0f;
+            var totalLines = 0;
             
             
-            var width = lines.Max(x => x.Width);
-            var height = lines.Sum(x => x.LineHeight);
+            for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
+            {
+                var lineMetric = LineMetrics[lineIndex];
+                var newTotalHeight = totalHeight + lineMetric.Height;
+                
+                if (newTotalHeight > availableSpace.Height + Size.Epsilon)
+                    break;
+                
+                totalHeight = newTotalHeight;
+                totalLines++;
+            }
 
 
-            if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
+            if (totalLines == 0)
                 return SpacePlan.Wrap();
                 return SpacePlan.Wrap();
 
 
-            var fullyRenderedItemsCount = lines
-                .SelectMany(x => x.Elements)
-                .GroupBy(x => x.Item)
-                .Count(x => x.Any(y => y.Measurement.IsLast));
+            var requiredArea = new Size(
+                Math.Min(MaximumWidth, availableSpace.Width),
+                Math.Min(totalHeight, availableSpace.Height));
             
             
-            if (fullyRenderedItemsCount == RenderingQueue.Count)
-                return SpacePlan.FullRender(width, height);
-            
-            return SpacePlan.PartialRender(width, height);
+            if (CurrentLineIndex + totalLines < LineMetrics.Length)
+                return SpacePlan.PartialRender(requiredArea);
+
+            return SpacePlan.FullRender(requiredArea);
         }
         }
 
 
         internal override void Draw(Size availableSpace)
         internal override void Draw(Size availableSpace)
         {
         {
-            SetDefaultAlignment();
+            if (Items.Count == 0)
+                return;
             
             
-            var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
+            var (linesToDraw, takenHeight) = CalculateDrawingMetrics();
+            DrawParagraph();
             
             
-            if (!lines.Any())
-                return;
+            CurrentLineIndex += linesToDraw;
+            CurrentTopOffset += takenHeight;
+
+            if (CurrentLineIndex == LineMetrics.Length)
+                ResetState();
             
             
-            var topOffset = 0f;
+            return;
 
 
-            foreach (var line in lines)
+            (int linesToDraw, float takenHeight) CalculateDrawingMetrics()
             {
             {
-                var leftOffset = GetAlignmentOffset(line.Width);
-
-                foreach (var item in line.Elements.Where(x => x.Measurement.Width > 0.0))
+                var linesToDraw = 0;
+                var takenHeight = 0f;
+                
+                for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
                 {
                 {
-                    var textDrawingRequest = new TextDrawingRequest
-                    {
-                        Canvas = Canvas,
-                        PageContext = PageContext,
-                        
-                        StartIndex = item.Measurement.StartIndex,
-                        EndIndex = item.Measurement.EndIndex,
-                        
-                        TextSize = new Size(item.Measurement.Width, line.LineHeight),
-                        TotalAscent = line.Ascent
-                    };
+                    var lineMetric = LineMetrics[lineIndex];
                 
                 
-                    var canvasOffset = ContentDirection == ContentDirection.LeftToRight
-                        ? new Position(leftOffset, topOffset - line.Ascent)
-                        : new Position(availableSpace.Width - leftOffset - item.Measurement.Width, topOffset - line.Ascent);
-                    
-                    Canvas.Translate(canvasOffset);
-                    item.Item.Draw(textDrawingRequest);
-                    Canvas.Translate(canvasOffset.Reverse());
+                    var newTotalHeight = takenHeight + lineMetric.Height;
+
+                    if (newTotalHeight > availableSpace.Height + Size.Epsilon)
+                        break;
                     
                     
-                    leftOffset += item.Measurement.Width;
+                    takenHeight = newTotalHeight;
+                    linesToDraw++;
                 }
                 }
-                
-                topOffset += line.LineHeight;
-            }
-
-            lines
-                .SelectMany(x => x.Elements)
-                .GroupBy(x => x.Item)
-                .Where(x => x.Any(y => y.Measurement.IsLast))
-                .Select(x => x.Key)
-                .ToList()
-                .ForEach(x => RenderingQueue.Dequeue());
 
 
-            var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
-            CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
-            
-            if (!RenderingQueue.Any())
-                ResetState();
+                return (linesToDraw, takenHeight);
+            }
             
             
-            float GetAlignmentOffset(float lineWidth)
+            void DrawParagraph()
             {
             {
-                var emptySpace = availableSpace.Width - lineWidth;
-
-                return Alignment switch
+                var takesMultiplePages = linesToDraw != LineMetrics.Length;
+                
+                if (takesMultiplePages)
                 {
                 {
-                    HorizontalAlignment.Left => ContentDirection == ContentDirection.LeftToRight ? 0 : emptySpace,
-                    HorizontalAlignment.Center => emptySpace / 2,
-                    HorizontalAlignment.Right => ContentDirection == ContentDirection.LeftToRight ? emptySpace : 0,
-                    _ => 0
-                };
+                    Canvas.Save();
+                    Canvas.ClipRectangle(new SkRect(0, 0, availableSpace.Width, takenHeight));
+                    Canvas.Translate(new Position(0, -CurrentTopOffset));
+                }
+                
+                Canvas.DrawParagraph(Paragraph);
+                DrawInjectedElements();
+                DrawHyperlinks();
+                DrawSectionLinks();
+                
+                if (takesMultiplePages)
+                    Canvas.Restore();
             }
             }
-        }
-
-        public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
-        {
-            var queue = new Queue<ITextBlockItem>(RenderingQueue);
-            var currentItemIndex = CurrentElementIndex;
-            var currentHeight = 0f;
 
 
-            while (queue.Any())
+            void DrawInjectedElements()
             {
             {
-                var line = GetNextLine();
+                var elementItems = Items.OfType<TextBlockElement>().ToArray();
                 
                 
-                if (!line.Elements.Any())
-                    yield break;
-                
-                if (currentHeight + line.LineHeight > availableHeight + Size.Epsilon)
-                    yield break;
+                for (var placeholderIndex = 0; placeholderIndex < PlaceholderPositions.Length; placeholderIndex++)
+                {
+                    var placeholder = PlaceholderPositions[placeholderIndex];
+                    var associatedElement = elementItems[placeholderIndex];
 
 
-                currentHeight += line.LineHeight;
-                yield return line;
+                    var offset = new Position(placeholder.Left, placeholder.Top);
+                    
+                    if (!IsPositionVisible(offset))
+                        continue;
+                    
+                    Canvas.Translate(offset);
+                    associatedElement.Element.Draw(new Size(placeholder.Width, placeholder.Height));
+                    Canvas.Translate(offset.Reverse());
+                }
             }
             }
-
-            TextLine GetNextLine()
+            
+            void DrawHyperlinks()
             {
             {
-                var currentWidth = 0f;
-
-                var currentLineElements = new List<TextLineElement>();
+                foreach (var hyperlink in Items.OfType<TextBlockHyperlink>())
+                {
+                    var positions = Paragraph.GetTextRangePositions(hyperlink.ParagraphBeginIndex, hyperlink.ParagraphBeginIndex + hyperlink.Text.Length);
+                    
+                    foreach (var position in positions)
+                    {
+                        var offset = new Position(position.Left, position.Top);
+                        
+                        if (!IsPositionVisible(offset))
+                            continue;
+                        
+                        Canvas.Translate(offset);
+                        Canvas.DrawHyperlink(hyperlink.Url, new Size(position.Width, position.Height));
+                        Canvas.Translate(offset.Reverse());
+                    }
+                }
+            }
             
             
-                while (true)
+            void DrawSectionLinks()
+            {
+                foreach (var sectionLink in Items.OfType<TextBlockSectionLink>())
                 {
                 {
-                    if (!queue.Any())
-                        break;
-
-                    var currentElement = queue.Peek();
+                    var positions = Paragraph.GetTextRangePositions(sectionLink.ParagraphBeginIndex, sectionLink.ParagraphBeginIndex + sectionLink.Text.Length);
+                    var targetName = PageContext.GetDocumentLocationName(sectionLink.SectionName);
                     
                     
-                    var measurementRequest = new TextMeasurementRequest
+                    foreach (var position in positions)
                     {
                     {
-                        Canvas = Canvas,
-                        PageContext = PageContext,
+                        var offset = new Position(position.Left, position.Top);
                         
                         
-                        StartIndex = currentItemIndex,
-                        AvailableWidth = availableWidth - currentWidth,
+                        if (!IsPositionVisible(offset))
+                            continue;
                         
                         
-                        IsFirstElementInBlock = currentElement == Items.First(),
-                        IsFirstElementInLine = !currentLineElements.Any()
-                    };
-                
-                    var measurementResponse = currentElement.Measure(measurementRequest);
+                        Canvas.Translate(offset);
+                        Canvas.DrawSectionLink(targetName, new Size(position.Width, position.Height));
+                        Canvas.Translate(offset.Reverse());
+                    }
+                }
+            }
+
+            bool IsPositionVisible(Position position)
+            {
+                return CurrentTopOffset <= position.Y || position.Y <= CurrentTopOffset + takenHeight;
+            }
+        }
+        
+        private void Initialize()
+        {
+            if (Paragraph != null && !RebuildParagraphForEveryPage)
+                return;
+            
+            RebuildParagraphForEveryPage = Items.Any(x => x is TextBlockPageNumber);
+            BuildParagraph();
+        }
+
+        private void BuildParagraph()
+        {
+            using var paragraphStyle = new SkParagraphStyle(new ParagraphStyleConfiguration
+            {
+                Alignment = ParagraphStyleConfiguration.TextAlign.Justify,
+                Direction = ParagraphStyleConfiguration.TextDirection.Ltr,
+                MaxLinesVisible = 1000000,
+                Ellipsis = "..."
+            });
+            
+            using var paragraphBuilder = SkParagraphBuilder.Create(paragraphStyle, FontManager.FontCollection);
+            var currentTextIndex = 0;
+            
+            foreach (var textBlockItem in Items)
+            {
+                if (textBlockItem is TextBlockSpan textBlockSpan)
+                {
+                    var style = textBlockSpan.Style;
                 
                 
-                    if (measurementResponse == null)
-                        break;
-                    
-                    currentLineElements.Add(new TextLineElement
+                    var textStyle = new SkTextStyle(new TextStyleConfiguration
                     {
                     {
-                        Item = currentElement,
-                        Measurement = measurementResponse
+                        FontFamilies = new string[8] { style.FontFamily, null, null, null, null, null, null, null },
+                        FontSize = style.Size.Value,
+                        FontWeight = (TextStyleConfiguration.FontWeights)style.FontWeight.Value,
+                        ForegroundColor = style.Color.ColorToCode()
                     });
                     });
 
 
-                    currentWidth += measurementResponse.Width;
-                    currentItemIndex = measurementResponse.NextIndex;
-                    
-                    if (!measurementResponse.IsLast)
-                        break;
+                    if (textBlockItem is TextBlockSectionLink textBlockSectionLink)
+                        textBlockSectionLink.ParagraphBeginIndex = currentTextIndex;
 
 
-                    currentItemIndex = 0;
-                    queue.Dequeue();
-                }
+                    else if (textBlockItem is TextBlockHyperlink textBlockHyperlink)
+                        textBlockHyperlink.ParagraphBeginIndex = currentTextIndex;
 
 
-                return TextLine.From(currentLineElements);
+                    else if (textBlockItem is TextBlockPageNumber textBlockPageNumber)
+                        textBlockPageNumber.UpdatePageNumberText(PageContext);
+                
+                    paragraphBuilder.AddText(textBlockSpan.Text, textStyle);
+                    currentTextIndex += textBlockSpan.Text.Length;
+                }
+                else if (textBlockItem is TextBlockElement textBlockElement)
+                {
+                    textBlockElement.UpdateElementSize(PageContext, Canvas);
+                    paragraphBuilder.AddPlaceholder(new SkPlaceholderStyle
+                    {
+                        Width = textBlockElement.ElementSize.Width,
+                        Height = textBlockElement.ElementSize.Height,
+                        Alignment = SkPlaceholderStyle.PlaceholderAlignment.AboveBaseline,
+                        Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
+                        BaselineOffset = 0
+                    });
+                }
             }
             }
+
+            Paragraph = paragraphBuilder.CreateParagraph();
         }
         }
     }
     }
 }
 }

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

@@ -298,22 +298,6 @@ namespace QuestPDF.Fluent
             });
             });
         }
         }
         
         
-        /// <summary>
-        /// Provides direct access to the low-level SkiaSharp API.
-        /// <a href="https://www.questpdf.com/api-reference/canvas.html">Learn more</a>
-        /// </summary>
-        /// <example>
-        /// <para>Use this element when needing to render advanced shapes not directly available in the QuestPDF API.</para>
-        /// <para>It's also ideal for integrating with other SkiaSharp-based libraries, such as charting tools, to produce pixel-perfect vector graphics.</para>
-        /// </example>
-        public static void Canvas(this IContainer element, DrawOnCanvas handler)
-        {
-            element.Element(new Canvas
-            {
-                Handler = handler
-            });
-        }
-        
         /// <summary>
         /// <summary>
         /// Removes size constraints and grants its content virtually unlimited space.
         /// Removes size constraints and grants its content virtually unlimited space.
         /// <a href="https://www.questpdf.com/api-reference/unconstrained.html">Learn more</a>
         /// <a href="https://www.questpdf.com/api-reference/unconstrained.html">Learn more</a>
@@ -376,5 +360,25 @@ namespace QuestPDF.Fluent
         {
         {
             return element.Element(new ScaleToFit());
             return element.Element(new ScaleToFit());
         }
         }
+
+        #region Canvas [Obsolete]
+
+        public delegate void DrawOnCanvas(object canvas, Size availableSpace);
+        
+        /// <summary>
+        /// Provides direct access to the low-level SkiaSharp API.
+        /// <a href="https://www.questpdf.com/api-reference/canvas.html">Learn more</a>
+        /// </summary>
+        /// <example>
+        /// <para>Use this element when needing to render advanced shapes not directly available in the QuestPDF API.</para>
+        /// <para>It's also ideal for integrating with other SkiaSharp-based libraries, such as charting tools, to produce pixel-perfect vector graphics.</para>
+        /// </example>
+        [Obsolete("This element has been deprecated since version 2024.2. Please the SVG functionality to provide custom content.")]
+        public static void Canvas(this IContainer element, DrawOnCanvas handler)
+        {
+            throw new NotImplementedException();
+        }
+
+        #endregion
     }
     }
 }
 }

+ 27 - 12
Source/QuestPDF/Fluent/GenerateExtensions.cs

@@ -2,6 +2,7 @@
 using System.IO;
 using System.IO;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Fluent
 namespace QuestPDF.Fluent
 {
 {
@@ -14,9 +15,11 @@ namespace QuestPDF.Fluent
         /// </summary>
         /// </summary>
         public static byte[] GeneratePdf(this IDocument document)
         public static byte[] GeneratePdf(this IDocument document)
         {
         {
-            using var stream = new MemoryStream();
-            document.GeneratePdf(stream);
-            return stream.ToArray();
+            using var stream = new SkWriteStream();
+            DocumentGenerator.GeneratePdf(stream, document);
+            
+            using var data = stream.DetachData();
+            return data.ToSpan().ToArray();
         }
         }
         
         
         /// <summary>
         /// <summary>
@@ -24,8 +27,12 @@ namespace QuestPDF.Fluent
         /// </summary>
         /// </summary>
         public static void GeneratePdf(this IDocument document, string filePath)
         public static void GeneratePdf(this IDocument document, string filePath)
         {
         {
-            using var stream = new FileStream(filePath, FileMode.Create);
-            document.GeneratePdf(stream);
+            var data = document.GeneratePdf();
+            
+            if (File.Exists(filePath))
+                File.Delete(filePath);
+            
+            File.WriteAllBytes(filePath, data);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -33,7 +40,8 @@ namespace QuestPDF.Fluent
         /// </summary>
         /// </summary>
         public static void GeneratePdf(this IDocument document, Stream stream)
         public static void GeneratePdf(this IDocument document, Stream stream)
         {
         {
-            DocumentGenerator.GeneratePdf(stream, document);
+            var data = document.GeneratePdf();
+            stream.Write(data, 0, data.Length);
         }
         }
         
         
         private static int GenerateAndShowCounter = 0;
         private static int GenerateAndShowCounter = 0;
@@ -59,9 +67,11 @@ namespace QuestPDF.Fluent
         /// </summary>
         /// </summary>
         public static byte[] GenerateXps(this IDocument document)
         public static byte[] GenerateXps(this IDocument document)
         {
         {
-            using var stream = new MemoryStream();
-            document.GenerateXps(stream);
-            return stream.ToArray();
+            using var stream = new SkWriteStream();
+            DocumentGenerator.GenerateXps(stream, document);
+            
+            using var data = stream.DetachData();
+            return data.ToSpan().ToArray();
         }
         }
         
         
         /// <summary>
         /// <summary>
@@ -69,8 +79,12 @@ namespace QuestPDF.Fluent
         /// </summary>
         /// </summary>
         public static void GenerateXps(this IDocument document, string filePath)
         public static void GenerateXps(this IDocument document, string filePath)
         {
         {
-            using var stream = new FileStream(filePath, FileMode.Create);
-            document.GenerateXps(stream);
+            var data = document.GenerateXps();
+            
+            if (File.Exists(filePath))
+                File.Delete(filePath);
+            
+            File.WriteAllBytes(filePath, data);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -78,7 +92,8 @@ namespace QuestPDF.Fluent
         /// </summary>
         /// </summary>
         public static void GenerateXps(this IDocument document, Stream stream)
         public static void GenerateXps(this IDocument document, Stream stream)
         {
         {
-            DocumentGenerator.GenerateXps(stream, document);
+            var data = document.GenerateXps();
+            stream.Write(data, 0, data.Length);
         }
         }
         
         
         /// <summary>
         /// <summary>

+ 21 - 0
Source/QuestPDF/Fluent/ImageExtensions.cs

@@ -209,6 +209,27 @@ namespace QuestPDF.Fluent
             return new DynamicImageDescriptor(dynamicImage);
             return new DynamicImageDescriptor(dynamicImage);
         }
         }
         
         
+        /// <summary>
+        /// Renders an image of dynamic size dictated by the document layout constraints.
+        /// </summary>
+        /// <remarks>
+        /// Ideal for generating pixel-perfect images that might lose quality upon scaling, such as maps or charts.
+        /// </remarks>
+        /// <param name="dynamicImageSource">
+        /// A delegate that requests an image of desired resolution calculated based on target physical image size and provided DPI.
+        /// </param>
+        /// <returns>A descriptor for adjusting image attributes like scaling behavior, compression quality, and resolution.</returns>
+        public static DynamicImageDescriptor Image(this IContainer element, Func<ImageSize, byte[]> dynamicImageSource)
+        {
+            var dynamicImage = new DynamicImage
+            {
+                Source = payload => dynamicImageSource(payload.ImageSize)
+            };
+            
+            element.Element(dynamicImage);
+            return new DynamicImageDescriptor(dynamicImage);
+        }
+        
         #region Obsolete
         #region Obsolete
         
         
         [Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that takes the GenerateDynamicImageDelegate as an argument.")]
         [Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that takes the GenerateDynamicImageDelegate as an argument.")]

+ 128 - 0
Source/QuestPDF/Fluent/SvgExtensions.cs

@@ -0,0 +1,128 @@
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+using SvgImage = QuestPDF.Infrastructure.SvgImage;
+
+namespace QuestPDF.Fluent;
+
+/// <summary>
+/// Generates an SVG image based on the given resolution.
+/// </summary>
+/// <param name="size">Desired resolution of the image in pixels.</param>
+/// <returns>An SVG format compatible text.</returns>
+public delegate string? GenerateDynamicSvgDelegate(Size size);
+
+public class SvgImageDescriptor
+{
+    private Elements.SvgImage ImageElement { get; }
+    private AspectRatio AspectRatioElement { get; }
+    private float ImageAspectRatio { get; }
+
+    internal SvgImageDescriptor(Elements.SvgImage imageElement, Elements.AspectRatio aspectRatioElement)
+    {
+        ImageElement = imageElement;
+        AspectRatioElement = aspectRatioElement;
+        ImageAspectRatio = ImageElement.Svg.ViewBox.Width /  ImageElement.Svg.ViewBox.Height;
+    }
+    
+    /// <summary>
+    /// Scales the image to fill the full width of its container. This is the default behavior.
+    /// </summary>
+    public SvgImageDescriptor FitWidth()
+    {
+        return SetAspectRatio(AspectRatioOption.FitWidth);
+    }
+    
+    /// <summary>
+    /// <para>The image stretches vertically to fit the full available height.</para>
+    /// <para>Often used with height-constraining elements such as: <see cref="ConstrainedExtensions.Height">Height</see>, <see cref="ConstrainedExtensions.MaxHeight">MaxHeight</see>, etc.</para>
+    /// </summary>
+    public SvgImageDescriptor FitHeight()
+    {
+        return SetAspectRatio(AspectRatioOption.FitHeight);
+    }
+    
+    /// <summary>
+    /// Combines the FitWidth and FitHeight settings.
+    /// The image resizes itself to utilize all available space, preserving its aspect ratio.
+    /// It will either fill the width or height based on the container's dimensions.
+    /// </summary>
+    /// <remarks>
+    /// An optimal and safe choice.
+    /// </remarks>
+    public SvgImageDescriptor FitArea()
+    {
+        return SetAspectRatio(AspectRatioOption.FitArea);
+    }
+
+    private SvgImageDescriptor SetAspectRatio(AspectRatioOption option)
+    {
+        AspectRatioElement.Ratio = ImageAspectRatio;
+        AspectRatioElement.Option = option;
+        return this;
+    }
+}
+
+public static class SvgExtensions
+{
+    internal static void SvgPath(this IContainer container, string svgPath, string color)
+    {
+        container.Element(new SvgPath
+        {
+            Path = svgPath,
+            FillColor = color
+        });
+    }
+    
+    /// <summary>
+    /// Draws the SVG image loaded from a text.
+    /// <a href="https://www.questpdf.com/api-reference/image.html">Learn more</a>
+    /// </summary>
+    /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="svg.remarks"]/*' />
+    /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="svg.descriptor"]/*' />
+    public static SvgImageDescriptor Svg(this IContainer container, string svg)
+    {
+        var image = SvgImage.FromText(svg);
+        return container.Svg(image);
+    }
+    
+    /// <summary>
+    /// Draws the <see cref="Infrastructure.SvgImage" /> object. Allows to optimize the generation process.
+    /// <a href="https://www.questpdf.com/api-reference/image.html">Learn more</a>
+    /// </summary>
+    /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="svg.remarks"]/*' />
+    /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="svg.descriptor"]/*' />
+    public static SvgImageDescriptor Svg(this IContainer parent, SvgImage image)
+    {
+        var imageElement = new QuestPDF.Elements.SvgImage
+        {
+            Svg = image.SkSvgImage
+        };
+
+        var aspectRationElement = new AspectRatio
+        {
+            Child = imageElement
+        };
+            
+        parent.Element(aspectRationElement);
+        return new SvgImageDescriptor(imageElement, aspectRationElement).FitWidth();
+    }
+    
+    /// <summary>
+    /// Renders an SVG image of dynamic size dictated by the document layout constraints.
+    /// </summary>
+    /// <remarks>
+    /// Ideal for integrating with other libraries, e.g. SkiaSharp or ScottPlot.
+    /// </remarks>
+    /// <param name="dynamicSvgSource">
+    /// A delegate that requests an image of desired size.
+    /// </param>
+    public static void Svg(this IContainer element, GenerateDynamicSvgDelegate dynamicSvgSource)
+    {
+        var dynamicImage = new DynamicSvgImage
+        {
+            SvgSource = dynamicSvgSource
+        };
+            
+        element.Element(dynamicImage);
+    }
+}

+ 17 - 100
Source/QuestPDF/Fluent/TextExtensions.cs

@@ -11,46 +11,21 @@ namespace QuestPDF.Fluent
 {
 {
     public class TextSpanDescriptor
     public class TextSpanDescriptor
     {
     {
-        // optimization: only of fields below has value, where TextBlockSpan is more frequent
-        private TextBlockSpan? TextBlockSpan;
-        private ICollection<TextBlockSpan>? TextBlockSpans;
+        private TextBlockSpan TextBlockSpan;
 
 
         internal TextSpanDescriptor(TextBlockSpan textBlockSpan)
         internal TextSpanDescriptor(TextBlockSpan textBlockSpan)
         {
         {
             TextBlockSpan = textBlockSpan;
             TextBlockSpan = textBlockSpan;
         }
         }
-        
-        internal TextSpanDescriptor(ICollection<TextBlockSpan> textBlockSpans)
-        {
-            TextBlockSpans = textBlockSpans;
-        }
 
 
         internal void MutateTextStyle<T>(Func<TextStyle, T, TextStyle> handler, T argument)
         internal void MutateTextStyle<T>(Func<TextStyle, T, TextStyle> handler, T argument)
         {
         {
-            if (TextBlockSpan != null)
-            {
-                TextBlockSpan.Style = handler(TextBlockSpan.Style, argument);
-                return;
-            }
-            
-            foreach (var textBlockSpan in TextBlockSpans)
-            {
-                textBlockSpan.Style = handler(textBlockSpan.Style, argument);
-            }
+            TextBlockSpan.Style = handler(TextBlockSpan.Style, argument);
         }
         }
         
         
         internal void MutateTextStyle(Func<TextStyle, TextStyle> handler)
         internal void MutateTextStyle(Func<TextStyle, TextStyle> handler)
         {
         {
-            if (TextBlockSpan != null)
-            {
-                TextBlockSpan.Style = handler(TextBlockSpan.Style);
-                return;
-            }
-            
-            foreach (var textBlockSpan in TextBlockSpans)
-            {
-                textBlockSpan.Style = handler(textBlockSpan.Style);
-            }
+            TextBlockSpan.Style = handler(TextBlockSpan.Style);
         }
         }
     }
     }
 
 
@@ -86,7 +61,7 @@ namespace QuestPDF.Fluent
     
     
     public class TextDescriptor
     public class TextDescriptor
     {
     {
-        private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
+        private TextBlock TextBlock { get; } = new();
         private TextStyle? DefaultStyle { get; set; }
         private TextStyle? DefaultStyle { get; set; }
         internal HorizontalAlignment? Alignment { get; set; }
         internal HorizontalAlignment? Alignment { get; set; }
         private float Spacing { get; set; } = 0f;
         private float Spacing { get; set; } = 0f;
@@ -141,26 +116,6 @@ namespace QuestPDF.Fluent
             Spacing = value.ToPoints(unit);
             Spacing = value.ToPoints(unit);
         }
         }
 
 
-        private void AddItemToLastTextBlock(ITextBlockItem item)
-        {
-            if (!TextBlocks.Any())
-                TextBlocks.Add(new TextBlock());
-            
-            var lastTextBlock = TextBlocks.Last();
-
-            // TextBlock with only one Span with empty text is a special case.
-            // It represents an empty line with a given text style (e.g. text height).
-            // When more content is put to text block, the first items should be ignored (removed in this case).
-            // This change fixes inconsistent line height problem.
-            if (lastTextBlock.Items.Count == 1 && lastTextBlock.Items[0] is TextBlockSpan { Text: "" })
-            {
-                lastTextBlock.Items[0] = item;
-                return;
-            }
-            
-            lastTextBlock.Items.Add(item);
-        }
-        
         [Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
         [Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
         public void Span(string? text, TextStyle style)
         public void Span(string? text, TextStyle style)
         {
         {
@@ -174,37 +129,11 @@ namespace QuestPDF.Fluent
         public TextSpanDescriptor Span(string? text)
         public TextSpanDescriptor Span(string? text)
         {
         {
             if (text == null)
             if (text == null)
-                return new TextSpanDescriptor(Array.Empty<TextBlockSpan>());
+                return new TextSpanDescriptor(new TextBlockSpan());
 
 
-            if (text.Contains('\r'))
-                text = text.Replace("\r", string.Empty);
-            
-            if (!text.Contains('\n'))
-            {
-                var textBlockSpan = new TextBlockSpan { Text = text };
-                AddItemToLastTextBlock(textBlockSpan);
-                return new TextSpanDescriptor(textBlockSpan);
-            }
-            
-            var items = text
-                .Split(new[] { '\n' }, StringSplitOptions.None)
-                .Select(x => new TextBlockSpan
-                {
-                    Text = x
-                })
-                .ToArray();
-
-            AddItemToLastTextBlock(items.First());
-            
-            foreach (var textBlockSpan in items.Skip(1))
-            {
-                TextBlocks.Add(new TextBlock
-                {   
-                    Items = new List<ITextBlockItem> { textBlockSpan }
-                });
-            }
-
-            return new TextSpanDescriptor(items);
+            var textSpan = new TextBlockSpan() { Text = text };
+            TextBlock.Items.Add(textSpan);
+            return new TextSpanDescriptor(textSpan);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -229,7 +158,7 @@ namespace QuestPDF.Fluent
         private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
         private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
         {
         {
             var textBlockItem = new TextBlockPageNumber();
             var textBlockItem = new TextBlockPageNumber();
-            AddItemToLastTextBlock(textBlockItem);
+            TextBlock.Items.Add(textBlockItem);
             
             
             return new TextPageNumberDescriptor(textBlockItem, x => textBlockItem.Source = context => x(pageNumber(context)));
             return new TextPageNumberDescriptor(textBlockItem, x => textBlockItem.Source = context => x(pageNumber(context)));
         }
         }
@@ -315,7 +244,7 @@ namespace QuestPDF.Fluent
                 throw new ArgumentException("Section name cannot be null or empty", nameof(sectionName));
                 throw new ArgumentException("Section name cannot be null or empty", nameof(sectionName));
 
 
             if (IsNullOrEmpty(text))
             if (IsNullOrEmpty(text))
-                return new TextSpanDescriptor(Array.Empty<TextBlockSpan>());
+                return new TextSpanDescriptor(new TextBlockSpan());
 
 
             var textBlockItem = new TextBlockSectionLink
             var textBlockItem = new TextBlockSectionLink
             {
             {
@@ -323,7 +252,7 @@ namespace QuestPDF.Fluent
                 SectionName = sectionName
                 SectionName = sectionName
             };
             };
 
 
-            AddItemToLastTextBlock(textBlockItem);
+            TextBlock.Items.Add(textBlockItem);
             return new TextSpanDescriptor(textBlockItem);
             return new TextSpanDescriptor(textBlockItem);
         }
         }
         
         
@@ -344,7 +273,7 @@ namespace QuestPDF.Fluent
                 throw new ArgumentException("Url cannot be null or empty", nameof(url));
                 throw new ArgumentException("Url cannot be null or empty", nameof(url));
 
 
             if (IsNullOrEmpty(text))
             if (IsNullOrEmpty(text))
-                return new TextSpanDescriptor(Array.Empty<TextBlockSpan>());
+                return new TextSpanDescriptor(new TextBlockSpan());
             
             
             var textBlockItem = new TextBlockHyperlink
             var textBlockItem = new TextBlockHyperlink
             {
             {
@@ -352,7 +281,7 @@ namespace QuestPDF.Fluent
                 Url = url
                 Url = url
             };
             };
 
 
-            AddItemToLastTextBlock(textBlockItem);
+            TextBlock.Items.Add(textBlockItem);
             return new TextSpanDescriptor(textBlockItem);
             return new TextSpanDescriptor(textBlockItem);
         }
         }
         
         
@@ -373,7 +302,7 @@ namespace QuestPDF.Fluent
         {
         {
             var container = new Container();
             var container = new Container();
                 
                 
-            AddItemToLastTextBlock(new TextBlockElement
+            TextBlock.Items.Add(new TextBlockElement
             {
             {
                 Element = container
                 Element = container
             });
             });
@@ -393,7 +322,7 @@ namespace QuestPDF.Fluent
             var container = new Container();
             var container = new Container();
             handler(container.AlignBottom().MinimalBox());
             handler(container.AlignBottom().MinimalBox());
                 
                 
-            AddItemToLastTextBlock(new TextBlockElement
+            TextBlock.Items.Add(new TextBlockElement
             {
             {
                 Element = container
                 Element = container
             });
             });
@@ -401,24 +330,12 @@ namespace QuestPDF.Fluent
         
         
         internal void Compose(IContainer container)
         internal void Compose(IContainer container)
         {
         {
-            TextBlocks.ToList().ForEach(x => x.Alignment ??= Alignment);
+            TextBlock.Alignment ??= Alignment;
             
             
             if (DefaultStyle != null)
             if (DefaultStyle != null)
                 container = container.DefaultTextStyle(DefaultStyle);
                 container = container.DefaultTextStyle(DefaultStyle);
 
 
-            if (TextBlocks.Count == 1)
-            {
-                container.Element(TextBlocks.First());
-                return;
-            }
-            
-            container.Column(column =>
-            {
-                column.Spacing(Spacing);
-
-                foreach (var textBlock in TextBlocks)
-                    column.Item().Element(textBlock);
-            }); 
+            container.Element(TextBlock);
         }
         }
     }
     }
     
     

+ 1 - 1
Source/QuestPDF/Fluent/TextStyleExtensions.cs

@@ -205,7 +205,7 @@ namespace QuestPDF.Fluent
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.fontFallback"]/*' />
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.fontFallback"]/*' />
         public static TextStyle Fallback(this TextStyle style, TextStyle? value = null)
         public static TextStyle Fallback(this TextStyle style, TextStyle? value = null)
         {
         {
-            return style.Mutate(TextStyleProperty.Fallback, value);
+            return style;
         }
         }
         
         
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.fontFallback"]/*' />
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.fontFallback"]/*' />

+ 14 - 0
Source/QuestPDF/Helpers/ColorParser.cs

@@ -0,0 +1,14 @@
+using System.Collections.Concurrent;
+using QuestPDF.Skia;
+
+namespace QuestPDF.Helpers;
+
+internal static class ColorParser
+{
+    private static readonly ConcurrentDictionary<string, uint> ColorCodes = new();
+    
+    internal static uint ColorToCode(this string color)
+    {
+        return ColorCodes.GetOrAdd(color, SkColor.Parse);
+    }
+}

+ 3 - 15
Source/QuestPDF/Helpers/ColorValidator.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Helpers
 namespace QuestPDF.Helpers
 {
 {
@@ -10,14 +11,14 @@ namespace QuestPDF.Helpers
             NativeDependencyCompatibilityChecker.Test();
             NativeDependencyCompatibilityChecker.Test();
         }
         }
         
         
-        private static readonly ConcurrentDictionary<string, bool> Colors = new();
+        private static readonly ConcurrentDictionary<string, bool> ColorsValidityCache = new();
         
         
         public static void Validate(string? color)
         public static void Validate(string? color)
         {
         {
             if (color == null)
             if (color == null)
                 throw new ArgumentException("Color value cannot be null");
                 throw new ArgumentException("Color value cannot be null");
             
             
-            var isValid = Colors.GetOrAdd(color, IsColorValid);
+            var isValid = ColorsValidityCache.GetOrAdd(color, x => SkColor.TryParse(x, out var _));
             
             
             if (isValid)
             if (isValid)
                 return;
                 return;
@@ -27,19 +28,6 @@ namespace QuestPDF.Helpers
                 "The following formats are supported: #RGB, #ARGB, #RRGGBB, #AARRGGBB. " +
                 "The following formats are supported: #RGB, #ARGB, #RRGGBB, #AARRGGBB. " +
                 "The hash sign is optional so the following formats are also valid: RGB, ARGB, RRGGBB, AARRGGBB. " +
                 "The hash sign is optional so the following formats are also valid: RGB, ARGB, RRGGBB, AARRGGBB. " +
                 "For example #FF8800 is a solid orange color, while #20CF is a barely visible aqua color.");
                 "For example #FF8800 is a solid orange color, while #20CF is a barely visible aqua color.");
-
-            static bool IsColorValid(string color)
-            {
-                try
-                {
-                    SKColor.Parse(color);
-                    return true;
-                }
-                catch
-                {
-                    return false;
-                }
-            }
         }
         }
     }
     }
 }
 }

+ 7 - 38
Source/QuestPDF/Helpers/Helpers.cs

@@ -5,6 +5,7 @@ using System.Linq.Expressions;
 using System.Reflection;
 using System.Reflection;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Helpers
 namespace QuestPDF.Helpers
 {
 {
@@ -65,17 +66,6 @@ namespace QuestPDF.Helpers
             return size.Width < 0f || size.Height < 0f;
             return size.Width < 0f || size.Height < 0f;
         }
         }
         
         
-        internal static SKEncodedImageFormat ToSkImageFormat(this ImageFormat format)
-        {
-            return format switch
-            {
-                ImageFormat.Jpeg => SKEncodedImageFormat.Jpeg,
-                ImageFormat.Png => SKEncodedImageFormat.Png,
-                ImageFormat.Webp=> SKEncodedImageFormat.Webp,
-                _ => throw new ArgumentOutOfRangeException(nameof(format), format, null)
-            };
-        }
-        
         internal static int ToQualityValue(this ImageCompressionQuality quality)
         internal static int ToQualityValue(this ImageCompressionQuality quality)
         {
         {
             return quality switch
             return quality switch
@@ -90,40 +80,19 @@ namespace QuestPDF.Helpers
             };
             };
         }
         }
         
         
-        internal static SKImage ScaleImage(this SKImage image, ImageSize targetResolution)
-        {
-            var imageInfo = new SKImageInfo(targetResolution.Width, targetResolution.Height, image.Info.ColorType, image.Info.AlphaType, image.Info.ColorSpace);
-            
-            using var bitmap = SKBitmap.FromImage(image);
-            using var resultBitmap = bitmap.Resize(imageInfo, SKFilterQuality.Medium);
-            return SKImage.FromBitmap(resultBitmap);
-        }
-        
-        internal static SKImage CompressImage(this SKImage image, ImageCompressionQuality compressionQuality)
+        internal static SkImage CompressImage(this SkImage image, ImageCompressionQuality compressionQuality)
         {
         {
-            var targetFormat = image.Info.IsOpaque 
-                ? SKEncodedImageFormat.Jpeg 
-                : SKEncodedImageFormat.Png;
-
-            if (targetFormat == SKEncodedImageFormat.Png)
-                compressionQuality = ImageCompressionQuality.Best;
-            
-            var data = image.Encode(targetFormat, compressionQuality.ToQualityValue());
-            return SKImage.FromEncodedData(data);
+            return image.ResizeAndCompress(image.Width, image.Height, compressionQuality.ToQualityValue());
         }
         }
 
 
-        internal static SKImage ResizeAndCompressImage(this SKImage image, ImageSize targetResolution, ImageCompressionQuality compressionQuality)
+        internal static SkImage ResizeAndCompressImage(this SkImage image, ImageSize targetResolution, ImageCompressionQuality compressionQuality)
         {
         {
-            if (image.Width == targetResolution.Width && image.Height == targetResolution.Height)
-                return CompressImage(image, compressionQuality);
-            
-            using var scaledImage = image.ScaleImage(targetResolution);
-            return CompressImage(scaledImage, compressionQuality);
+            return image.ResizeAndCompress(targetResolution.Width, targetResolution.Height, compressionQuality.ToQualityValue());
         }
         }
 
 
-        internal static SKImage GetImageWithSmallerSize(SKImage one, SKImage second)
+        internal static SkImage GetImageWithSmallerSize(SkImage one, SkImage second)
         {
         {
-            return one.EncodedData.Size < second.EncodedData.Size
+            return one.EncodedDataSize < second.EncodedDataSize
                 ? one
                 ? one
                 : second;
                 : second;
         }
         }

+ 3 - 115
Source/QuestPDF/Helpers/NativeDependencyCompatibilityChecker.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Exceptions;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Helpers
 namespace QuestPDF.Helpers
 {
 {
@@ -14,121 +15,8 @@ namespace QuestPDF.Helpers
             if (IsCompatibilityChecked)
             if (IsCompatibilityChecked)
                 return;
                 return;
             
             
-            var innerException = CheckIfExceptionIsThrownWhenLoadingNativeDependencies();
-
-            if (innerException == null)
-            {
-                IsCompatibilityChecked = true;
-                return;
-            }
-
-            var newLine = Environment.NewLine;
-            var paragraph = newLine + newLine;
-
-            var initializationExceptionMessage = 
-                $"The QuestPDF library has encountered an issue while loading one of its dependencies. " +
-                $"This type of error often occurs when the current runtime is missing necessary NuGet packages. {paragraph}" +
-                $"Please ensure the following NuGet packages are added to your primary/startup project and has matching versions:";
-
-            foreach (var nuget in GetRecommendedNugetDependencies())
-                initializationExceptionMessage += $"{newLine}- {nuget}";
-
-            initializationExceptionMessage += $"{paragraph}For a detailed understanding, please examine the inner exception and consult the official SkiaSharp library documentation.";
-            
-            throw new InitializationException(initializationExceptionMessage, innerException);
-        }
-
-        private static Exception? CheckIfExceptionIsThrownWhenLoadingNativeDependencies()
-        {
-            try
-            {
-                // accessing any SkiaSharp object triggers loading of SkiaSharp-related DLL dependency
-                using var paint = new SkiaSharp.SKPaint();
-
-                // accessing any HarfBuzzSharp object triggers loading of HarfBuzz-related DLL dependency
-                using var buffer = new HarfBuzzSharp.Buffer();
-
-                // everything loads and works correctly
-                return null;
-            }
-            catch (Exception exception)
-            {
-                return exception;
-            }
-        }
-        
-        private static IEnumerable<string> GetRecommendedNugetDependencies()
-        {
-            const string skiaSharp = "SkiaSharp.NativeAssets";
-            const string harfBuzzSharp = "HarfBuzzSharp.NativeAssets";
-            
-            #if NET5_0_OR_GREATER
-            
-            if (OperatingSystem.IsMacOS())
-            {
-                yield return $"{skiaSharp}.macOS";
-                yield return $"{harfBuzzSharp}.macOS";
-            }
-            else if (OperatingSystem.IsMacCatalyst())
-            {
-                yield return $"{skiaSharp}.MacCatalyst";
-                yield return $"{harfBuzzSharp}.MacCatalyst";
-            }
-            else if (OperatingSystem.IsIOS())
-            {
-                yield return $"{skiaSharp}.iOS";
-                yield return $"{harfBuzzSharp}.iOS";
-            }
-            else if (OperatingSystem.IsWatchOS())
-            {
-                yield return $"{skiaSharp}.watchOS";
-                yield return $"{harfBuzzSharp}.watchOS";
-            }
-            else if (OperatingSystem.IsTvOS())
-            {
-                yield return $"{skiaSharp}.tvOS";
-                yield return $"{harfBuzzSharp}.tvOS";
-            }
-            else if (OperatingSystem.IsAndroid())
-            {
-                yield return $"{skiaSharp}.Android";
-                yield return $"{harfBuzzSharp}.Android";
-            }
-            else if (OperatingSystem.IsBrowser())
-            {
-                yield return $"{skiaSharp}.WebAssembly";
-                yield return $"{harfBuzzSharp}.WebAssembly";
-            }
-            else if (OperatingSystem.IsLinux())
-            {
-                yield return $"{skiaSharp}.Linux.NoDependencies";
-                yield return $"{harfBuzzSharp}.Linux";
-            }
-            else if (OperatingSystem.IsWindows())
-            {
-                yield return $"{skiaSharp}.Win32";
-                yield return $"{harfBuzzSharp}.Win32";
-            }
-            
-            #else
-            
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
-            {
-                yield return $"{skiaSharp}.Linux.NoDependencies";
-                yield return $"{harfBuzzSharp}.Linux";
-            }
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
-            {
-                yield return $"{skiaSharp}.macOS";
-                yield return $"{harfBuzzSharp}.macOS";
-            }
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            {
-                yield return $"{skiaSharp}.Win32";
-                yield return $"{harfBuzzSharp}.Win32";
-            }
-            
-            #endif
+            IsCompatibilityChecked = true;
+            SkNativeDependencyCompatibilityChecker.Test();
         }
         }
     }
     }
 }
 }

+ 7 - 52
Source/QuestPDF/Helpers/Placeholders.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using System.Linq;
 using System.Linq;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Helpers
 namespace QuestPDF.Helpers
 {
 {
@@ -388,61 +389,15 @@ namespace QuestPDF.Helpers
         /// <returns>Random image encoded in the JPEG format.</returns>
         /// <returns>Random image encoded in the JPEG format.</returns>
         public static byte[] Image(ImageSize size)
         public static byte[] Image(ImageSize size)
         {
         {
-            // shuffle corner positions
-            var targetPositions = new[]
-            {
-                new SKPoint(0, 0),
-                new SKPoint(size.Width, 0),
-                new SKPoint(0, size.Height),
-                new SKPoint(size.Width, size.Height)
-            };
-            
-            var positions = targetPositions
-                .OrderBy(x => Random.Next())
-                .ToList();
-            
-            // rand and shuffle colors
             var colors = BackgroundColors
             var colors = BackgroundColors
-                .OrderBy(x => Random.Next())
-                .Take(4)
-                .Select(SKColor.Parse)
+                .OrderBy(_ => Random.Next())
+                .Take(2)
+                .Select(SkColor.Parse)
                 .ToArray();
                 .ToArray();
             
             
-            // create image with white background
-            var imageInfo = new SKImageInfo(size.Width, size.Height);
-            using var surface = SKSurface.Create(imageInfo);
-   
-            using var backgroundPaint = new SKPaint
-            {
-                Color = SKColors.White
-            };
-            
-            surface.Canvas.DrawRect(0, 0, size.Width, size.Height, backgroundPaint);
-
-            // draw gradient
-            SKShader GetForegroundShader(int index)
-            {
-                var radius = Math.Max(size.Width, size.Height);
-                var color = colors[index];
-                
-                return SKShader.CreateRadialGradient(
-                    positions[index], radius,
-                    new[] {color, color.WithAlpha(0)}, new[] {0, 1f},
-                    SKShaderTileMode.Decal);
-            }
-            
-            using var shaderPaint = new SKPaint
-            {
-                Shader = SKShader.CreateCompose(
-                    SKShader.CreateCompose(GetForegroundShader(0), GetForegroundShader(1)),
-                    SKShader.CreateCompose(GetForegroundShader(2), GetForegroundShader(3)))
-            };
-            
-            surface.Canvas.DrawRect(0, 0, size.Width, size.Height, shaderPaint);
-            
-            // return result as an image
-            surface.Canvas.Save();
-            return surface.Snapshot().Encode(SKEncodedImageFormat.Jpeg, 90).ToArray();
+            using var placeholderImage = SkImage.GeneratePlaceholder(size.Width, size.Height, colors[0], colors[1]);
+            using var imageData = placeholderImage.GetEncodedData();
+            return imageData.ToSpan().ToArray();
         }
         }
         
         
         #endregion
         #endregion

+ 17 - 3
Source/QuestPDF/Infrastructure/ICanvas.cs

@@ -1,13 +1,27 @@
+using QuestPDF.Skia;
+using QuestPDF.Skia.Text;
+
 namespace QuestPDF.Infrastructure
 namespace QuestPDF.Infrastructure
 {
 {
     internal interface ICanvas
     internal interface ICanvas
     {
     {
+        void Save();
+        void Restore();
+        
         void Translate(Position vector);
         void Translate(Position vector);
         
         
-        void DrawRectangle(Position vector, Size size, string color);
-        void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style);
-        void DrawImage(SKImage image, Position position, Size size);
+        void DrawFilledRectangle(Position vector, Size size, string color);
+        void DrawStrokeRectangle(Position vector, Size size, float strokeWidth, string color);
+        void DrawParagraph(SkParagraph paragraph);
+        void DrawImage(SkImage image, Size size);
+        void DrawPicture(SkPicture picture);
+        void DrawSvgPath(string path, uint color);
+        void DrawSvg(SkSvgImage svgImage, Size size);
 
 
+        void DrawOverflowArea(SkRect area);
+        void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace);
+        void ClipRectangle(SkRect clipArea);
+        
         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);

+ 18 - 28
Source/QuestPDF/Infrastructure/Image.cs

@@ -1,7 +1,9 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
+using QuestPDF.Skia;
 
 
 namespace QuestPDF.Infrastructure
 namespace QuestPDF.Infrastructure
 {
 {
@@ -27,12 +29,12 @@ namespace QuestPDF.Infrastructure
             NativeDependencyCompatibilityChecker.Test();
             NativeDependencyCompatibilityChecker.Test();
         }
         }
         
         
-        internal SKImage SkImage { get; }
+        internal SkImage SkImage { get; }
         internal ImageSize Size { get; }
         internal ImageSize Size { get; }
 
 
-        internal LinkedList<(GetImageVersionRequest request, SKImage image)> ScaledImageCache { get; } = new();
+        internal LinkedList<(GetImageVersionRequest request, SkImage image)> ScaledImageCache { get; } = new();
  
  
-        internal Image(SKImage image)
+        internal Image(SkImage image)
         {
         {
             SkImage = image;
             SkImage = image;
             Size = new ImageSize(image.Width, image.Height);
             Size = new ImageSize(image.Width, image.Height);
@@ -48,7 +50,7 @@ namespace QuestPDF.Infrastructure
         
         
         #region Scaling Image
         #region Scaling Image
 
 
-        internal SKImage GetVersionOfSize(GetImageVersionRequest request)
+        internal SkImage GetVersionOfSize(GetImageVersionRequest request)
         {
         {
             foreach (var cacheKey in ScaledImageCache)
             foreach (var cacheKey in ScaledImageCache)
             {
             {
@@ -64,10 +66,8 @@ namespace QuestPDF.Infrastructure
         #endregion
         #endregion
 
 
         #region public constructors
         #region public constructors
-
-        private const string CannotDecodeExceptionMessage = "Cannot decode the provided image.";
         
         
-        internal static Image FromSkImage(SKImage image)
+        internal static Image FromSkImage(SkImage image)
         {
         {
             return new Image(image);
             return new Image(image);
         }
         }
@@ -77,13 +77,10 @@ namespace QuestPDF.Infrastructure
         /// <a href="https://www.questpdf.com/api-reference/image.html">Learn more</a>
         /// <a href="https://www.questpdf.com/api-reference/image.html">Learn more</a>
         /// </summary>
         /// </summary>
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="image.remarks"]/*' />
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="image.remarks"]/*' />
-        public static Image FromBinaryData(byte[] imageData)
+        public static Image FromBinaryData(byte[] imageBytes)
         {
         {
-            var image = SKImage.FromEncodedData(imageData);
-            
-            if (image == null)
-                throw new DocumentComposeException(CannotDecodeExceptionMessage);
-            
+            using var imageData = SkData.FromBinary(imageBytes);
+            var image = SkImage.FromData(imageData);
             return new Image(image);
             return new Image(image);
         }
         }
 
 
@@ -94,15 +91,11 @@ namespace QuestPDF.Infrastructure
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="image.remarks"]/*' />
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="image.remarks"]/*' />
         public static Image FromFile(string filePath)
         public static Image FromFile(string filePath)
         {
         {
-            var image = SKImage.FromEncodedData(filePath);
-
-            if (image == null)
-            {
-                throw File.Exists(filePath) 
-                    ? new DocumentComposeException(CannotDecodeExceptionMessage)
-                    : new DocumentComposeException($"Cannot load provided image, file not found: ${filePath}");
-            }
+            if (File.Exists(filePath))
+                new DocumentComposeException($"Cannot load provided image, file not found: ${filePath}");
             
             
+            using var imageData = SkData.FromFile(filePath);
+            var image = SkImage.FromData(imageData);
             return new Image(image);
             return new Image(image);
         }
         }
 
 
@@ -111,13 +104,10 @@ namespace QuestPDF.Infrastructure
         /// <a href="https://www.questpdf.com/api-reference/image.html">Learn more</a>
         /// <a href="https://www.questpdf.com/api-reference/image.html">Learn more</a>
         /// </summary>
         /// </summary>
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="image.remarks"]/*' />
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="image.remarks"]/*' />
-        public static Image FromStream(Stream fileStream)
+        public static Image FromStream(Stream stream)
         {
         {
-            var image = SKImage.FromEncodedData(fileStream);
-            
-            if (image == null)
-                throw new DocumentComposeException(CannotDecodeExceptionMessage);
-            
+            using var imageData = SkData.FromStream(stream);
+            var image = SkImage.FromData(imageData);
             return new Image(image);
             return new Image(image);
         }
         }
 
 

+ 6 - 0
Source/QuestPDF/Infrastructure/ParagraphStyle.cs

@@ -0,0 +1,6 @@
+namespace QuestPDF.Infrastructure;
+
+internal class ParagraphStyle
+{
+    
+}

+ 51 - 0
Source/QuestPDF/Infrastructure/SvgImage.cs

@@ -0,0 +1,51 @@
+using System;
+using System.IO;
+using QuestPDF.Drawing.Exceptions;
+using QuestPDF.Skia;
+
+namespace QuestPDF.Infrastructure;
+
+/// <summary>
+/// Caches the SVG image in local memory for efficient reuse.
+/// </summary>
+/// <remarks>
+/// This class is thread safe.
+/// </remarks>
+public class SvgImage
+{
+    internal SkSvgImage SkSvgImage { get; }
+    
+    private SvgImage(string content)
+    {
+        SkSvgImage = new SkSvgImage(content);    
+    }
+
+    ~SvgImage()
+    {
+        SkSvgImage.Dispose();
+    }
+    
+    /// <summary>
+    /// Loads the SVG image from a file with specified path.
+    /// <a href="https://www.questpdf.com/api-reference/image.html">Learn more</a>
+    /// </summary>
+    /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="image.remarks"]/*' />
+    public static SvgImage FromFile(string filePath)
+    {
+        if (File.Exists(filePath))
+            new DocumentComposeException($"Cannot load provided image, file not found: ${filePath}");
+            
+        var svg = File.ReadAllText(filePath);
+        return new SvgImage(svg);
+    }
+
+    /// <summary>
+    /// Loads the SVG image from a stream.
+    /// <a href="https://www.questpdf.com/api-reference/image.html">Learn more</a>
+    /// </summary>
+    /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="image.remarks"]/*' />
+    public static SvgImage FromText(string svg)
+    {
+        return new SvgImage(svg);
+    }
+}

+ 5 - 32
Source/QuestPDF/Infrastructure/TextStyleManager.cs

@@ -17,7 +17,6 @@ namespace QuestPDF.Infrastructure
         HasStrikethrough,
         HasStrikethrough,
         HasUnderline,
         HasUnderline,
         WrapAnywhere,
         WrapAnywhere,
-        Fallback,
         Direction
         Direction
     }
     }
     
     
@@ -194,20 +193,7 @@ namespace QuestPDF.Infrastructure
                 
                 
                 return origin with { WrapAnywhere = castedValue };
                 return origin with { WrapAnywhere = castedValue };
             }
             }
-            
-            if (property == TextStyleProperty.Fallback)
-            {
-                if (!overrideValue && origin.Fallback != null)
-                    return origin;
 
 
-                var castedValue = (TextStyle?)value;
-                
-                if (origin.Fallback == castedValue)
-                    return origin;
-                
-                return origin with { Fallback = castedValue };
-            }
-            
             if (property == TextStyleProperty.Direction)
             if (property == TextStyleProperty.Direction)
             {
             {
                 if (!overrideValue && origin.Direction != null)
                 if (!overrideValue && origin.Direction != null)
@@ -227,31 +213,21 @@ namespace QuestPDF.Infrastructure
         internal static TextStyle ApplyInheritedStyle(this TextStyle style, TextStyle parent)
         internal static TextStyle ApplyInheritedStyle(this TextStyle style, TextStyle parent)
         {
         {
             var cacheKey = (style, parent);
             var cacheKey = (style, parent);
-            return TextStyleApplyInheritedCache.GetOrAdd(cacheKey, key => key.origin.ApplyStyleProperties(key.parent, overrideStyle: false, overrideFontFamily: false, applyFallback: true).UpdateFontFallback(overrideStyle: true));
+            return TextStyleApplyInheritedCache.GetOrAdd(cacheKey, key => key.origin.ApplyStyleProperties(key.parent, overrideStyle: false, overrideFontFamily: false));
         }
         }
         
         
         internal static TextStyle ApplyGlobalStyle(this TextStyle style)
         internal static TextStyle ApplyGlobalStyle(this TextStyle style)
         {
         {
-            return TextStyleApplyGlobalCache.GetOrAdd(style, key => key.ApplyStyleProperties(TextStyle.LibraryDefault, overrideStyle: false, overrideFontFamily: false, applyFallback: true).UpdateFontFallback(overrideStyle: false));
+            return TextStyleApplyGlobalCache.GetOrAdd(style, key => key.ApplyStyleProperties(TextStyle.LibraryDefault, overrideStyle: false, overrideFontFamily: false));
         }
         }
-        
-        private static TextStyle UpdateFontFallback(this TextStyle style, bool overrideStyle)
-        {
-            var targetFallbackStyle = style
-                ?.Fallback
-                ?.ApplyStyleProperties(style, overrideStyle: overrideStyle, overrideFontFamily: false, applyFallback: false)
-                ?.UpdateFontFallback(overrideStyle);
-            
-            return style.MutateStyle(TextStyleProperty.Fallback, targetFallbackStyle, overrideValue: true);
-        }
-        
+
         internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent)
         internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent)
         {
         {
             var cacheKey = (style, parent);
             var cacheKey = (style, parent);
-            return TextStyleOverrideCache.GetOrAdd(cacheKey, key => ApplyStyleProperties(key.origin, key.parent, overrideStyle: true, overrideFontFamily: true, applyFallback: true));
+            return TextStyleOverrideCache.GetOrAdd(cacheKey, key => ApplyStyleProperties(key.origin, key.parent, overrideStyle: true, overrideFontFamily: true));
         }
         }
         
         
-        private static TextStyle ApplyStyleProperties(this TextStyle style, TextStyle parent, bool overrideStyle, bool overrideFontFamily, bool applyFallback)
+        private static TextStyle ApplyStyleProperties(this TextStyle style, TextStyle parent, bool overrideStyle, bool overrideFontFamily)
         {
         {
             var result = style;
             var result = style;
             
             
@@ -270,9 +246,6 @@ namespace QuestPDF.Infrastructure
             result = MutateStyle(result, TextStyleProperty.HasUnderline, parent.HasUnderline, overrideStyle);
             result = MutateStyle(result, TextStyleProperty.HasUnderline, parent.HasUnderline, overrideStyle);
             result = MutateStyle(result, TextStyleProperty.WrapAnywhere, parent.WrapAnywhere, overrideStyle);
             result = MutateStyle(result, TextStyleProperty.WrapAnywhere, parent.WrapAnywhere, overrideStyle);
             result = MutateStyle(result, TextStyleProperty.Direction, parent.Direction, overrideStyle);
             result = MutateStyle(result, TextStyleProperty.Direction, parent.Direction, overrideStyle);
-            
-            if (applyFallback)
-                result = MutateStyle(result, TextStyleProperty.Fallback, parent.Fallback, overrideStyle);
 
 
             return result;
             return result;
         }
         }

+ 9 - 16
Source/QuestPDF/Previewer/ExceptionDocument.cs

@@ -7,6 +7,9 @@ namespace QuestPDF.Previewer
 {
 {
     internal sealed class ExceptionDocument : IDocument
     internal sealed class ExceptionDocument : IDocument
     {
     {
+        private const int ExceptionIconSvgSize = 24;
+        const string ExceptionIconSvgPath = "M23,12L20.56,14.78L20.9,18.46L17.29,19.28L15.4,22.46L12,21L8.6,22.47L6.71,19.29L3.1,18.47L3.44,14.78L1,12L3.44,9.21L3.1,5.53L6.71,4.72L8.6,1.54L12,3L15.4,1.54L17.29,4.72L20.9,5.54L20.56,9.22L23,12M20.33,12L18.5,9.89L18.74,7.1L16,6.5L14.58,4.07L12,5.18L9.42,4.07L8,6.5L5.26,7.09L5.5,9.88L3.67,12L5.5,14.1L5.26,16.9L8,17.5L9.42,19.93L12,18.81L14.58,19.92L16,17.5L18.74,16.89L18.5,14.1L20.33,12M11,15H13V17H11V15M11,7H13V13H11V7";
+        
         private Exception Exception { get; }
         private Exception Exception { get; }
     
     
         public ExceptionDocument(Exception exception)
         public ExceptionDocument(Exception exception)
@@ -40,25 +43,15 @@ namespace QuestPDF.Previewer
                     .Row(row =>
                     .Row(row =>
                     {
                     {
                         row.Spacing(15);
                         row.Spacing(15);
+
+                        var iconSize = 48;
                         
                         
                         row.AutoItem()
                         row.AutoItem()
                             .PaddingTop(15)
                             .PaddingTop(15)
-                            .Width(48)
-                            .AspectRatio(1)
-                            .Canvas((canvas, size) =>
-                            {
-                                const float iconSize = 24;
-                                using var iconPath = SKPath.ParseSvgPathData("M23,12L20.56,14.78L20.9,18.46L17.29,19.28L15.4,22.46L12,21L8.6,22.47L6.71,19.29L3.1,18.47L3.44,14.78L1,12L3.44,9.21L3.1,5.53L6.71,4.72L8.6,1.54L12,3L15.4,1.54L17.29,4.72L20.9,5.54L20.56,9.22L23,12M20.33,12L18.5,9.89L18.74,7.1L16,6.5L14.58,4.07L12,5.18L9.42,4.07L8,6.5L5.26,7.09L5.5,9.88L3.67,12L5.5,14.1L5.26,16.9L8,17.5L9.42,19.93L12,18.81L14.58,19.92L16,17.5L18.74,16.89L18.5,14.1L20.33,12M11,15H13V17H11V15M11,7H13V13H11V7");
-                                
-                                using var paint = new SKPaint()
-                                { 
-                                    Color = SKColors.Red,
-                                    IsAntialias = true
-                                };
-                                
-                                canvas.Scale(size.Width / iconSize);
-                                canvas.DrawPath(iconPath, paint);
-                            });
+                            .Width(iconSize)
+                            .Height(iconSize)
+                            .Scale(ExceptionIconSvgSize / iconSize)
+                            .SvgPath(ExceptionIconSvgPath, Colors.Red.Medium);
                         
                         
                         row.RelativeItem()
                         row.RelativeItem()
                             .Column(column =>
                             .Column(column =>

+ 7 - 0
Source/QuestPDF/QuestPDF.csproj

@@ -75,4 +75,11 @@
         <None Remove="Resources\DefaultFont\Lato-ThinItalic.ttf" />
         <None Remove="Resources\DefaultFont\Lato-ThinItalic.ttf" />
         <EmbeddedResource Include="Resources\DefaultFont\Lato-ThinItalic.ttf" />
         <EmbeddedResource Include="Resources\DefaultFont\Lato-ThinItalic.ttf" />
     </ItemGroup>
     </ItemGroup>
+
+    <ItemGroup>
+      <None Remove="skia_questpdf.dylib" />
+      <Content Include="skia_questpdf.dylib">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      </Content>
+    </ItemGroup>
 </Project>
 </Project>

+ 15 - 0
Source/QuestPDF/Resources/Documentation.xml

@@ -86,6 +86,21 @@
         <returns>Descriptor allowing adjustments to image attributes, such as scaling behavior, compression quality, and target DPI.</returns>
         <returns>Descriptor allowing adjustments to image attributes, such as scaling behavior, compression quality, and target DPI.</returns>
     </doc>
     </doc>
 
 
+    <!-- SVG -->
+
+    <doc for="svg.remarks">
+        <remarks>
+            Multiple strategies exist for the Image element to determine its final size.
+            By default, the image fills all the available width.
+            This behavior can be customized using options within the <see cref="Fluent.SvgImageDescriptor">descriptor</see> class.
+        </remarks>
+    </doc>
+
+    <doc for="svg.descriptor">
+        <returns>Descriptor allowing adjustments to the SVG image scaling behavior.</returns>
+    </doc>
+
+
     <!-- CONTENT DIRECTION -->
     <!-- CONTENT DIRECTION -->
     
     
     <doc for="contentDirection.ltr.remarks">
     <doc for="contentDirection.ltr.remarks">

+ 20 - 3
Source/QuestPDF/Skia/SkCanvas.cs

@@ -4,10 +4,10 @@ using QuestPDF.Skia.Text;
 
 
 namespace QuestPDF.Skia;
 namespace QuestPDF.Skia;
 
 
-internal class SkCanvas : IDisposable
+internal sealed class SkCanvas : IDisposable
 {
 {
-    internal IntPtr Instance;
-    internal bool DisposeNativeObject = true;
+    public IntPtr Instance { get; private set; }
+    private bool DisposeNativeObject = true;
 
 
     public SkCanvas(IntPtr instance, bool disposeNativeObject = true)
     public SkCanvas(IntPtr instance, bool disposeNativeObject = true)
     {
     {
@@ -119,6 +119,11 @@ internal class SkCanvas : IDisposable
         API.canvas_flush(Instance);
         API.canvas_flush(Instance);
     }
     }
     
     
+    public CanvasMatrix GetCurrentTotalMatrix()
+    {
+        return API.canvas_get_matrix(Instance);
+    }
+    
     ~SkCanvas()
     ~SkCanvas()
     {
     {
         Dispose();
         Dispose();
@@ -134,6 +139,15 @@ internal class SkCanvas : IDisposable
         
         
         Instance = IntPtr.Zero;
         Instance = IntPtr.Zero;
     }
     }
+    
+    public struct CanvasMatrix
+    {
+        public float ScaleX;
+        public float TranslateX;
+
+        public float ScaleY;
+        public float TranslateY;
+    }
 
 
     private static class API
     private static class API
     {
     {
@@ -199,5 +213,8 @@ internal class SkCanvas : IDisposable
         
         
         [DllImport(SkiaAPI.LibraryName)]
         [DllImport(SkiaAPI.LibraryName)]
         public static extern void canvas_flush(IntPtr canvas);
         public static extern void canvas_flush(IntPtr canvas);
+        
+        [DllImport(SkiaAPI.LibraryName)]
+        public static extern CanvasMatrix canvas_get_matrix(IntPtr canvas);
     }
     }
 }
 }

+ 98 - 0
Source/QuestPDF/Skia/SkColor.cs

@@ -0,0 +1,98 @@
+using System;
+using System.Globalization;
+
+namespace QuestPDF.Skia;
+
+internal static class SkColor
+{
+    public static uint Parse(string hexString)
+    {
+        if (!TryParse(hexString, out var color))
+        {
+            throw new ArgumentException(
+                $"The provided value '{color}' is not a valid hex color. " +
+                "The following formats are supported: #RGB, #ARGB, #RRGGBB, #AARRGGBB. " +
+                "The hash sign is optional so the following formats are also valid: RGB, ARGB, RRGGBB, AARRGGBB. " +
+                "For example #FF8800 is a solid orange color, while #20CF is a barely visible aqua color.",
+                nameof(color));
+        }
+        
+        return color;
+    }
+    
+    // inspired by: https://github.com/mono/SkiaSharp/blob/9274aeec807fd17eec2a3266ad4c2475c37d8a0c/binding/SkiaSharp/SKColor.cs#L123
+    public static bool TryParse(string hexString, out uint color)
+    {
+        color = 0;
+        
+        if (string.IsNullOrWhiteSpace(hexString))
+            return false;
+
+        // clean up string
+        var hexSpan = hexString.AsSpan().Trim().TrimStart('#');
+
+        var len = hexSpan.Length;
+
+        if (len == 3 || len == 4)
+        {
+            byte a;
+            
+            // parse [A]
+            if (len == 4)
+            {
+                if (!byte.TryParse(hexSpan.Slice(0, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out a))
+                    return false;
+
+                a = (byte)((a << 4) | a);
+            }
+            else
+            {
+                a = 255;
+            }
+
+            // parse RGB
+            if (!byte.TryParse(hexSpan.Slice(len - 3, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var r) ||
+                !byte.TryParse(hexSpan.Slice(len - 2, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var g) ||
+                !byte.TryParse(hexSpan.Slice(len - 1, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b))
+            {
+                return false;
+            }
+
+            r |= (byte)(r << 4);
+            g |= (byte)(g << 4);
+            b |= (byte)(b << 4);
+            
+            // success
+            color = (uint)((a << 24) | (r << 16) | (g << 8) | b);
+            return true;
+        }
+
+        if (len == 6 || len == 8)
+        {
+            // parse [AA]RRGGBB
+            if (!uint.TryParse(hexSpan, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var number))
+                return false;
+
+            // success
+            color = number;
+
+            // alpha was not provided, so use 255
+            if (len == 6)
+                color |= 0xFF000000;
+            
+            return true;
+        }
+
+        return false;
+    }
+    
+    public static uint ColorWithAlpha(this uint color, byte alpha)
+    {
+        return (color & 0x00FFFFFF) | ((uint)alpha << 24);
+    }
+    
+    public static string ColorToString(this uint color)
+    {
+        return $"#{color:X8}";
+    }
+}

+ 6 - 2
Source/QuestPDF/Skia/SkImage.cs

@@ -3,9 +3,9 @@ using System.Runtime.InteropServices;
 
 
 namespace QuestPDF.Skia;
 namespace QuestPDF.Skia;
 
 
-internal class SkImage : IDisposable
+internal sealed class SkImage : IDisposable
 {
 {
-    internal IntPtr Instance;
+    public IntPtr Instance { get; private set; }
 
 
     public readonly int Width;
     public readonly int Width;
     public readonly int Height;
     public readonly int Height;
@@ -28,6 +28,10 @@ internal class SkImage : IDisposable
     public static SkImage FromData(SkData data)
     public static SkImage FromData(SkData data)
     {
     {
         var instance = API.image_create_from_data(data.Instance);
         var instance = API.image_create_from_data(data.Instance);
+        
+        if (instance == IntPtr.Zero)
+            throw new Exception("Cannot decode the provided image.");
+        
         data.SetAsOwnedByAnotherObject();
         data.SetAsOwnedByAnotherObject();
         return new SkImage(instance);
         return new SkImage(instance);
     }
     }

+ 3 - 0
Source/QuestPDF/Skia/SkRect.cs

@@ -17,4 +17,7 @@ internal struct SkRect
         Right = right;
         Right = right;
         Bottom = bottom;
         Bottom = bottom;
     }
     }
+    
+    public float Width => Right - Left;
+    public float Height => Bottom - Top;
 }
 }

+ 21 - 0
Source/QuestPDF/Skia/SkSize.cs

@@ -0,0 +1,21 @@
+using System.Runtime.InteropServices;
+
+namespace QuestPDF.Skia;
+
+[StructLayout(LayoutKind.Sequential)]
+internal class SkSize
+{
+    public float Width;
+    public float Height;
+    
+    public SkSize()
+    {
+        
+    }
+    
+    public SkSize(float width, float height)
+    {
+        Width = width;
+        Height = height;
+    }
+}

+ 4 - 0
Source/QuestPDF/Skia/SkSvgImage.cs

@@ -14,6 +14,10 @@ internal class SkSvgImage : IDisposable
         data.SetAsOwnedByAnotherObject();
         data.SetAsOwnedByAnotherObject();
         
         
         Instance = API.svg_create(data.Instance);
         Instance = API.svg_create(data.Instance);
+        
+        if (Instance == IntPtr.Zero)
+            throw new Exception("Cannot decode the provided SVG image.");
+        
         API.svg_get_viewbox(Instance, out ViewBox);
         API.svg_get_viewbox(Instance, out ViewBox);
     }
     }
     
     

+ 34 - 7
Source/QuestPDF/Skia/Text/SkParagraph.cs

@@ -3,9 +3,9 @@ using System.Runtime.InteropServices;
 
 
 namespace QuestPDF.Skia.Text;
 namespace QuestPDF.Skia.Text;
 
 
-internal class SkParagraph : IDisposable
+internal sealed class SkParagraph : IDisposable
 {
 {
-    internal IntPtr Instance;
+    public IntPtr Instance { get; private set; }
     
     
     public SkParagraph(IntPtr instance)
     public SkParagraph(IntPtr instance)
     {
     {
@@ -17,12 +17,19 @@ internal class SkParagraph : IDisposable
         API.paragraph_plan_layout(Instance, availableWidth);
         API.paragraph_plan_layout(Instance, availableWidth);
     }
     }
     
     
-    public double[] GetLineHeights()
+    public SkSize[] GetLineMetrics()
     {
     {
-        API.paragraph_get_line_heights(Instance, out var array, out var arrayLength);
+        API.paragraph_get_line_metrics(Instance, out var array, out var arrayLength);
         
         
-        var managedArray = new double[arrayLength];
-        Marshal.Copy(array, managedArray,  0, arrayLength);
+        var managedArray = new SkSize[arrayLength];
+        
+        var size = Marshal.SizeOf<SkSize>();
+        
+        for (var i = 0; i < arrayLength; i++)
+        {
+            var ptr = IntPtr.Add(array, i * size);
+            managedArray[i] = Marshal.PtrToStructure<SkSize>(ptr);
+        }
 
 
         return managedArray;
         return managedArray;
     }
     }
@@ -54,6 +61,23 @@ internal class SkParagraph : IDisposable
         return managedArray;
         return managedArray;
     }
     }
     
     
+    public SkRect[] GetTextRangePositions(int rangeStart, int rangeEnd)
+    {
+        API.paragraph_get_text_range_positions(Instance, rangeStart, rangeEnd, out var array, out var arrayLength);
+        
+        var managedArray = new SkRect[arrayLength];
+        
+        var size = Marshal.SizeOf<SkRect>();
+        
+        for (var i = 0; i < arrayLength; i++)
+        {
+            var ptr = IntPtr.Add(array, i * size);
+            managedArray[i] = Marshal.PtrToStructure<SkRect>(ptr);
+        }
+
+        return managedArray;
+    }
+    
     ~SkParagraph()
     ~SkParagraph()
     {
     {
         Dispose();
         Dispose();
@@ -74,7 +98,7 @@ internal class SkParagraph : IDisposable
         public static extern void paragraph_plan_layout(IntPtr paragraph, float availableWidth);
         public static extern void paragraph_plan_layout(IntPtr paragraph, float availableWidth);
         
         
         [DllImport(SkiaAPI.LibraryName)]
         [DllImport(SkiaAPI.LibraryName)]
-        public static extern void paragraph_get_line_heights(IntPtr paragraph, out IntPtr array, out int arrayLength);
+        public static extern void paragraph_get_line_metrics(IntPtr paragraph, out IntPtr array, out int arrayLength);
         
         
         [DllImport(SkiaAPI.LibraryName)]
         [DllImport(SkiaAPI.LibraryName)]
         public static extern void paragraph_get_unresolved_codepoints(IntPtr paragraph, out IntPtr array, out int arrayLength);
         public static extern void paragraph_get_unresolved_codepoints(IntPtr paragraph, out IntPtr array, out int arrayLength);
@@ -82,6 +106,9 @@ internal class SkParagraph : IDisposable
         [DllImport(SkiaAPI.LibraryName)]
         [DllImport(SkiaAPI.LibraryName)]
         public static extern void paragraph_get_placeholder_positions(IntPtr paragraph, out IntPtr array, out int arrayLength);
         public static extern void paragraph_get_placeholder_positions(IntPtr paragraph, out IntPtr array, out int arrayLength);
         
         
+        [DllImport(SkiaAPI.LibraryName)]
+        public static extern void paragraph_get_text_range_positions(IntPtr paragraph, int rangeStart, int rangeEnd, out IntPtr array, out int arrayLength);
+        
         [DllImport(SkiaAPI.LibraryName)]
         [DllImport(SkiaAPI.LibraryName)]
         public static extern void paragraph_delete(IntPtr paragraph);
         public static extern void paragraph_delete(IntPtr paragraph);
     }
     }

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

@@ -62,7 +62,7 @@ internal class SkParagraphBuilder : IDisposable
 {
 {
     internal IntPtr Instance;
     internal IntPtr Instance;
     
     
-    public SkParagraphBuilder(IntPtr instance)
+    private SkParagraphBuilder(IntPtr instance)
     {
     {
         Instance = instance;
         Instance = instance;
     }
     }

+ 1 - 0
Source/QuestPDF/Skia/Text/SkTextStyle.cs

@@ -21,6 +21,7 @@ internal struct TextStyleConfiguration
     
     
     public float LetterSpacing;
     public float LetterSpacing;
     public float WordSpacing;
     public float WordSpacing;
+    public float BaselineOffset;
     
     
     public enum FontWeights
     public enum FontWeights
     {
     {

BIN
Source/QuestPDF/skia_questpdf.dylib