Browse Source

Merge branch 'text-shaping-new'

MarcinZiabek 3 years ago
parent
commit
44b3ff8d0d

+ 1 - 1
QuestPDF.Examples/TextBenchmark.cs

@@ -31,7 +31,7 @@ namespace QuestPDF.Examples
         {
         {
             var chapters = GetBookChapters().ToList();
             var chapters = GetBookChapters().ToList();
   
   
-            var results = PerformTest(16).ToList();
+            var results = PerformTest(128).ToList();
  
  
             Console.WriteLine($"Min: {results.Min():F}");
             Console.WriteLine($"Min: {results.Min():F}");
             Console.WriteLine($"Max: {results.Max():F}");
             Console.WriteLine($"Max: {results.Max():F}");

+ 26 - 11
QuestPDF.Examples/TextExamples.cs

@@ -11,12 +11,32 @@ namespace QuestPDF.Examples
 {
 {
     public class TextExamples
     public class TextExamples
     {
     {
+        [Test]
+        public void SimpleText()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(500, 100)
+                
+                .ProduceImages()
+                .ShowResults()
+                .Render(container =>
+                {
+                    container
+                        .Padding(5)
+                        .MinimalBox()
+                        .Border(1)
+                        .Padding(10)
+                        .Text(Placeholders.Paragraph());
+                });
+        }
+        
         [Test]
         [Test]
         public void SimpleTextBlock()
         public void SimpleTextBlock()
         {
         {
             RenderingTest
             RenderingTest
                 .Create()
                 .Create()
-                .PageSize(500, 300)
+                .PageSize(600, 300)
                 
                 
                 .ProduceImages()
                 .ProduceImages()
                 .ShowResults()
                 .ShowResults()
@@ -26,6 +46,7 @@ namespace QuestPDF.Examples
                         .Padding(5)
                         .Padding(5)
                         .MinimalBox()
                         .MinimalBox()
                         .Border(1)
                         .Border(1)
+                        .MaxWidth(300)
                         .Padding(10)
                         .Padding(10)
                         .Text(text =>
                         .Text(text =>
                         {
                         {
@@ -191,7 +212,7 @@ namespace QuestPDF.Examples
         {
         {
             RenderingTest
             RenderingTest
                 .Create()
                 .Create()
-                .PageSize(500, 300)
+                .PageSize(500, 500)
                 .ProduceImages()
                 .ProduceImages()
                 .ShowResults()
                 .ShowResults()
                 .Render(container =>
                 .Render(container =>
@@ -203,13 +224,7 @@ namespace QuestPDF.Examples
                         .Padding(10)
                         .Padding(10)
                         .Text(text =>
                         .Text(text =>
                         {
                         {
-                            text.ParagraphSpacing(10);
-    
-                            foreach (var i in Enumerable.Range(1, 3))
-                            {
-                                text.Span($"Paragraph {i}: ").SemiBold();
-                                text.Line(Placeholders.Paragraph());
-                            }
+                            text.Line(Placeholders.Paragraph());
                         });
                         });
                 });
                 });
         }
         }
@@ -388,7 +403,7 @@ namespace QuestPDF.Examples
                         .Padding(10)
                         .Padding(10)
                         .Text(text =>
                         .Text(text =>
                         {
                         {
-                            text.DefaultTextStyle(TextStyle.Default.FontSize(20));
+                            text.DefaultTextStyle(TextStyle.Default.FontSize(20).BackgroundColor(Colors.Red.Lighten4));
                             text.AlignLeft();
                             text.AlignLeft();
                             text.ParagraphSpacing(10);
                             text.ParagraphSpacing(10);
 
 
@@ -407,7 +422,7 @@ namespace QuestPDF.Examples
                             {
                             {
                                 text.Line($"{i}: {Placeholders.Paragraph()}");
                                 text.Line($"{i}: {Placeholders.Paragraph()}");
 
 
-                                text.Hyperlink("Please visit QuestPDF website", "https://www.questpdf.com");
+                                text.Hyperlink("Please visit QuestPDF website. ", "https://www.questpdf.com");
                                 
                                 
                                 text.Span("This is page number ");
                                 text.Span("This is page number ");
                                 text.CurrentPageNumber();
                                 text.CurrentPageNumber();

+ 79 - 0
QuestPDF.Examples/TextShapingTests.cs

@@ -0,0 +1,79 @@
+using NUnit.Framework;
+using QuestPDF.Examples.Engine;
+using QuestPDF.Fluent;
+using SkiaSharp;
+using SkiaSharp.HarfBuzz;
+
+namespace QuestPDF.Examples
+{
+    public class TextShapingTests
+    {
+        // [Test]
+        // public void ShapeText()
+        // {
+        //     using var textPaint = new SKPaint
+        //     {
+        //         Color = SKColors.Black,
+        //         Typeface = SKTypeface.CreateDefault(),
+        //         IsAntialias = true,
+        //         TextSize = 20
+        //     };
+        //
+        //     using var backgroundPaint = new SKPaint
+        //     {
+        //         Color = SKColors.LightGray
+        //     };
+        //
+        //     RenderingTest
+        //         .Create()
+        //         .PageSize(550, 250)
+        //         .ProduceImages()
+        //         .ShowResults()
+        //         .Render(container =>
+        //         {
+        //             //var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio ipsum, aliquam a neque a, lacinia vehicula lectus.";
+        //             //var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا الواجب والعمل سنتنازل غالباً ونرفض الشعور";
+        //
+        //             var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
+        //             var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
+        //
+        //             var text = arabic;
+        //             var metrics = textPaint.FontMetrics;
+        //
+        //             container
+        //                 .Padding(25)
+        //                 .Canvas((canvas, space) =>
+        //                 {
+        //                     canvas.Translate(0, 20);
+        //
+        //                     var width = MeasureText(text, textPaint);
+        //                     var widthReal = textPaint.MeasureText(text);
+        //                     canvas.DrawRect(0, metrics.Descent, width, metrics.Ascent - metrics.Descent, backgroundPaint);
+        //
+        //                     canvas.DrawShapedText(text, 0, 0, textPaint);
+        //
+        //                     canvas.Translate(0, 40);
+        //                     canvas.DrawText(text, 0, 0, textPaint);
+        //                 });
+        //         });
+        // }
+
+        [Test]
+        public void MeasureTest()
+        {
+            using var textPaint = new SKPaint
+            {
+                Color = SKColors.Black,
+                Typeface = SKTypeface.CreateDefault(),
+                IsAntialias = true,
+                TextSize = 20
+            };
+            
+            var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio ipsum, aliquam a neque a, lacinia vehicula lectus.";
+            var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
+            //            012345678901234567890123456789012345678901234567890123456
+            var shaper = new SKShaper(textPaint.Typeface);
+            var result = shaper.Shape(lorem, textPaint);
+        } 
+    }
+}

+ 2 - 0
QuestPDF.Previewer/CommunicationService.cs

@@ -1,5 +1,6 @@
 using System.Text.Json;
 using System.Text.Json;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -29,6 +30,7 @@ class CommunicationService
     {
     {
         var builder = WebApplication.CreateBuilder();
         var builder = WebApplication.CreateBuilder();
         builder.Services.AddLogging(x => x.ClearProviders());
         builder.Services.AddLogging(x => x.ClearProviders());
+        builder.WebHost.UseKestrel(options => options.Limits.MaxRequestBodySize = null);
         Application = builder.Build();
         Application = builder.Build();
 
 
         Application.MapGet("ping", HandlePing);
         Application.MapGet("ping", HandlePing);

+ 1 - 1
QuestPDF.Previewer/QuestPDF.Previewer.csproj

@@ -4,7 +4,7 @@
         <Authors>MarcinZiabek</Authors>
         <Authors>MarcinZiabek</Authors>
         <Company>CodeFlint</Company>
         <Company>CodeFlint</Company>
         <PackageId>QuestPDF.Previewer</PackageId>
         <PackageId>QuestPDF.Previewer</PackageId>
-        <Version>2022.5.0</Version>
+        <Version>2022.6.0</Version>
         <PackAsTool>true</PackAsTool>
         <PackAsTool>true</PackAsTool>
         <ToolCommandName>questpdf-previewer</ToolCommandName>
         <ToolCommandName>questpdf-previewer</ToolCommandName>
         <PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
         <PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>

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

@@ -1,4 +1,5 @@
 using System;
 using System;
+using QuestPDF.Drawing;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using SkiaSharp;
 using SkiaSharp;
 
 
@@ -10,7 +11,6 @@ namespace QuestPDF.UnitTests.TestEngine
         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<string, Position, TextStyle> DrawTextFunc { get; set; }
         public Action<Position, Size, string> DrawRectFunc { get; set; }
         public Action<Position, Size, string> DrawRectFunc { get; set; }
 
 
         public void Translate(Position vector) => TranslateFunc(vector);
         public void Translate(Position vector) => TranslateFunc(vector);
@@ -18,7 +18,7 @@ namespace QuestPDF.UnitTests.TestEngine
         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 DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color);
-        public void DrawText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style);
+        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 DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
 
 
         public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
         public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();

+ 2 - 1
QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using QuestPDF.Drawing;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.UnitTests.TestEngine.Operations;
 using QuestPDF.UnitTests.TestEngine.Operations;
 using SkiaSharp;
 using SkiaSharp;
@@ -15,7 +16,7 @@ namespace QuestPDF.UnitTests.TestEngine
         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 DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
-        public void DrawText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style));
+        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 DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
         
         
         public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
         public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();

+ 0 - 18
QuestPDF.UnitTests/TestEngine/TestPlan.cs

@@ -82,19 +82,6 @@ namespace QuestPDF.UnitTests.TestEngine
                     
                     
                     Assert.AreEqual(expected.Color, color, "Draw rectangle: color");
                     Assert.AreEqual(expected.Color, color, "Draw rectangle: color");
                 },
                 },
-                DrawTextFunc = (text, position, style) => 
-                {
-                    var expected = GetExpected<CanvasDrawTextOperation>();
-                    
-                    Assert.AreEqual(expected.Text, text);
-                    
-                    Assert.AreEqual(expected.Position.X, position.X, "Draw text: X");
-                    Assert.AreEqual(expected.Position.Y, position.Y, "Draw text: Y");
-                    
-                    Assert.AreEqual(expected.Style.Color, style.Color, "Draw text: color");
-                    Assert.AreEqual(expected.Style.FontFamily, style.FontFamily, "Draw text: font");
-                    Assert.AreEqual(expected.Style.Size, style.Size, "Draw text: size");
-                },
                 DrawImageFunc = (image, position, size) =>
                 DrawImageFunc = (image, position, size) =>
                 {
                 {
                     var expected = GetExpected<CanvasDrawImageOperation>();
                     var expected = GetExpected<CanvasDrawImageOperation>();
@@ -201,11 +188,6 @@ namespace QuestPDF.UnitTests.TestEngine
             return AddOperation(new CanvasDrawRectangleOperation(position, size, color));
             return AddOperation(new CanvasDrawRectangleOperation(position, size, color));
         }
         }
         
         
-        public TestPlan ExpectCanvasDrawText(string text, Position position, TextStyle style)
-        {
-            return AddOperation(new CanvasDrawTextOperation(text, position, style));
-        }
-        
         public TestPlan ExpectCanvasDrawImage(Position position, Size size)
         public TestPlan ExpectCanvasDrawImage(Position position, Size size)
         {
         {
             return AddOperation(new CanvasDrawImageOperation(position, size));
             return AddOperation(new CanvasDrawImageOperation(position, size));

+ 4 - 5
QuestPDF/Drawing/DocumentGenerator.cs

@@ -135,11 +135,10 @@ namespace QuestPDF.Drawing
                               $"In this case, please increase the value {nameof(DocumentMetadata)}.{nameof(DocumentMetadata.DocumentLayoutExceptionThreshold)} property configured in the {nameof(IDocument.GetMetadata)} method. " +
                               $"In this case, please increase the value {nameof(DocumentMetadata)}.{nameof(DocumentMetadata.DocumentLayoutExceptionThreshold)} property configured in the {nameof(IDocument.GetMetadata)} method. " +
                               $"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." +
                               $"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." +
                               $"Please analyze your documents structure to detect this element and fix its size constraints.";
                               $"Please analyze your documents structure to detect this element and fix its size constraints.";
-                
-                throw new DocumentLayoutException(message)
-                {
-                    ElementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode."
-                };
+
+                var elementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode.";
+
+                throw new DocumentLayoutException(message, elementTrace);
             }
             }
         }
         }
 
 

+ 1 - 11
QuestPDF/Drawing/Exceptions/DocumentComposeException.cs

@@ -4,17 +4,7 @@ namespace QuestPDF.Drawing.Exceptions
 {
 {
     public class DocumentComposeException : Exception
     public class DocumentComposeException : Exception
     {
     {
-        public DocumentComposeException()
-        {
-            
-        }
-
-        public DocumentComposeException(string message) : base(message)
-        {
-            
-        }
-
-        public DocumentComposeException(string message, Exception inner) : base(message, inner)
+        internal DocumentComposeException(string message) : base(message)
         {
         {
             
             
         }
         }

+ 1 - 11
QuestPDF/Drawing/Exceptions/DocumentDrawingException.cs

@@ -4,17 +4,7 @@ namespace QuestPDF.Drawing.Exceptions
 {
 {
     public class DocumentDrawingException : Exception
     public class DocumentDrawingException : Exception
     {
     {
-        public DocumentDrawingException()
-        {
-            
-        }
-
-        public DocumentDrawingException(string message) : base(message)
-        {
-            
-        }
-
-        public DocumentDrawingException(string message, Exception inner) : base(message, inner)
+        internal DocumentDrawingException(string message, Exception inner) : base(message, inner)
         {
         {
             
             
         }
         }

+ 3 - 13
QuestPDF/Drawing/Exceptions/DocumentLayoutException.cs

@@ -4,21 +4,11 @@ namespace QuestPDF.Drawing.Exceptions
 {
 {
     public class DocumentLayoutException : Exception
     public class DocumentLayoutException : Exception
     {
     {
-        public string ElementTrace { get; set; }
-        
-        public DocumentLayoutException()
-        {
-            
-        }
-
-        public DocumentLayoutException(string message) : base(message)
-        {
-            
-        }
+        public string? ElementTrace { get; }
 
 
-        public DocumentLayoutException(string message, Exception inner) : base(message, inner)
+        internal DocumentLayoutException(string message, string? elementTrace = null) : base(message)
         {
         {
-            
+            ElementTrace = elementTrace;
         }
         }
     }
     }
 }
 }

+ 20 - 0
QuestPDF/Drawing/Exceptions/InitializationException.cs

@@ -0,0 +1,20 @@
+using System;
+
+namespace QuestPDF.Drawing.Exceptions
+{
+    public class InitializationException : Exception
+    {
+        internal InitializationException(string documentType, Exception innerException) : base(CreateMessage(documentType), innerException)
+        {
+            
+        }
+
+        private static string CreateMessage(string documentType)
+        {
+            return $"Cannot create the {documentType} document using the SkiaSharp library. " +
+                   $"This exception usually means that, on your operating system where you run the application, SkiaSharp requires installing additional dependencies. " +
+                   $"Such dependencies are available as additional nuget packages, for example SkiaSharp.NativeAssets.Linux. " +
+                   $"Please refer to the SkiaSharp documentation for more details.";
+        }
+    }
+}

+ 42 - 4
QuestPDF/Drawing/FontManager.cs

@@ -2,9 +2,11 @@
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using HarfBuzzSharp;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using SkiaSharp;
 using SkiaSharp;
+using SkiaSharp.HarfBuzz;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
@@ -12,8 +14,11 @@ namespace QuestPDF.Drawing
     {
     {
         private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
         private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
         private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
         private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
-        private static ConcurrentDictionary<object, SKPaint> Paints = new();
-        private static ConcurrentDictionary<string, SKPaint> ColorPaint = new();
+        private static ConcurrentDictionary<object, SKPaint> FontPaints = new();
+        private static ConcurrentDictionary<string, SKPaint> ColorPaints = new();
+        private static ConcurrentDictionary<object, Font> ShaperFonts = new();
+        private static ConcurrentDictionary<object, SKFont> Fonts = new();
+        private static ConcurrentDictionary<object, TextShaper> TextShapers = new();
 
 
         private static void RegisterFontType(SKData fontData, string? customName = null)
         private static void RegisterFontType(SKData fontData, string? customName = null)
         {
         {
@@ -47,7 +52,7 @@ namespace QuestPDF.Drawing
 
 
         internal static SKPaint ColorToPaint(this string color)
         internal static SKPaint ColorToPaint(this string color)
         {
         {
-            return ColorPaint.GetOrAdd(color, Convert);
+            return ColorPaints.GetOrAdd(color, Convert);
 
 
             static SKPaint Convert(string color)
             static SKPaint Convert(string color)
             {
             {
@@ -61,7 +66,7 @@ namespace QuestPDF.Drawing
 
 
         internal static SKPaint ToPaint(this TextStyle style)
         internal static SKPaint ToPaint(this TextStyle style)
         {
         {
-            return Paints.GetOrAdd(style.PaintKey, key => Convert(style));
+            return FontPaints.GetOrAdd(style.PaintKey, key => Convert(style));
 
 
             static SKPaint Convert(TextStyle style)
             static SKPaint Convert(TextStyle style)
             {
             {
@@ -122,5 +127,38 @@ namespace QuestPDF.Drawing
         {
         {
             return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics);
             return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics);
         }
         }
+
+        internal static Font ToShaperFont(this TextStyle style)
+        {
+            return ShaperFonts.GetOrAdd(style.PaintKey, _ =>
+            {
+                var typeface = style.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.PaintKey, _ => new TextShaper(style));
+        }
+        
+        internal static SKFont FoFont(this TextStyle style)
+        {
+            return Fonts.GetOrAdd(style.PaintKey, _ => style.ToPaint().ToFont());
+        }
     }
     }
 }
 }

+ 1 - 1
QuestPDF/Drawing/FreeCanvas.cs

@@ -41,7 +41,7 @@ namespace QuestPDF.Drawing
             
             
         }
         }
 
 
-        public void DrawText(string text, Position position, TextStyle style)
+        public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style)
         {
         {
             
             
         }
         }

+ 17 - 3
QuestPDF/Drawing/PdfCanvas.cs

@@ -1,4 +1,6 @@
-using System.IO;
+using System;
+using System.IO;
+using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using SkiaSharp;
 using SkiaSharp;
 
 
@@ -7,11 +9,23 @@ namespace QuestPDF.Drawing
     internal class PdfCanvas : SkiaDocumentCanvasBase
     internal class PdfCanvas : SkiaDocumentCanvasBase
     {
     {
         public PdfCanvas(Stream stream, DocumentMetadata documentMetadata) 
         public PdfCanvas(Stream stream, DocumentMetadata documentMetadata) 
-            : base(SKDocument.CreatePdf(stream, MapMetadata(documentMetadata)))
+            : base(CreatePdf(stream, documentMetadata))
         {
         {
             
             
         }
         }
-        
+
+        private static SKDocument CreatePdf(Stream stream, DocumentMetadata documentMetadata)
+        {
+            try
+            {
+                return SKDocument.CreatePdf(stream, MapMetadata(documentMetadata));
+            }
+            catch (TypeInitializationException exception)
+            {
+                throw new InitializationException("PDF", exception);
+            }
+        }
+
         private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata)
         private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata)
         {
         {
             return new SKDocumentPdfMetadata
             return new SKDocumentPdfMetadata

+ 3 - 2
QuestPDF/Drawing/SkiaCanvasBase.cs

@@ -1,5 +1,6 @@
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using SkiaSharp;
 using SkiaSharp;
+using SkiaSharp.HarfBuzz;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
@@ -27,9 +28,9 @@ namespace QuestPDF.Drawing
             Canvas.DrawRect(vector.X, vector.Y, size.Width, size.Height, paint);
             Canvas.DrawRect(vector.X, vector.Y, size.Width, size.Height, paint);
         }
         }
 
 
-        public void DrawText(string text, Position vector, TextStyle style)
+        public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style)
         {
         {
-            Canvas.DrawText(text, vector.X, vector.Y, style.ToPaint());
+            Canvas.DrawText(skTextBlob, position.X, position.Y, style.ToPaint());
         }
         }
 
 
         public void DrawImage(SKImage image, Position vector, Size size)
         public void DrawImage(SKImage image, Position vector, Size size)

+ 155 - 0
QuestPDF/Drawing/TextShaper.cs

@@ -0,0 +1,155 @@
+using System;
+using HarfBuzzSharp;
+using QuestPDF.Infrastructure;
+using SkiaSharp;
+using Buffer = HarfBuzzSharp.Buffer;
+
+namespace QuestPDF.Drawing
+{
+    internal class TextShaper
+    {
+        public const int FontShapingScale = 512;
+     
+        private Font Font { get; }
+        private SKPaint Paint { get; }
+        
+        public TextShaper(TextStyle style)
+        {
+            Font = style.ToShaperFont();
+            Paint = style.ToPaint();
+        }
+
+        public TextShapingResult Shape(string text)
+        {
+            var buffer = new Buffer();
+            
+            PopulateBufferWithText(buffer, text);
+            buffer.GuessSegmentProperties();
+
+            Font.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;
+            
+            var glyphs = new ShapedGlyph[length];
+            
+            for (var i = 0; i < length; i++)
+            {
+                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(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 class TextShapingResult
+    {
+        public ShapedGlyph[] Glyphs { get; }
+
+        public TextShapingResult(ShapedGlyph[] glyphs)
+        {
+            Glyphs = glyphs;
+        }
+
+        public int BreakText(int startIndex, float maxWidth)
+        {
+            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;
+        }
+        
+        public float MeasureWidth(int startIndex, int endIndex)
+        {
+            if (Glyphs.Length == 0)
+                return 0;
+            
+            var start = Glyphs[startIndex];
+            var end = Glyphs[endIndex];
+
+            return end.Position.X - start.Position.X + end.Width;
+        }
+        
+        public DrawTextCommand? PositionText(int startIndex, int endIndex, TextStyle textStyle)
+        {
+            if (Glyphs.Length == 0)
+                return null;
+            
+            using var skTextBlobBuilder = new SKTextBlobBuilder();
+            
+            var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(textStyle.FoFont(), endIndex - startIndex + 1);
+            var glyphSpan = positionedRunBuffer.GetGlyphSpan();
+            var positionSpan = positionedRunBuffer.GetPositionSpan();
+                
+            for (var sourceIndex = startIndex; sourceIndex <= endIndex; sourceIndex++)
+            {
+                var runIndex = sourceIndex - startIndex;
+                
+                glyphSpan[runIndex] = Glyphs[sourceIndex].Codepoint;
+                positionSpan[runIndex] = Glyphs[sourceIndex].Position;
+            }
+            
+            return new DrawTextCommand
+            {
+                SkTextBlob = skTextBlobBuilder.Build(),
+                TextOffsetX = -Glyphs[startIndex].Position.X
+            };
+        }
+    }
+    
+}

+ 16 - 2
QuestPDF/Drawing/XpsCanvas.cs

@@ -1,4 +1,6 @@
-using System.IO;
+using System;
+using System.IO;
+using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using SkiaSharp;
 using SkiaSharp;
 
 
@@ -7,9 +9,21 @@ namespace QuestPDF.Drawing
     internal class XpsCanvas : SkiaDocumentCanvasBase
     internal class XpsCanvas : SkiaDocumentCanvasBase
     {
     {
         public XpsCanvas(Stream stream, DocumentMetadata documentMetadata) 
         public XpsCanvas(Stream stream, DocumentMetadata documentMetadata) 
-            : base(SKDocument.CreateXps(stream, documentMetadata.RasterDpi))
+            : base(CreateXps(stream, documentMetadata))
         {
         {
             
             
         }
         }
+        
+        private static SKDocument CreateXps(Stream stream, DocumentMetadata documentMetadata)
+        {
+            try
+            {
+                return SKDocument.CreateXps(stream, documentMetadata.RasterDpi);
+            }
+            catch (TypeInitializationException exception)
+            {
+                throw new InitializationException("XPS", exception);
+            }
+        }
     }
     }
 }
 }

+ 2 - 1
QuestPDF/Elements/Text/Calculation/TextDrawingRequest.cs

@@ -1,4 +1,5 @@
-using QuestPDF.Infrastructure;
+using QuestPDF.Drawing;
+using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Elements.Text.Calculation
 namespace QuestPDF.Elements.Text.Calculation
 {
 {

+ 1 - 0
QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using QuestPDF.Drawing;
 
 
 namespace QuestPDF.Elements.Text.Calculation
 namespace QuestPDF.Elements.Text.Calculation
 {
 {

+ 1 - 0
QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs

@@ -8,6 +8,7 @@ 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)
         public override TextMeasurementResult? Measure(TextMeasurementRequest request)
         {
         {

+ 57 - 41
QuestPDF/Elements/Text/Items/TextBlockSpan.cs

@@ -1,20 +1,24 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
 using QuestPDF.Elements.Text.Calculation;
 using QuestPDF.Elements.Text.Calculation;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using SkiaSharp;
+using SkiaSharp.HarfBuzz;
 using Size = QuestPDF.Infrastructure.Size;
 using Size = QuestPDF.Infrastructure.Size;
 
 
 namespace QuestPDF.Elements.Text.Items
 namespace QuestPDF.Elements.Text.Items
 {
 {
     internal class TextBlockSpan : ITextBlockItem
     internal class TextBlockSpan : ITextBlockItem
     {
     {
-        private const char Space = ' ';
-        
         public string Text { get; set; }
         public string Text { get; set; }
         public TextStyle Style { get; set; } = new();
         public TextStyle Style { get; set; } = new();
+        public TextShapingResult? TextShapingResult { get; set; }
 
 
         private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
         private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
+        protected virtual bool EnableTextCache => true; 
 
 
         public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
         public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
         {
         {
@@ -25,11 +29,17 @@ namespace QuestPDF.Elements.Text.Items
             
             
             return MeasureCache[cacheKey];
             return MeasureCache[cacheKey];
         }
         }
-        
+
         internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
         internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
         {
         {
+            if (!EnableTextCache)
+                TextShapingResult = null;
+            
+            TextShapingResult ??= Style.ToTextShaper().Shape(Text);
+            
             var paint = Style.ToPaint();
             var paint = Style.ToPaint();
             var fontMetrics = Style.ToFontMetrics();
             var fontMetrics = Style.ToFontMetrics();
+            var spaceCodepoint = paint.ToFont().Typeface.GetGlyphs(" ")[0];
 
 
             var startIndex = request.StartIndex;
             var startIndex = request.StartIndex;
             
             
@@ -37,11 +47,11 @@ namespace QuestPDF.Elements.Text.Items
             // ignore leading spaces
             // ignore leading spaces
             if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
             if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
             {
             {
-                while (startIndex < Text.Length && Text[startIndex] == Space)
+                while (startIndex < TextShapingResult.Glyphs.Length && Text[startIndex] == spaceCodepoint)
                     startIndex++;
                     startIndex++;
             }
             }
-            
-            if (Text.Length == 0 || startIndex == Text.Length)
+
+            if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length)
             {
             {
                 return new TextMeasurementResult
                 return new TextMeasurementResult
                 {
                 {
@@ -54,27 +64,19 @@ namespace QuestPDF.Elements.Text.Items
             }
             }
             
             
             // start breaking text from requested position
             // start breaking text from requested position
-            var text = Text.AsSpan().Slice(startIndex);
-            
-            var textLength = (int)paint.BreakText(text, request.AvailableWidth + Size.Epsilon);
+            var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth);
 
 
-            if (textLength <= 0)
+            if (endIndex < 0)
                 return null;
                 return null;
   
   
             // break text only on spaces
             // break text only on spaces
-            var wrappedTextLength = WrapText(text, textLength, request.IsFirstElementInLine);
+            var wrappedText = WrapText(startIndex, endIndex, request.IsFirstElementInLine);
 
 
-            if (wrappedTextLength == null)
+            if (wrappedText == null)
                 return null;
                 return null;
-
-            textLength = wrappedTextLength.Value.fragmentLength;
-
-            text = text.Slice(0, textLength);
-
-            var endIndex = startIndex + textLength;
-
+            
             // measure final text
             // measure final text
-            var width = paint.MeasureText(text);
+            var width = TextShapingResult.MeasureWidth(startIndex, wrappedText.Value.endIndex);
             
             
             return new TextMeasurementResult
             return new TextMeasurementResult
             {
             {
@@ -86,56 +88,70 @@ namespace QuestPDF.Elements.Text.Items
                 LineHeight = Style.LineHeight ?? 1,
                 LineHeight = Style.LineHeight ?? 1,
                 
                 
                 StartIndex = startIndex,
                 StartIndex = startIndex,
-                EndIndex = endIndex,
-                NextIndex = startIndex + wrappedTextLength.Value.nextIndex,
-                TotalIndex = Text.Length
+                EndIndex = wrappedText.Value.endIndex,
+                NextIndex = wrappedText.Value.nextIndex,
+                TotalIndex = TextShapingResult.Glyphs.Length - 1
             };
             };
         }
         }
         
         
-        // TODO: consider introduce text wrapping abstraction (basic, english-like, asian-like)
-        private (int fragmentLength, int nextIndex)? WrapText(ReadOnlySpan<char> text, int textLength, bool isFirstElementInLine)
+        // TODO: consider introducing text wrapping abstraction (basic, english-like, asian-like)
+        private (int endIndex, int nextIndex)? WrapText(int startIndex, int endIndex, bool isFirstElementInLine)
         {
         {
+            var spaceCodepoint = Style.ToPaint().ToFont().Typeface.GetGlyphs(" ")[0];
+            
             // textLength - length of the part of the text that fits in available width (creating a line)
             // textLength - length of the part of the text that fits in available width (creating a line)
-                
+
             // entire text fits, no need to wrap
             // entire text fits, no need to wrap
-            if (textLength == text.Length)
-                return (textLength, textLength + 1);
+            if (endIndex == TextShapingResult.Glyphs.Length - 1)
+                return (endIndex, endIndex);
 
 
             // breaking anywhere
             // breaking anywhere
             if (Style.WrapAnywhere ?? false)
             if (Style.WrapAnywhere ?? false)
-                return (textLength, textLength);
+                return (endIndex, endIndex + 1);
                 
                 
             // current line ends at word, next character is space, perfect place to wrap
             // current line ends at word, next character is space, perfect place to wrap
-            if (text[textLength - 1] != Space && text[textLength] == Space)
-                return (textLength, textLength + 1);
+            if (TextShapingResult.Glyphs[endIndex].Codepoint != spaceCodepoint && TextShapingResult.Glyphs[endIndex + 1].Codepoint == spaceCodepoint)
+                return (endIndex, endIndex + 2);
                 
                 
             // find last space within the available text to wrap
             // find last space within the available text to wrap
-            var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(Space);
+            var lastSpaceIndex = endIndex;
+
+            while (lastSpaceIndex >= startIndex)
+            {
+                if (TextShapingResult.Glyphs[lastSpaceIndex].Codepoint == spaceCodepoint)
+                    break;
+
+                lastSpaceIndex--;
+            }
 
 
             // text contains space that can be used to wrap
             // text contains space that can be used to wrap
-            if (lastSpaceIndex > 0)
-                return (lastSpaceIndex, lastSpaceIndex + 1);
+            if (lastSpaceIndex >= startIndex)
+                return (lastSpaceIndex - 1, lastSpaceIndex + 1);
                 
                 
             // there is no available space to wrap text
             // there is no available space to wrap text
             // if the item is first within the line, perform safe mode and chop the word
             // if the item is first within the line, perform safe mode and chop the word
             // otherwise, move the item into the next line
             // otherwise, move the item into the next line
-            return isFirstElementInLine ? (textLength, textLength) : null;
+            return isFirstElementInLine ? (endIndex, endIndex + 1) : null;
         }
         }
         
         
         public virtual void Draw(TextDrawingRequest request)
         public virtual void Draw(TextDrawingRequest request)
         {
         {
             var fontMetrics = Style.ToFontMetrics();
             var fontMetrics = Style.ToFontMetrics();
 
 
-            var glyphOffset = GetGlyphOffset();
-            var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
+            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);
             
             
-            request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
-            request.Canvas.DrawText(text, new Position(0, glyphOffset), Style);
+            if (textDrawingCommand.HasValue)
+                request.Canvas.DrawText(textDrawingCommand.Value.SkTextBlob, new Position(textDrawingCommand.Value.TextOffsetX, glyphOffsetY), Style);
 
 
             // draw underline
             // draw underline
             if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
             if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
             {
             {
-                var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffset;
+                var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffsetY;
                 DrawLine(fontMetrics.UnderlinePosition.Value + underlineOffset, fontMetrics.UnderlineThickness ?? 1);
                 DrawLine(fontMetrics.UnderlinePosition.Value + underlineOffset, fontMetrics.UnderlineThickness ?? 1);
             }
             }
             
             
@@ -145,7 +161,7 @@ namespace QuestPDF.Elements.Text.Items
                 var strikeoutThickness = fontMetrics.StrikeoutThickness ?? 1;
                 var strikeoutThickness = fontMetrics.StrikeoutThickness ?? 1;
                 strikeoutThickness *= Style.FontPosition == FontPosition.Normal ? 1f : 0.625f;
                 strikeoutThickness *= Style.FontPosition == FontPosition.Normal ? 1f : 0.625f;
                 
                 
-                DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffset, strikeoutThickness);
+                DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffsetY, strikeoutThickness);
             }
             }
             
             
             void DrawLine(float offset, float thickness)
             void DrawLine(float offset, float thickness)

+ 1 - 1
QuestPDF/Helpers/Placeholders.cs

@@ -7,7 +7,7 @@ namespace QuestPDF.Helpers
 {
 {
     public static class Placeholders
     public static class Placeholders
     {
     {
-        public static readonly Random Random = new();
+        public static readonly Random Random = new Random(0);
         
         
         #region Word Cache
         #region Word Cache
 
 

+ 2 - 1
QuestPDF/Infrastructure/ICanvas.cs

@@ -1,3 +1,4 @@
+using QuestPDF.Drawing;
 using SkiaSharp;
 using SkiaSharp;
 
 
 namespace QuestPDF.Infrastructure
 namespace QuestPDF.Infrastructure
@@ -7,7 +8,7 @@ namespace QuestPDF.Infrastructure
         void Translate(Position vector);
         void Translate(Position vector);
         
         
         void DrawRectangle(Position vector, Size size, string color);
         void DrawRectangle(Position vector, Size size, string color);
-        void DrawText(string text, Position position, TextStyle style);
+        void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style);
         void DrawImage(SKImage image, Position position, Size size);
         void DrawImage(SKImage image, Position position, Size size);
 
 
         void DrawHyperlink(string url, Size size);
         void DrawHyperlink(string url, Size size);

+ 1 - 1
QuestPDF/Previewer/PreviewerService.cs

@@ -94,7 +94,7 @@ namespace QuestPDF.Previewer
 
 
         private void CheckVersionCompatibility(Version version)
         private void CheckVersionCompatibility(Version version)
         {
         {
-            if (version.Major == 2022 && version.Minor == 5)
+            if (version.Major == 2022 && version.Minor == 6)
                 return;
                 return;
             
             
             throw new Exception($"Previewer version is not compatible. Possible solutions: " +
             throw new Exception($"Previewer version is not compatible. Possible solutions: " +

+ 3 - 4
QuestPDF/QuestPDF.csproj

@@ -1,10 +1,9 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
-
     <PropertyGroup>
     <PropertyGroup>
         <Authors>MarcinZiabek</Authors>
         <Authors>MarcinZiabek</Authors>
         <Company>CodeFlint</Company>
         <Company>CodeFlint</Company>
         <PackageId>QuestPDF</PackageId>
         <PackageId>QuestPDF</PackageId>
-        <Version>2022.5.0</Version>
+        <Version>2022.6.0-prerelease</Version>
         <PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
         <PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
         <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
         <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
         <LangVersion>9</LangVersion>
         <LangVersion>9</LangVersion>
@@ -24,7 +23,8 @@
     </PropertyGroup>
     </PropertyGroup>
 
 
     <ItemGroup>
     <ItemGroup>
-      <PackageReference Include="SkiaSharp" Version="2.80.*" />
+      <PackageReference Include="SkiaSharp" Version="2.80.3" />
+      <PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.3" />
     </ItemGroup>
     </ItemGroup>
 
 
     <ItemGroup>
     <ItemGroup>
@@ -45,5 +45,4 @@
         <PackagePath>\</PackagePath>
         <PackagePath>\</PackagePath>
       </None>
       </None>
     </ItemGroup>
     </ItemGroup>
-
 </Project>
 </Project>

+ 4 - 4
QuestPDF/Resources/ReleaseNotes.txt

@@ -1,4 +1,4 @@
-- Implemented the DynamicComponent element (useful when you want to generate dynamic and conditional content that is page aware, e.g. per-page totals),
-- Extended text rendering capabilities by adding subscript and superscript effects,
-- Improved table rendering performance,
-- Previewer tool stability fixes.
+Implemented support for the text shaping algorithm that fixes rendering more advanced languages such as Arabic. 
+Improved exception message when SkiaSharp throws the TypeInitializationException (when additional dependencies are needed).
+Fixed: a rare case when the Row.AutoItem does not calculate properly the width of its content.
+Fixed: the QuestPDF Previewer does not work with content-rich documents.