소스 검색

Merge branch 'main' of https://github.com/QuestPDF/library into element-proxy

 Conflicts:
	QuestPDF.ReportSample/QuestPDF.ReportSample.csproj
	QuestPDF.ReportSample/Tests.cs
	QuestPDF.UnitTests/ShowOnceTest.cs
	QuestPDF.UnitTests/StackTests.cs
	QuestPDF.UnitTests/TestEngine/Operations/ChildMeasureOperation.cs
	QuestPDF.UnitTests/TestEngine/SingleChildTests.cs
	QuestPDF.UnitTests/TestEngine/TestPlan.cs
	QuestPDF/Drawing/DocumentGenerator.cs
	QuestPDF/Drawing/TextRender.cs
	QuestPDF/Elements/PageBreak.cs
	QuestPDF/Elements/PageNumber.cs
	QuestPDF/Elements/Rotate.cs
	QuestPDF/Elements/Row.cs
	QuestPDF/Elements/Scale.cs
	QuestPDF/Elements/SimpleRotate.cs
	QuestPDF/Elements/Text.cs
	QuestPDF/Elements/Unconstrained.cs
	QuestPDF/Infrastructure/Element.cs
Marcin Ziąbek 4 년 전
부모
커밋
f3b75b43ac
94개의 변경된 파일3253개의 추가작업 그리고 646개의 파일을 삭제
  1. 1 0
      QuestPDF.Examples/BarCode.cs
  2. 32 0
      QuestPDF.Examples/BarcodeExamples.cs
  3. 41 18
      QuestPDF.Examples/ElementExamples.cs
  4. 51 8
      QuestPDF.Examples/Engine/RenderingTest.cs
  5. 4 2
      QuestPDF.Examples/Engine/SimpleDocument.cs
  6. 2 1
      QuestPDF.Examples/FrameExample.cs
  7. 4 2
      QuestPDF.Examples/Padding.cs
  8. 6 3
      QuestPDF.Examples/QuestPDF.Examples.csproj
  9. 228 0
      QuestPDF.Examples/TextBenchmark.cs
  10. 277 0
      QuestPDF.Examples/TextExamples.cs
  11. 285 0
      QuestPDF.Examples/quo-vadis.txt
  12. 2 2
      QuestPDF.ReportSample/DataSource.cs
  13. 2 2
      QuestPDF.ReportSample/Layouts/SectionTemplate.cs
  14. 15 5
      QuestPDF.ReportSample/Layouts/StandardReport.cs
  15. 1 1
      QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs
  16. 3 5
      QuestPDF.ReportSample/QuestPDF.ReportSample.csproj
  17. 1 1
      QuestPDF.ReportSample/Typography.cs
  18. 1 5
      QuestPDF.UnitTests/AlignmentTests.cs
  19. 0 6
      QuestPDF.UnitTests/AspectRatioTests.cs
  20. 2 2
      QuestPDF.UnitTests/BackgroundTests.cs
  21. 2 2
      QuestPDF.UnitTests/BorderTests.cs
  22. 56 0
      QuestPDF.UnitTests/BoxTests.cs
  23. 0 6
      QuestPDF.UnitTests/ConstrainedTests.cs
  24. 0 10
      QuestPDF.UnitTests/DebugTests.cs
  25. 3 0
      QuestPDF.UnitTests/EnsureSpaceTests.cs
  26. 3 6
      QuestPDF.UnitTests/ExtendTests.cs
  27. 15 0
      QuestPDF.UnitTests/ExternalLinkTests.cs
  28. 27 27
      QuestPDF.UnitTests/GridTests.cs
  29. 0 30
      QuestPDF.UnitTests/Helpers.cs
  30. 0 6
      QuestPDF.UnitTests/ImageTests.cs
  31. 15 0
      QuestPDF.UnitTests/InternalLinkTests.cs
  32. 15 0
      QuestPDF.UnitTests/InternalLocationTests.cs
  33. 131 0
      QuestPDF.UnitTests/LayersTests.cs
  34. 0 6
      QuestPDF.UnitTests/PaddingTests.cs
  35. 0 10
      QuestPDF.UnitTests/PageNumberTests.cs
  36. 4 5
      QuestPDF.UnitTests/QuestPDF.UnitTests.csproj
  37. 31 0
      QuestPDF.UnitTests/RotateTests.cs
  38. 154 3
      QuestPDF.UnitTests/RowTests.cs
  39. 152 0
      QuestPDF.UnitTests/ScaleTests.cs
  40. 3 0
      QuestPDF.UnitTests/ShowEntireTests.cs
  41. 1 27
      QuestPDF.UnitTests/ShowOnceTest.cs
  42. 162 0
      QuestPDF.UnitTests/SimpleRotateTests.cs
  43. 76 2
      QuestPDF.UnitTests/StackTests.cs
  44. 9 25
      QuestPDF.UnitTests/TestEngine/MockCanvas.cs
  45. 25 0
      QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs
  46. 2 2
      QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawImageOperation.cs
  47. 2 2
      QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawRectangleOperation.cs
  48. 2 2
      QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawTextOperation.cs
  49. 12 0
      QuestPDF.UnitTests/TestEngine/Operations/CanvasRotateOperation.cs
  50. 14 0
      QuestPDF.UnitTests/TestEngine/Operations/CanvasScaleOperation.cs
  51. 2 2
      QuestPDF.UnitTests/TestEngine/Operations/CanvasTranslateOperation.cs
  52. 2 2
      QuestPDF.UnitTests/TestEngine/Operations/ChildDrawOperation.cs
  53. 2 2
      QuestPDF.UnitTests/TestEngine/Operations/ElementMeasureOperation.cs
  54. 68 0
      QuestPDF.UnitTests/TestEngine/SimpleContainerTests.cs
  55. 0 31
      QuestPDF.UnitTests/TestEngine/SingleChildTests.cs
  56. 84 37
      QuestPDF.UnitTests/TestEngine/TestPlan.cs
  57. 22 0
      QuestPDF.UnitTests/TestsBase.cs
  58. 0 10
      QuestPDF.UnitTests/TextTests.cs
  59. 32 0
      QuestPDF.UnitTests/TranslateTests.cs
  60. 100 0
      QuestPDF.UnitTests/UnconstrainedTests.cs
  61. 4 17
      QuestPDF/Drawing/FontManager.cs
  62. 10 0
      QuestPDF/Drawing/FreeCanvas.cs
  63. 10 0
      QuestPDF/Drawing/SkiaCanvasBase.cs
  64. 13 0
      QuestPDF/Drawing/TextRender.cs
  65. 1 1
      QuestPDF/Elements/Grid.cs
  66. 0 52
      QuestPDF/Elements/PageNumber.cs
  67. 3 4
      QuestPDF/Elements/Placeholder.cs
  68. 3 10
      QuestPDF/Elements/Rotate.cs
  69. 0 1
      QuestPDF/Elements/Row.cs
  70. 9 14
      QuestPDF/Elements/Scale.cs
  71. 10 14
      QuestPDF/Elements/SimpleRotate.cs
  72. 1 1
      QuestPDF/Elements/Stack.cs
  73. 0 116
      QuestPDF/Elements/Text.cs
  74. 16 0
      QuestPDF/Elements/Text/Calculation/TextDrawingRequest.cs
  75. 47 0
      QuestPDF/Elements/Text/Calculation/TextLine.cs
  76. 10 0
      QuestPDF/Elements/Text/Calculation/TextLineElement.cs
  77. 14 0
      QuestPDF/Elements/Text/Calculation/TextMeasurementRequest.cs
  78. 22 0
      QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs
  79. 10 0
      QuestPDF/Elements/Text/Items/ITextBlockItem.cs
  80. 48 0
      QuestPDF/Elements/Text/Items/TextBlockElement.cs
  81. 35 0
      QuestPDF/Elements/Text/Items/TextBlockExternalLink.cs
  82. 35 0
      QuestPDF/Elements/Text/Items/TextBlockInternalLink.cs
  83. 36 0
      QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs
  84. 131 0
      QuestPDF/Elements/Text/Items/TextBlockSpan.cs
  85. 199 0
      QuestPDF/Elements/Text/TextBlock.cs
  86. 3 6
      QuestPDF/Elements/Translate.cs
  87. 0 27
      QuestPDF/Fluent/ElementExtensions.cs
  88. 188 0
      QuestPDF/Fluent/TextExtensions.cs
  89. 10 19
      QuestPDF/Fluent/TextStyleExtensions.cs
  90. 19 19
      QuestPDF/Helpers/Placeholders.cs
  91. 3 0
      QuestPDF/Infrastructure/ICanvas.cs
  92. 7 2
      QuestPDF/Infrastructure/TextStyle.cs
  93. 4 4
      QuestPDF/QuestPDF.csproj
  94. 166 21
      readme.md

+ 1 - 0
QuestPDF.Examples/BarCode.cs

@@ -19,6 +19,7 @@ namespace QuestPDF.Examples
                 .Create()
                 .PageSize(400, 100)
                 .FileName()
+                .ShowResults()
                 .Render(container =>
                 {
                     container

+ 32 - 0
QuestPDF.Examples/BarcodeExamples.cs

@@ -0,0 +1,32 @@
+using NUnit.Framework;
+using QuestPDF.Examples.Engine;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Examples
+{
+    [TestFixture]
+    public class BarcodeExamples
+    {
+        [Test]
+        public void Barcode()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(300, 300)
+                .FileName()
+                .Render(container =>
+                {
+                    container
+                        .Background("#FFF")
+                        .Padding(25)
+                        .Stack(stack =>
+                        {
+                            stack.Item().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Barcode Example");
+                            stack.Item().Border(1).Padding(5).AlignCenter().Text("*123456789*", TextStyle.Default.FontType("CarolinaBar-Demo-25E2").Size(20));
+                        });
+                });
+        }
+    }
+}

+ 41 - 18
QuestPDF.Examples/ElementExamples.cs

@@ -1,4 +1,3 @@
-using System;
 using System.Linq;
 using NUnit.Framework;
 using QuestPDF.Examples.Engine;
@@ -296,7 +295,7 @@ namespace QuestPDF.Examples
                             layers
                                 .Layer()
                                 .AlignBottom()
-                                .PageNumber("Page {pdf:currentPage}", TextStyle.Default.Size(16).Color(Colors.Green.Medium));
+                                .Text(text => text.CurrentPageNumber(TextStyle.Default.Size(16).Color(Colors.Green.Medium)));
                         });
                 });
         }
@@ -502,25 +501,49 @@ namespace QuestPDF.Examples
         {
             RenderingTest
                 .Create()
-                .PageSize(400, 250)
+                .PageSize(300, 175)
                 .FileName()
                 .Render(container =>
                 {
                     container
-                        .Padding(25)
-                        .Stack(stack =>
+                        .Background(Colors.White)
+                        .Padding(10)
+                        .Decoration(decoration =>
                         {
-                            var scales = new[] { 0.75f, 1f, 1.25f, 1.5f };
+                            var headerFontStyle = TextStyle
+                                .Default
+                                .Size(20)
+                                .Color(Colors.Blue.Darken2)
+                                .SemiBold();
+    
+                            decoration
+                                .Header()
+                                .PaddingBottom(10)
+                                .Text("Example: scale component", headerFontStyle);
+    
+                            decoration
+                                .Content()
+                                .Stack(stack =>
+                                {
+                                    var scales = new[] { 0.8f, 0.9f, 1.1f, 1.2f };
 
-                            foreach (var scale in scales)
-                            {
-                                stack
-                                    .Item()
-                                    .Border(1)
-                                    .Scale(scale)
-                                    .Padding(10)
-                                    .Text($"Content with {scale} scale.", TextStyle.Default.Size(20));
-                            }
+                                    foreach (var scale in scales)
+                                    {
+                                        var fontColor = scale <= 1f
+                                            ? Colors.Red.Lighten4
+                                            : Colors.Green.Lighten4;
+
+                                        var fontStyle = TextStyle.Default.Size(16);
+                
+                                        stack
+                                            .Item()
+                                            .Border(1)
+                                            .Background(fontColor)
+                                            .Scale(scale)
+                                            .Padding(5)
+                                            .Text($"Content with {scale} scale.", fontStyle);
+                                    }
+                                });
                         });
                 });
         }
@@ -558,7 +581,7 @@ namespace QuestPDF.Examples
         {
             RenderingTest
                 .Create()
-                .PageSize(350, 350)
+                .PageSize(650, 450)
                 .FileName()
                 .Render(container =>
                 {
@@ -572,8 +595,8 @@ namespace QuestPDF.Examples
                             foreach (var turns in Enumerable.Range(0, 4))
                             {
                                 grid.Item()
-                                    .Width(150)
-                                    .Height(150)
+                                    .Width(300)
+                                    .Height(200)
                                     .Background(Colors.Grey.Lighten2)
                                     .Padding(10)
                                     .Element(element =>

+ 51 - 8
QuestPDF.Examples/Engine/RenderingTest.cs

@@ -1,18 +1,24 @@
 using System;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
-using QuestPDF.Drawing;
 using QuestPDF.Elements;
 using QuestPDF.Fluent;
-using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Examples.Engine
 {
+    public enum RenderingTestResult
+    {
+        Pdf,
+        Images
+    }
+    
     public class RenderingTest
     {
         private string FileNamePrefix = "test";
         private Size Size { get; set; }
+        private bool ShowResult { get; set; }
+        private RenderingTestResult ResultType { get; set; } = RenderingTestResult.Images;
         
         private RenderingTest()
         {
@@ -30,9 +36,32 @@ namespace QuestPDF.Examples.Engine
             return this;
         }
         
+        public RenderingTest PageSize(Size size)
+        {
+            Size = size;
+            return this;
+        }
+        
         public RenderingTest PageSize(int width, int height)
         {
-            Size = new Size(width, height);
+            return PageSize(new Size(width, height));
+        }
+
+        public RenderingTest ProducePdf()
+        {
+            ResultType = RenderingTestResult.Pdf;
+            return this;
+        }
+        
+        public RenderingTest ProduceImages()
+        {
+            ResultType = RenderingTestResult.Images;
+            return this;
+        }
+
+        public RenderingTest ShowResults()
+        {
+            ShowResult = true;
             return this;
         }
         
@@ -40,13 +69,27 @@ namespace QuestPDF.Examples.Engine
         {
             var container = new Container();
             content(container);
-            
-            Func<int, string> fileNameSchema = i => $"{FileNamePrefix}-${i}.png";
 
-            var document = new SimpleDocument(container, Size);
-            document.GenerateImages(fileNameSchema);
+            var maxPages = ResultType == RenderingTestResult.Pdf ? 1000 : 10;
+            var document = new SimpleDocument(container, Size, maxPages);
+
+            if (ResultType == RenderingTestResult.Images)
+            {
+                Func<int, string> fileNameSchema = i => $"{FileNamePrefix}-${i}.png";
+                document.GenerateImages(fileNameSchema);
+                
+                if (ShowResult)
+                    Process.Start("explorer", fileNameSchema(0));
+            }
 
-            Process.Start("explorer", fileNameSchema(0));
+            if (ResultType == RenderingTestResult.Pdf)
+            {
+                var fileName = $"{FileNamePrefix}.pdf";
+                document.GeneratePdf(fileName);
+                
+                if (ShowResult)
+                    Process.Start("explorer", fileName);
+            }
         }
     }
 }

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

@@ -12,11 +12,13 @@ namespace QuestPDF.Examples.Engine
         
         private IContainer Container { get; }
         private Size Size { get; }
+        private int MaxPages { get; }
 
-        public SimpleDocument(IContainer container, Size size)
+        public SimpleDocument(IContainer container, Size size, int maxPages)
         {
             Container = container;
             Size = size;
+            MaxPages = maxPages;
         }
         
         public DocumentMetadata GetMetadata()
@@ -24,7 +26,7 @@ namespace QuestPDF.Examples.Engine
             return new DocumentMetadata()
             {
                 RasterDpi = PageSizes.PointsPerInch * ImageScalingFactor,
-                DocumentLayoutExceptionThreshold = 10
+                DocumentLayoutExceptionThreshold = MaxPages
             };
         }
         

+ 2 - 1
QuestPDF.Examples/FrameExample.cs

@@ -23,12 +23,13 @@ namespace QuestPDF.Examples
     public class FrameExample
     {
         [Test]
-        public void Frame(IContainer container)
+        public void Frame()
         {
             RenderingTest
                 .Create()
                 .PageSize(550, 400)
                 .FileName()
+                .ShowResults()
                 .Render(container =>
                 {
                     container

+ 4 - 2
QuestPDF.Examples/Padding.cs

@@ -12,8 +12,9 @@ namespace QuestPDF.Examples
         {
             RenderingTest
                 .Create()
-                .PageSize(200, 150)
+                .PageSize(300, 300)
                 .FileName()
+                .ShowResults()
                 .Render(container =>
                 {
                     container
@@ -50,7 +51,8 @@ namespace QuestPDF.Examples
                 
                         .Background("FFF")
                         .Padding(5)
-                        .Text("Sample text", TextStyle.Default.FontType("Segoe UI emoji").Alignment(HorizontalAlignment.Center));
+                        .AlignCenter()
+                        .Text("Sample text", TextStyle.Default.FontType("Segoe UI emoji"));
                 });
         }
         

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

@@ -6,10 +6,10 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="nunit" Version="3.12.0" />
-        <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
+        <PackageReference Include="nunit" Version="3.13.2" />
+        <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
-        <PackageReference Include="SkiaSharp" Version="2.80.2" />
+        <PackageReference Include="SkiaSharp" Version="2.80.3" />
     </ItemGroup>
 
     <ItemGroup>
@@ -17,6 +17,9 @@
     </ItemGroup>
 
     <ItemGroup>
+      <None Update="quo-vadis.txt">
+        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+      </None>
       <None Update="LibreBarcode39-Regular.ttf">
         <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       </None>

+ 228 - 0
QuestPDF.Examples/TextBenchmark.cs

@@ -0,0 +1,228 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using NUnit.Framework;
+using QuestPDF.Examples.Engine;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Examples
+{
+    public class TextBenchmark
+    {
+        [Test]
+        public void Generate()
+        {
+            var chapters = GetBookChapters().ToList();
+            
+            RenderingTest
+                .Create()
+                .PageSize(PageSizes.A4)
+                .FileName()
+                .ProducePdf()
+                .ShowResults()
+                .Render(x => ComposeBook(x, chapters));
+        }
+        
+        [Test]
+        public void Benchmark()
+        {
+            var chapters = GetBookChapters().ToList();
+  
+            var results = PerformTest(16).ToList();
+ 
+            Console.WriteLine($"Min: {results.Min():F}");
+            Console.WriteLine($"Max: {results.Max():F}");
+            Console.WriteLine($"Avg: {results.Average():F}");
+            
+            void GenerateDocument()
+            {
+                RenderingTest
+                    .Create()
+                    .PageSize(PageSizes.A4)
+                    .FileName()
+                    .ProducePdf()
+                    .Render(x => ComposeBook(x, chapters));
+            }
+
+            IEnumerable<float> PerformTest(int attempts)
+            {
+                foreach (var i in Enumerable.Range(0, attempts))
+                {
+                    var timer = new Stopwatch();
+                
+                    timer.Start();
+                    GenerateDocument();
+                    timer.Stop();
+
+                    Console.WriteLine($"Attempt {i}: {timer.ElapsedMilliseconds:F}");
+                    yield return timer.ElapsedMilliseconds;
+                }
+            }
+        }
+
+        class BookChapter
+        {
+            public string Title { get; set; }
+            public string Content { get; set; }
+        }
+        
+        private static IEnumerable<BookChapter> GetBookChapters()
+        {
+            var book = File.ReadAllLines("quo-vadis.txt");
+            
+            var chapterPointers = book
+                .Select((line, index) => new
+                {
+                    LineNumber = index,
+                    Text = line
+                })
+                .Where(x => x.Text.Length < 50 && x.Text.Contains("Rozdział") || x.Text.Contains("-----"))
+                .Select(x => x.LineNumber)
+                .ToList();
+
+            foreach (var index in Enumerable.Range(0, chapterPointers.Count - 1))
+            {
+                var chapter = chapterPointers[index];
+                    
+                var title = book[chapter];
+                    
+                var lineFrom = chapterPointers[index];
+                var lineTo = chapterPointers[index + 1] - 1;
+
+                var lines = book.Skip(lineFrom + 1).Take(lineTo - lineFrom).Where(x => !string.IsNullOrWhiteSpace(x));
+                var content = string.Join(Environment.NewLine, lines);
+
+                yield return new BookChapter
+                {
+                    Title = title,
+                    Content = content
+                };
+            }
+        }
+        
+        private void ComposeBook(IContainer container, ICollection<BookChapter> chapters)
+        {
+            var subtitleStyle = TextStyle.Default.Size(24).SemiBold().Color(Colors.Blue.Medium);
+            var normalStyle = TextStyle.Default.Size(14);
+            
+            ComposePage(container);
+
+            void ComposePage(IContainer container)
+            {
+                container
+                    .Padding(50)
+                    .Decoration(decoration =>
+                    {
+                        decoration
+                            .Content()
+                            .Stack(stack =>
+                            {
+                                stack.Item().Element(Title);
+                                stack.Item().PageBreak();
+                                stack.Item().Element(TableOfContents);
+                                stack.Item().PageBreak();
+
+                                Chapters(stack);
+
+                                stack.Item().Element(Acknowledgements);
+                            });
+
+                        decoration.Footer().Element(Footer);
+                    });
+            }
+            
+            void Title(IContainer container)
+            {
+                container
+                    .Extend()
+                    .PaddingBottom(200)
+                    .AlignBottom()
+                    .Stack(stack =>
+                    {
+                        stack.Item().Text("Quo Vadis", TextStyle.Default.Size(72).Bold().Color(Colors.Blue.Darken2));
+                        stack.Item().Text("Henryk Sienkiewicz", TextStyle.Default.Size(24).Color(Colors.Grey.Darken2));
+                    });
+            }
+
+            void TableOfContents(IContainer container)
+            {
+                container.Stack(stack =>
+                {
+                    SectionTitle(stack, "Spis treści");
+                    
+                    foreach (var chapter in chapters)
+                    {
+                        stack.Item().InternalLink(chapter.Title).Row(row =>
+                        {
+                            row.RelativeColumn().Text(chapter.Title, normalStyle);
+                            row.ConstantColumn(100).AlignRight().Text(text => text.PageNumberOfLocation(chapter.Title, normalStyle));
+                        });
+                    }
+                });
+            }
+
+            void Chapters(StackDescriptor stack)
+            {
+                foreach (var chapter in chapters)
+                {
+                    stack.Item().Element(container => Chapter(container, chapter.Title, chapter.Content));
+                }
+            }
+            
+            void Chapter(IContainer container, string title, string content)
+            {
+                container.Stack(stack =>
+                {
+                    SectionTitle(stack, title);
+  
+                    stack.Item().Text(text =>
+                    {
+                        text.ParagraphSpacing(5);
+                        text.Span(content, normalStyle);
+                    });
+                    
+                    stack.Item().PageBreak();
+                });
+            }
+
+            void Acknowledgements(IContainer container)
+            {
+                container.Stack(stack =>
+                {
+                    SectionTitle(stack, "Podziękowania");
+                    
+                    stack.Item().Text(text =>
+                    {
+                        text.DefaultTextStyle(normalStyle);
+                        
+                        text.Span("Ten dokument został wygenerowany na podstawie książki w formacie TXT opublikowanej w serwisie ");
+                        text.ExternalLocation("wolnelektury.pl", "https://wolnelektury.pl/", normalStyle.Color(Colors.Blue.Medium).Underline());
+                        text.Span(". Dziękuję za wspieranie polskiego czytelnictwa!");
+                    });
+                });
+            }
+
+            void SectionTitle(StackDescriptor stack, string text)
+            {
+                stack.Item().Location(text).Text(text, subtitleStyle);
+                stack.Item().PaddingTop(10).PaddingBottom(50).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).ExtendHorizontal();
+            }
+            
+            void Footer(IContainer container)
+            {
+                container
+                    .AlignCenter()
+                    .Text(text =>
+                    {
+                        text.CurrentPageNumber();
+                        text.Span(" / ");
+                        text.TotalPages();
+                    });
+            }
+        }
+    }
+}

+ 277 - 0
QuestPDF.Examples/TextExamples.cs

@@ -0,0 +1,277 @@
+using System;
+using System.Linq;
+using System.Text;
+using NUnit.Framework;
+using QuestPDF.Examples.Engine;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Examples
+{
+    public class TextExamples
+    {
+        [Test]
+        public void SimpleTextBlock()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(500, 300)
+                .FileName()
+                .ProduceImages()
+                .ShowResults()
+                .Render(container =>
+                {
+                    container
+                        .Padding(5)
+                        .Box()
+                        .Border(1)
+                        .Padding(10)
+                        .Text(text =>
+                        {
+                            text.DefaultTextStyle(TextStyle.Default.Size(20));
+                            text.Span("This is a normal text, followed by an ");
+                            text.Span("underlined red text.", TextStyle.Default.Size(20).Color(Colors.Red.Medium).Underline());
+                        });
+                });
+        }
+        
+        [Test]
+        public void ParagraphSpacing()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(500, 300)
+                .FileName()
+                .ProduceImages()
+                .ShowResults()
+                .Render(container =>
+                {
+                    container
+                        .Padding(5)
+                        .Box()
+                        .Border(1)
+                        .Padding(10)
+                        .Text(text =>
+                        {
+                            text.ParagraphSpacing(10);
+    
+                            foreach (var i in Enumerable.Range(1, 3))
+                            {
+                                text.Span($"Paragraph {i}: ", TextStyle.Default.SemiBold());
+                                text.Line(Placeholders.Paragraph());
+                            }
+                        });
+                });
+        }
+        
+        [Test]
+        public void CustomElement()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(500, 200)
+                .FileName()
+                .ProduceImages()
+                .ShowResults()
+                .Render(container =>
+                {
+                    container
+                        .Padding(5)
+                        .Box()
+                        .Border(1)
+                        .Padding(10)
+                        .Text(text =>
+                        {
+                            text.DefaultTextStyle(TextStyle.Default.Size(20));
+                            text.Span("This is a random image aligned to the baseline: ");
+                            
+                            text.Element()
+                                .PaddingBottom(-6)
+                                .Height(24)
+                                .Width(48)
+                                .Image(Placeholders.Image);
+                            
+                            text.Span(".");
+                        });
+                });
+        }
+        
+        [Test]
+        public void TextElements()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(PageSizes.A4)
+                .FileName()
+                .ProducePdf()
+                .ShowResults()
+                .Render(container =>
+                {
+                    container
+                        .Padding(20)
+                        .Padding(10)
+                        .Box()
+                        .Border(1)
+                        .Padding(5)
+                        .Padding(10)
+                        .Text(text =>
+                        {
+                            text.DefaultTextStyle(TextStyle.Default);
+                            text.AlignLeft();
+                            text.ParagraphSpacing(10);
+
+                            text.Line(Placeholders.LoremIpsum());
+
+                            text.Span($"This is target text that should show up. {DateTime.UtcNow:T} > This is a short sentence that will be wrapped into second line hopefully, right? <", TextStyle.Default.Underline());
+                        });
+                });
+        }
+        
+        [Test]
+        public void TextStack()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(PageSizes.A4)
+                .FileName()
+                .ProducePdf()
+                .ShowResults()
+                .Render(container =>
+                {
+                    container
+                        .Padding(20)
+                        .Padding(10)
+                        .Box()
+                        .Border(1)
+                        .Padding(5)
+                        .Padding(10)
+                        .Text(text =>
+                        {
+                            text.DefaultTextStyle(TextStyle.Default);
+                            text.AlignLeft();
+                            text.ParagraphSpacing(10);
+                            
+                            foreach (var i in Enumerable.Range(1, 100))
+                                text.Line($"{i}: {Placeholders.Paragraph()}");
+                        });
+                });
+        }
+
+        [Test]
+        public void SpaceIssue()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(PageSizes.A4)
+                .FileName()
+                .ProducePdf()
+                .ShowResults()
+                .Render(container =>
+                {
+                    container
+                        .Padding(20)
+                        .Padding(10)
+                        .Box()
+                        .Border(1)
+                        .Padding(5)
+                        .Padding(10)
+                        .Text(text =>
+                        {
+                            text.DefaultTextStyle(TextStyle.Default);
+                            text.AlignLeft();
+                            text.ParagraphSpacing(10);
+
+                            text.Span(Placeholders.LoremIpsum());
+
+                            text.EmptyLine();
+
+                            text.Span("This text is a normal text, ");
+                            text.Span("this is a bold text, ", TextStyle.Default.Bold());
+                            text.Span("this is a red and underlined text, ", TextStyle.Default.Color(Colors.Red.Medium).Underline());
+                            text.Span("and this is slightly bigger text.", TextStyle.Default.Size(16));
+
+                            text.EmptyLine();
+
+                            text.Span("The new text element also supports injecting custom content between words: ");
+                            text.Element().PaddingBottom(-10).Height(16).Width(32).Image(Placeholders.Image);
+                            text.Span(".");
+
+                            text.EmptyLine();
+
+                            text.Span("This is page number ");
+                            text.CurrentPageNumber();
+                            text.Span(" out of ");
+                            text.TotalPages();
+
+                            text.EmptyLine();
+
+                            text.ExternalLocation("Please visit QuestPDF website", "https://www.questpdf.com");
+
+                            text.EmptyLine();
+
+                            text.Span(Placeholders.Paragraphs());
+                            
+                            
+                            text.EmptyLine();
+
+                            text.Span(Placeholders.Paragraphs(), TextStyle.Default.Italic());
+                            
+                            text.Line("This is target text that does not show up. " + Placeholders.Paragraph());
+                        });
+                });
+        }
+
+        [Test]
+        public void HugeList()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(PageSizes.A4)
+                .FileName()
+                .ProducePdf()
+                .ShowResults()
+                .Render(container =>
+                {
+                    container
+                        .Padding(20)
+                        .Padding(10)
+                        .Box()
+                        .Border(1)
+                        .Padding(5)
+                        .Padding(10)
+                        .Text(text =>
+                        {
+                            text.DefaultTextStyle(TextStyle.Default);
+                            text.AlignLeft();
+                            text.ParagraphSpacing(10);
+
+                            text.Span("This text is a normal text, ");
+                            text.Span("this is a bold text, ", TextStyle.Default.Bold());
+                            text.Span("this is a red and underlined text, ", TextStyle.Default.Color(Colors.Red.Medium).Underline());
+                            text.Span("and this is slightly bigger text.", TextStyle.Default.Size(16));
+                            
+                            text.Span("The new text element also supports injecting custom content between words: ");
+                            text.Element().PaddingBottom(-10).Height(16).Width(32).Image(Placeholders.Image);
+                            text.Span(".");
+                            
+                            text.EmptyLine();
+                            
+                            foreach (var i in Enumerable.Range(1, 100))
+                            {
+                                text.Line($"{i}: {Placeholders.Paragraph()}");
+
+                                text.ExternalLocation("Please visit QuestPDF website", "https://www.questpdf.com");
+                                
+                                text.Span("This is page number ");
+                                text.CurrentPageNumber();
+                                text.Span(" out of ");
+                                text.TotalPages();
+                                
+                                text.EmptyLine();
+                            }
+                        });
+                });
+        }
+    }
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 285 - 0
QuestPDF.Examples/quo-vadis.txt


+ 2 - 2
QuestPDF.ReportSample/DataSource.cs

@@ -18,8 +18,8 @@ namespace QuestPDF.ReportSample
                 HeaderFields = HeaderFields(),
                 
                 LogoData = Helpers.GetImage("Logo.png"),
-                Sections = Enumerable.Range(0, 50).Select(x => GenerateSection()).ToList(),
-                Photos = Enumerable.Range(0, 30).Select(x => GetReportPhotos()).ToList()
+                Sections = Enumerable.Range(0, 40).Select(x => GenerateSection()).ToList(),
+                Photos = Enumerable.Range(0, 25).Select(x => GetReportPhotos()).ToList()
             };
 
             List<ReportHeaderField> HeaderFields()

+ 2 - 2
QuestPDF.ReportSample/Layouts/SectionTemplate.cs

@@ -28,13 +28,13 @@ namespace QuestPDF.ReportSample.Layouts
                     {
                         foreach (var part in Model.Parts)
                         {
-                            stack.Item().Row(row =>
+                            stack.Item().EnsureSpace(25).Row(row =>
                             {
                                 row.ConstantColumn(150).LabelCell().Text(part.Label, Typography.Normal);
                                 var frame = row.RelativeColumn().ValueCell();
                             
                                 if (part is ReportSectionText text)
-                                    frame.Text(text.Text, Typography.Normal);
+                                    frame.ShowEntire().Text(text.Text, Typography.Normal);
                         
                                 if (part is ReportSectionMap map)
                                     frame.Element(x => MapElement(x, map));

+ 15 - 5
QuestPDF.ReportSample/Layouts/StandardReport.cs

@@ -34,7 +34,15 @@ namespace QuestPDF.ReportSample.Layouts
                         
                     page.Header().Element(ComposeHeader);
                     page.Content().Element(ComposeContent);
-                    page.Footer().AlignCenter().PageNumber();
+                    
+                    page.Footer().AlignCenter().Text(text =>
+                    {
+                        text.DefaultTextStyle(Typography.Normal);
+                        
+                        text.CurrentPageNumber();
+                        text.Span(" / ");
+                        text.TotalPages();
+                    });
                 });
         }
 
@@ -59,10 +67,10 @@ namespace QuestPDF.ReportSample.Layouts
                         
                     foreach (var field in Model.HeaderFields)
                     {
-                        grid.Item().Stack(row =>
-                        {   
-                            row.Item().AlignLeft().Text(field.Label, Typography.Normal.SemiBold());
-                            row.Item().Text(field.Value, Typography.Normal);
+                        grid.Item().Text(text =>
+                        {
+                            text.Span($"{field.Label}: ", Typography.Normal.SemiBold());
+                            text.Span(field.Value, Typography.Normal);
                         });
                     }
                 });
@@ -77,6 +85,8 @@ namespace QuestPDF.ReportSample.Layouts
 
                 stack.Item().Component(new TableOfContentsTemplate(Model.Sections));
                 
+                stack.Item().PageBreak();
+                
                 foreach (var section in Model.Sections)
                     stack.Item().Location(section.Title).Component(new SectionTemplate(section));
 

+ 1 - 1
QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs

@@ -43,7 +43,7 @@ namespace QuestPDF.ReportSample.Layouts
                 {
                     row.ConstantColumn(25).Text($"{number}.", Typography.Normal);
                     row.RelativeColumn().Text(locationName, Typography.Normal);
-                    row.ConstantColumn(150).AlignRight().PageNumber($"Page {{pdf:{locationName}}}", Typography.Normal.AlignRight());
+                    row.ConstantColumn(150).AlignRight().Text(text => text.PageNumberOfLocation(locationName, Typography.Normal));
                 });
         }
     }

+ 3 - 5
QuestPDF.ReportSample/QuestPDF.ReportSample.csproj

@@ -8,12 +8,10 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
-        <PackageReference Include="DeepCloner" Version="0.10.2" />
-        <PackageReference Include="nunit" Version="3.12.0" />
-        <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
+        <PackageReference Include="nunit" Version="3.13.2" />
+        <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
-        <PackageReference Include="SkiaSharp" Version="2.80.2" />
+        <PackageReference Include="SkiaSharp" Version="2.80.3" />
     </ItemGroup>
 
     <ItemGroup>

+ 1 - 1
QuestPDF.ReportSample/Typography.cs

@@ -8,6 +8,6 @@ namespace QuestPDF.ReportSample
     {
         public static TextStyle Title => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Darken3).Size(26).Black();
         public static TextStyle Headline => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Medium).Size(16).SemiBold();
-        public static TextStyle Normal => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Black).Size(11).LineHeight(1.25f).AlignLeft();
+        public static TextStyle Normal => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Black).Size(11).LineHeight(1.1f);
     }
 }

+ 1 - 5
QuestPDF.UnitTests/AlignmentTests.cs

@@ -10,11 +10,7 @@ namespace QuestPDF.UnitTests
     public class AlignmentTests
     {
         [Test]
-        public void Measure_ShouldHandleNullChild() => new Alignment().MeasureWithoutChild();
-        
-        [Test]
-        public void Draw_ShouldHandleNullChild() => new Alignment().DrawWithoutChild();
-
+        public void Measure() => SimpleContainerTests.Measure<Alignment>();
         [Test]
         public void Draw_HorizontalCenter_VerticalCenter()
         {

+ 0 - 6
QuestPDF.UnitTests/AspectRatioTests.cs

@@ -9,12 +9,6 @@ namespace QuestPDF.UnitTests
     [TestFixture]
     public class AspectRatioTests
     {
-        [Test]
-        public void Measure_ShouldHandleNullChild() => new AspectRatio().MeasureWithoutChild();
-        
-        [Test]
-        public void Draw_ShouldHandleNullChild() => new AspectRatio().DrawWithoutChild();
-        
         [Test]
         public void Measure_FitWidth_EnoughSpace_FullRender()
         {

+ 2 - 2
QuestPDF.UnitTests/BackgroundTests.cs

@@ -10,8 +10,8 @@ namespace QuestPDF.UnitTests
     public class BackgroundTests
     {
         [Test]
-        public void Measure_ShouldHandleNullChild() => new Background().MeasureWithoutChild();
-
+        public void Measure() => SimpleContainerTests.Measure<Background>();
+        
         [Test]
         public void Draw_ShouldHandleNullChild()
         {

+ 2 - 2
QuestPDF.UnitTests/BorderTests.cs

@@ -11,8 +11,8 @@ namespace QuestPDF.UnitTests
     public class BorderTests
     {
         [Test]
-        public void Measure_ShouldHandleNullChild() => new Border().MeasureWithoutChild();
-
+        public void Measure() => SimpleContainerTests.Measure<Border>();
+        
         [Test]
         public void ComponentShouldNotAffectLayout()
         {

+ 56 - 0
QuestPDF.UnitTests/BoxTests.cs

@@ -0,0 +1,56 @@
+using NUnit.Framework;
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+using QuestPDF.UnitTests.TestEngine;
+
+namespace QuestPDF.UnitTests
+{
+    [TestFixture]
+    public class BoxTests
+    {
+        [Test]
+        public void Measure() => SimpleContainerTests.Measure<Box>();
+        
+        [Test]
+        public void Draw_Wrap()
+        {
+            TestPlan
+                .For(x => new Box
+                {
+                    Child = x.CreateChild()
+                })
+                .DrawElement(new Size(400, 300))
+                .ExpectChildMeasure(expectedInput: new Size(400, 300), returns: new Wrap())
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Measure_PartialRender()
+        {
+            TestPlan
+                .For(x => new Box
+                {
+                    Child = x.CreateChild()
+                })
+                .MeasureElement(new Size(400, 300))
+                .ExpectChildMeasure(expectedInput: new Size(400, 300), returns: new PartialRender(200, 100))
+                .ExpectChildDraw(new Size(200, 100))
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Measure_FullRender()
+        {
+            TestPlan
+                .For(x => new Box
+                {
+                    Child = x.CreateChild()
+                })
+                .MeasureElement(new Size(500, 400))
+                .ExpectChildMeasure(expectedInput: new Size(500, 400), returns: new FullRender(300, 200))
+                .ExpectChildDraw(new Size(300, 200))
+                .CheckDrawResult();
+        }
+    }
+}

+ 0 - 6
QuestPDF.UnitTests/ConstrainedTests.cs

@@ -9,12 +9,6 @@ namespace QuestPDF.UnitTests
     [TestFixture]
     public class ConstrainedTests
     {
-        [Test]
-        public void Measure_ShouldHandleNullChild() => new Constrained().MeasureWithoutChild();
-        
-        [Test]
-        public void Draw_ShouldHandleNullChild() => new Constrained().DrawWithoutChild();
-
         [Test]
         public void Measure_MinHeight_ExpectWrap()
         {

+ 0 - 10
QuestPDF.UnitTests/DebugTests.cs

@@ -1,10 +0,0 @@
-using NUnit.Framework;
-
-namespace QuestPDF.UnitTests
-{
-    [TestFixture]
-    public class DebugTests
-    {
-        
-    }
-}

+ 3 - 0
QuestPDF.UnitTests/EnsureSpaceTests.cs

@@ -78,5 +78,8 @@ namespace QuestPDF.UnitTests
                 .ExpectChildMeasure(new Size(400, 300), SpacePlan.FullRender(300, 250))
                 .CheckMeasureResult(SpacePlan.FullRender(300, 250));
         }
+        
+        [Test]
+        public void Draw() => SimpleContainerTests.Draw<EnsureSpace>();
     }
 }

+ 3 - 6
QuestPDF.UnitTests/ExtendTests.cs

@@ -9,12 +9,6 @@ namespace QuestPDF.UnitTests
     [TestFixture]
     public class ExtendTests
     {
-        [Test]
-        public void Measure_ShouldHandleNullChild() => new Extend().MeasureWithoutChild();
-        
-        [Test]
-        public void Draw_ShouldHandleNullChild() => new Extend().DrawWithoutChild();
-        
         [Test]
         public void Measure_ReturnsWrap_WhenChildReturnsWrap()
         {
@@ -87,5 +81,8 @@ namespace QuestPDF.UnitTests
                 .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(100, 100))
                 .CheckMeasureResult(SpacePlan.FullRender(100, 200));
         }
+
+        [Test]
+        public void Draw() => SimpleContainerTests.Draw<Extend>();
     }
 }

+ 15 - 0
QuestPDF.UnitTests/ExternalLinkTests.cs

@@ -0,0 +1,15 @@
+using NUnit.Framework;
+using QuestPDF.Elements;
+using QuestPDF.UnitTests.TestEngine;
+
+namespace QuestPDF.UnitTests
+{
+    [TestFixture]
+    public class ExternalLinkTests
+    {
+        [Test]
+        public void Measure() => SimpleContainerTests.Measure<ExternalLink>();
+        
+        // TODO: consider tests for the Draw method
+    }
+}

+ 27 - 27
QuestPDF.UnitTests/GridTests.cs

@@ -39,30 +39,30 @@ namespace QuestPDF.UnitTests
             // assert
             var expected = new Container();
             
-            expected.Container().Stack(stack =>
+            expected.Stack(stack =>
             {
                 stack.Item().Row(row =>
                 {
-                    row.RelativeColumn(6).Container().Element(childA);
-                    row.RelativeColumn(4).Container().Element(childB);
+                    row.RelativeColumn(6).Element(childA);
+                    row.RelativeColumn(4).Element(childB);
                     row.RelativeColumn(2);
                 });
                 
                 stack.Item().Row(row =>
                 {
-                    row.RelativeColumn(4).Container().Element(childC);
-                    row.RelativeColumn(2).Container().Element(childD);
+                    row.RelativeColumn(4).Element(childC);
+                    row.RelativeColumn(2).Element(childD);
                     row.RelativeColumn(6);
                 });
                 
                 stack.Item().Row(row =>
                 {
-                    row.RelativeColumn(8).Container().Element(childE);
+                    row.RelativeColumn(8).Element(childE);
                     row.RelativeColumn(4);
                 });
             });
             
-            structure.Should().BeEquivalentTo(expected, o => o.WithTracing().WithAutoConversion().WithStrictOrdering().IncludingAllRuntimeProperties());
+            TestPlan.CompareOperations(structure, expected);
         }
         
         [Test]
@@ -93,33 +93,33 @@ namespace QuestPDF.UnitTests
             // assert
             var expected = new Container();
             
-            expected.Container().Stack(stack =>
+            expected.Stack(stack =>
             {
                 stack.Item().Row(row =>
                 {
                     row.RelativeColumn(1);
-                    row.RelativeColumn(6).Container().Element(childA);
-                    row.RelativeColumn(4).Container().Element(childB);
+                    row.RelativeColumn(6).Element(childA);
+                    row.RelativeColumn(4).Element(childB);
                     row.RelativeColumn(1);
                 });
                 
                 stack.Item().Row(row =>
                 {
                     row.RelativeColumn(3);
-                    row.RelativeColumn(4).Container().Element(childC);
-                    row.RelativeColumn(2).Container().Element(childD);
+                    row.RelativeColumn(4).Element(childC);
+                    row.RelativeColumn(2).Element(childD);
                     row.RelativeColumn(3);
                 });
                 
                 stack.Item().Row(row =>
                 {
                     row.RelativeColumn(2);
-                    row.RelativeColumn(8).Container().Element(childE);
+                    row.RelativeColumn(8).Element(childE);
                     row.RelativeColumn(2);
                 });
             });
 
-            structure.Should().BeEquivalentTo(expected, o => o.WithTracing().WithAutoConversion().WithStrictOrdering().IncludingAllRuntimeProperties());
+            TestPlan.CompareOperations(structure, expected);
         }
         
         [Test]
@@ -150,30 +150,30 @@ namespace QuestPDF.UnitTests
             // assert
             var expected = new Container();
             
-            expected.Container().Stack(stack =>
+            expected.Stack(stack =>
             {
                 stack.Item().Row(row =>
                 {
                     row.RelativeColumn(2);
-                    row.RelativeColumn(6).Container().Element(childA);
-                    row.RelativeColumn(4).Container().Element(childB);
+                    row.RelativeColumn(6).Element(childA);
+                    row.RelativeColumn(4).Element(childB);
                 });
                 
                 stack.Item().Row(row =>
                 {
                     row.RelativeColumn(6);
-                    row.RelativeColumn(4).Container().Element(childC);
-                    row.RelativeColumn(2).Container().Element(childD);
+                    row.RelativeColumn(4).Element(childC);
+                    row.RelativeColumn(2).Element(childD);
                 });
                 
                 stack.Item().Row(row =>
                 {
                     row.RelativeColumn(4);
-                    row.RelativeColumn(8).Container().Element(childE);
+                    row.RelativeColumn(8).Element(childE);
                 });
             });
             
-            structure.Should().BeEquivalentTo(expected, o => o.WithTracing().WithAutoConversion().WithStrictOrdering().IncludingAllRuntimeProperties());
+            TestPlan.CompareOperations(structure, expected);
         }
         
         #endregion
@@ -210,7 +210,7 @@ namespace QuestPDF.UnitTests
             // assert
             var expected = new Container();
             
-            expected.Container().Stack(stack =>
+            expected.Stack(stack =>
             {
                 stack.Spacing(20);
                 
@@ -219,8 +219,8 @@ namespace QuestPDF.UnitTests
                     row.Spacing(30);
                     
                     row.RelativeColumn(3);
-                    row.RelativeColumn(5).Container().Element(childA);
-                    row.RelativeColumn(5).Container().Element(childB);
+                    row.RelativeColumn(5).Element(childA);
+                    row.RelativeColumn(5).Element(childB);
                     row.RelativeColumn(3);
                 });
                 
@@ -229,7 +229,7 @@ namespace QuestPDF.UnitTests
                     row.Spacing(30);
                     
                     row.RelativeColumn(3);
-                    row.RelativeColumn(10).Container().Element(childC);
+                    row.RelativeColumn(10).Element(childC);
                     row.RelativeColumn(3);
                 });
                 
@@ -238,12 +238,12 @@ namespace QuestPDF.UnitTests
                     row.Spacing(30);
                     
                     row.RelativeColumn(2);
-                    row.RelativeColumn(12).Container().Element(childD);
+                    row.RelativeColumn(12).Element(childD);
                     row.RelativeColumn(2);
                 });
             });
             
-            structure.Should().BeEquivalentTo(expected, o => o.WithTracing().WithAutoConversion().WithStrictOrdering().IncludingAllRuntimeProperties().AllowingInfiniteRecursion());
+            TestPlan.CompareOperations(structure, expected);
         }
         
         #endregion

+ 0 - 30
QuestPDF.UnitTests/Helpers.cs

@@ -1,30 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using FluentAssertions;
-using QuestPDF.Infrastructure;
-using QuestPDF.UnitTests.TestEngine;
-
-namespace QuestPDF.UnitTests
-{
-    public static class Helpers
-    {
-        public static Random Random = new Random();
-        
-        public static Size RandomSize => new Size(Random.Next(200, 400), Random.Next(100, 200));
-
-        public static void ShouldEqual(this IEnumerable<OperationBase> commands, IEnumerable<OperationBase> expected)
-        {
-            commands.Should().HaveSameCount(expected);
-            
-            commands
-                .Zip(expected)
-                .ToList()
-                .ForEach(x =>
-                {
-                    x.First.Should().BeOfType(x.Second.GetType());
-                    x.First.Should().BeEquivalentTo(x.Second, y => y.RespectingRuntimeTypes());
-                });
-        }
-    }
-}

+ 0 - 6
QuestPDF.UnitTests/ImageTests.cs

@@ -11,12 +11,6 @@ namespace QuestPDF.UnitTests
     [TestFixture]
     public class ImageTests
     {
-        [Test]
-        public void Measure_ShouldHandleNullChild() => new AspectRatio().MeasureWithoutChild();
-        
-        [Test]
-        public void Draw_ShouldHandleNullChild() => new AspectRatio().DrawWithoutChild();
-        
         [Test]
         public void Measure_TakesAvailableSpaceRegardlessOfSize()
         {

+ 15 - 0
QuestPDF.UnitTests/InternalLinkTests.cs

@@ -0,0 +1,15 @@
+using NUnit.Framework;
+using QuestPDF.Elements;
+using QuestPDF.UnitTests.TestEngine;
+
+namespace QuestPDF.UnitTests
+{
+    [TestFixture]
+    public class InternalLinkTests
+    {
+        [Test]
+        public void Measure() => SimpleContainerTests.Measure<InternalLink>();
+        
+        // TODO: consider tests for the Draw method
+    }
+}

+ 15 - 0
QuestPDF.UnitTests/InternalLocationTests.cs

@@ -0,0 +1,15 @@
+using NUnit.Framework;
+using QuestPDF.Elements;
+using QuestPDF.UnitTests.TestEngine;
+
+namespace QuestPDF.UnitTests
+{
+    [TestFixture]
+    public class InternalLocationTests
+    {
+        [Test]
+        public void Measure() => SimpleContainerTests.Measure<InternalLink>();
+        
+        // TODO: consider tests for the Draw method
+    }
+}

+ 131 - 0
QuestPDF.UnitTests/LayersTests.cs

@@ -0,0 +1,131 @@
+using System.Collections.Generic;
+using NUnit.Framework;
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+using QuestPDF.UnitTests.TestEngine;
+
+namespace QuestPDF.UnitTests
+{
+    [TestFixture]
+    public class LayersTests
+    {
+        private const string BackgroundLayer = "background";
+        private const string MainLayer = "main";
+        private const string ForegroundLayer = "foreground";
+        
+        private static Layers GetLayers(TestPlan x)
+        {
+            return new Layers
+            {
+                Children = new List<Layer>
+                {
+                    new Layer
+                    {
+                        Child = x.CreateChild(BackgroundLayer)
+                    },
+                    new Layer
+                    {
+                        Child = x.CreateChild(MainLayer),
+                        IsPrimary = true
+                    },
+                    new Layer
+                    {
+                        Child = x.CreateChild(ForegroundLayer)
+                    }
+                }
+            };
+        }
+        
+        #region measure
+        
+        [Test]
+        public void Measure_Wrap()
+        {
+            TestPlan
+                .For(GetLayers)
+                .MeasureElement(new Size(800, 600))
+                .ExpectChildMeasure(MainLayer, new Size(800, 600), new Wrap())
+                .CheckMeasureResult(new Wrap());
+        }
+
+        [Test]
+        public void Measure_PartialRender()
+        {
+            TestPlan
+                .For(GetLayers)
+                .MeasureElement(new Size(800, 600))
+                .ExpectChildMeasure(MainLayer, new Size(800, 600), new PartialRender(700, 500))
+                .CheckMeasureResult(new PartialRender(700, 500));
+        }
+        
+        [Test]
+        public void Measure_FullRender()
+        {
+            TestPlan
+                .For(GetLayers)
+                .MeasureElement(new Size(800, 600))
+                .ExpectChildMeasure(MainLayer, new Size(800, 600), new FullRender(500, 400))
+                .CheckMeasureResult(new FullRender(500, 400));
+        }
+        
+        #endregion
+        
+        #region draw
+        
+        [Test]
+        public void Draw_Simple()
+        {
+            TestPlan
+                .For(GetLayers)
+                .MeasureElement(new Size(800, 600))
+                
+                .ExpectChildMeasure(BackgroundLayer, new Size(800, 600), new FullRender(100, 200))
+                .ExpectChildMeasure(MainLayer, new Size(800, 600), new PartialRender(200, 300))
+                .ExpectChildMeasure(ForegroundLayer, new Size(800, 600), new FullRender(300, 400))
+                
+                
+                .ExpectChildDraw(BackgroundLayer, new Size(800, 600))
+                .ExpectChildDraw(MainLayer, new Size(800, 600))
+                .ExpectChildDraw(ForegroundLayer, new Size(800, 600))
+                
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Draw_WhenSecondaryLayerReturnsWrap_SkipThatLayer_1()
+        {
+            TestPlan
+                .For(GetLayers)
+                .MeasureElement(new Size(800, 600))
+                
+                .ExpectChildMeasure(BackgroundLayer, new Size(800, 600), new PartialRender(100, 200))
+                .ExpectChildMeasure(MainLayer, new Size(800, 600), new PartialRender(200, 300))
+                .ExpectChildMeasure(ForegroundLayer, new Size(800, 600), new Wrap())
+                
+                .ExpectChildDraw(BackgroundLayer, new Size(800, 600))
+                .ExpectChildDraw(MainLayer, new Size(800, 600))
+                
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Draw_WhenSecondaryLayerReturnsWrap_SkipThatLayer_2()
+        {
+            TestPlan
+                .For(GetLayers)
+                .MeasureElement(new Size(800, 600))
+                
+                .ExpectChildMeasure(BackgroundLayer, new Size(800, 600), new Wrap())
+                .ExpectChildMeasure(MainLayer, new Size(800, 600), new PartialRender(200, 300))
+                .ExpectChildMeasure(ForegroundLayer, new Size(800, 600), new PartialRender(300, 400))
+                
+                .ExpectChildDraw(MainLayer, new Size(800, 600))
+                .ExpectChildDraw(ForegroundLayer, new Size(800, 600))
+                
+                .CheckDrawResult();
+        }
+        
+        #endregion
+    }
+}

+ 0 - 6
QuestPDF.UnitTests/PaddingTests.cs

@@ -9,12 +9,6 @@ namespace QuestPDF.UnitTests
     [TestFixture]
     public class PaddingTests
     {
-        [Test]
-        public void Measure_ShouldHandleNullChild() => new Padding().MeasureWithoutChild();
-        
-        [Test]
-        public void Draw_ShouldHandleNullChild() => new Padding().DrawWithoutChild();
-        
         private Padding GetPadding(TestPlan plan)
         {
             return new Padding()

+ 0 - 10
QuestPDF.UnitTests/PageNumberTests.cs

@@ -1,10 +0,0 @@
-using NUnit.Framework;
-
-namespace QuestPDF.UnitTests
-{
-    [TestFixture]
-    public class PageNumberTests
-    {
-
-    }
-}

+ 4 - 5
QuestPDF.UnitTests/QuestPDF.UnitTests.csproj

@@ -6,12 +6,11 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="FluentAssertions" Version="5.10.3" />
-        <PackageReference Include="Moq" Version="4.13.1" />
-        <PackageReference Include="nunit" Version="3.13.1" />
-        <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
+        <PackageReference Include="FluentAssertions" Version="6.1.0" />
+        <PackageReference Include="nunit" Version="3.13.2" />
+        <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
-        <PackageReference Include="SkiaSharp" Version="2.80.2" />
+        <PackageReference Include="SkiaSharp" Version="2.80.3" />
     </ItemGroup>
 
     <ItemGroup>

+ 31 - 0
QuestPDF.UnitTests/RotateTests.cs

@@ -0,0 +1,31 @@
+using NUnit.Framework;
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+using QuestPDF.UnitTests.TestEngine;
+
+namespace QuestPDF.UnitTests
+{
+    [TestFixture]
+    public class RotateTests
+    {
+        [Test]
+        public void Measure() => SimpleContainerTests.Measure<Rotate>();
+
+        [Test]
+        public void Draw()
+        {
+            TestPlan
+                .For(x => new Rotate
+                {
+                    Child = x.CreateChild(),
+                    Angle = 123
+                })
+                .DrawElement(new Size(400, 300))
+                .ExpectCanvasRotate(123)
+                .ExpectChildDraw(new Size(400, 300))
+                .ExpectCanvasRotate(-123)
+                .CheckDrawResult();
+        } 
+    }
+}

+ 154 - 3
QuestPDF.UnitTests/RowTests.cs

@@ -1,6 +1,7 @@
 using NUnit.Framework;
 using QuestPDF.Drawing;
 using QuestPDF.Elements;
+using QuestPDF.Fluent;
 using QuestPDF.Infrastructure;
 using QuestPDF.UnitTests.TestEngine;
 
@@ -109,8 +110,158 @@ namespace QuestPDF.UnitTests
 
         #endregion
         
-        // TODO: add tests for the spacing property
-        // TODO: add tests for the tree builder method
-        // TODO: add tests for relative column
+        #region Structure
+        
+        [Test]
+        public void Structure_RelativeColumnsHandling()
+        { 
+            // arrange
+            var childA = TestPlan.CreateUniqueElement();
+            var childB = TestPlan.CreateUniqueElement();
+            var childC = TestPlan.CreateUniqueElement();
+            var childD = TestPlan.CreateUniqueElement();
+            var childE = TestPlan.CreateUniqueElement();
+
+            const int spacing = 25;
+            var availableSpace = new Size(1100, 400);
+            
+            // act
+            var value = new Container();
+
+            value.Row(row =>
+            {
+                row.Spacing(spacing);
+                
+                row.ConstantColumn(150).Element(childA);
+                row.ConstantColumn(250).Element(childB);
+                row.RelativeColumn(1).Element(childC);
+                row.RelativeColumn(2).Element(childD);
+                row.RelativeColumn(3).Element(childE);
+            });
+            
+            // assert
+            var expected = new Container();
+
+            expected.Row(row =>
+            {
+                row.Spacing(spacing);
+                
+                row.ConstantColumn(150).Element(childA);
+                row.ConstantColumn(250).Element(childB);
+                row.ConstantColumn(100).Element(childC);
+                row.ConstantColumn(200).Element(childD);
+                row.ConstantColumn(300).Element(childE);
+            });
+            
+            TestPlan.CompareOperations(value, expected, availableSpace);
+        }
+        
+        [Test]
+        public void Structure_Tree()
+        { 
+            // arrange
+            var childA = TestPlan.CreateUniqueElement();
+            var childB = TestPlan.CreateUniqueElement();
+            var childC = TestPlan.CreateUniqueElement();
+            var childD = TestPlan.CreateUniqueElement();
+            var childE = TestPlan.CreateUniqueElement();
+
+            const int spacing = 25;
+            var availableSpace = new Size(1200, 400);
+            
+            // act
+            var value = new Container();
+
+            value.Row(row =>
+            {
+                row.Spacing(spacing);
+                
+                row.ConstantColumn(150).Element(childA);
+                row.ConstantColumn(200).Element(childB);
+                row.ConstantColumn(250).Element(childC);
+                row.RelativeColumn(2).Element(childD);
+                row.RelativeColumn(3).Element(childE);
+            });
+            
+            // assert
+            var expected = new SimpleRow
+            {
+                Left = new SimpleRow
+                {
+                    Left = new SimpleRow
+                    {
+                        Left = new Constrained
+                        {
+                            MinWidth = 150,
+                            MaxWidth = 150,
+                            Child = childA
+                        },
+                        Right = new Constrained
+                        {
+                            MinWidth = 25,
+                            MaxWidth = 25
+                        }
+                    },
+                    Right = new SimpleRow
+                    {
+                        Left = new Constrained
+                        {
+                            MinWidth = 200,
+                            MaxWidth = 200,
+                            Child = childB
+                        },
+                        Right = new Constrained
+                        {
+                            MinWidth = 25,
+                            MaxWidth = 25
+                        }
+                    }
+                },
+                Right = new SimpleRow
+                {
+                    Left = new SimpleRow
+                    {
+                        Left = new Constrained
+                        {
+                            MinWidth = 250,
+                            MaxWidth = 250,
+                            Child = childC
+                        },
+                        Right = new Constrained
+                        {
+                            MinWidth = 25,
+                            MaxWidth = 25
+                        }
+                    },
+                    Right = new SimpleRow
+                    {
+                        Left = new Constrained
+                        {
+                            MinWidth = 200,
+                            MaxWidth = 200,
+                            Child = childD
+                        },
+                        Right = new SimpleRow
+                        {
+                            Left = new Constrained
+                            {
+                                MinWidth = 25,
+                                MaxWidth = 25
+                            },
+                            Right = new Constrained
+                            {
+                                MinWidth = 300,
+                                MaxWidth = 300,
+                                Child = childE
+                            }
+                        }
+                    }
+                }
+            };
+            
+            TestPlan.CompareOperations(value, expected, availableSpace);
+        }
+        
+        #endregion
     }
 }

+ 152 - 0
QuestPDF.UnitTests/ScaleTests.cs

@@ -0,0 +1,152 @@
+using NUnit.Framework;
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+using QuestPDF.UnitTests.TestEngine;
+
+namespace QuestPDF.UnitTests
+{
+    [TestFixture]
+    public class ScaleTests
+    {
+        #region measure
+        
+        [Test]
+        public void Measure_Wrap()
+        {
+            TestPlan
+                .For(x => new Scale
+                {
+                    Child = x.CreateChild(),
+                    ScaleX = 3,
+                    ScaleY = 2
+                })
+                .MeasureElement(new Size(900, 800))
+                .ExpectChildMeasure(new Size(300, 400), new Wrap())
+                .CheckMeasureResult(new Wrap());
+        }
+        
+        [Test]
+        public void Measure_PartialRender()
+        {
+            TestPlan
+                .For(x => new Scale
+                {
+                    Child = x.CreateChild(),
+                    ScaleX = 3,
+                    ScaleY = 2
+                })
+                .MeasureElement(new Size(900, 800))
+                .ExpectChildMeasure(new Size(300, 400), new PartialRender(200, 350))
+                .CheckMeasureResult(new PartialRender(600, 700));
+        }
+        
+        [Test]
+        public void Measure_FullRender()
+        {
+            TestPlan
+                .For(x => new Scale
+                {
+                    Child = x.CreateChild(),
+                    ScaleX = 3,
+                    ScaleY = 2
+                })
+                .MeasureElement(new Size(900, 800))
+                .ExpectChildMeasure(new Size(300, 400), new FullRender(250, 300))
+                .CheckMeasureResult(new FullRender(750, 600));
+        }
+        
+        [Test]
+        public void Measure_NegativeScaleX()
+        {
+            TestPlan
+                .For(x => new Scale
+                {
+                    Child = x.CreateChild(),
+                    ScaleX = -2,
+                    ScaleY = 1
+                })
+                .MeasureElement(new Size(800, 600))
+                .ExpectChildMeasure(new Size(400, 600), new FullRender(300, 500))
+                .CheckMeasureResult(new FullRender(600, 500));
+        }
+        
+        [Test]
+        public void Measure_NegativeScaleY()
+        {
+            TestPlan
+                .For(x => new Scale
+                {
+                    Child = x.CreateChild(),
+                    ScaleX = 1,
+                    ScaleY = -3
+                })
+                .MeasureElement(new Size(800, 600))
+                .ExpectChildMeasure(new Size(800, 200), new FullRender(800, 100))
+                .CheckMeasureResult(new FullRender(800, 300));
+        }
+        
+        #endregion
+        
+        #region draw
+        
+        [Test]
+        public void Draw_Simple()
+        {
+            TestPlan
+                .For(x => new Scale
+                {
+                    Child = x.CreateChild(),
+                    ScaleX = 3,
+                    ScaleY = 2
+                })
+                .DrawElement(new Size(900, 800))
+                .ExpectCanvasTranslate(0, 0)
+                .ExpectCanvasScale(3, 2)
+                .ExpectChildDraw(new Size(300, 400))
+                .ExpectCanvasScale(1/3f, 1/2f)
+                .ExpectCanvasTranslate(0, 0)
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Draw_NegativeScaleX()
+        {
+            TestPlan
+                .For(x => new Scale
+                {
+                    Child = x.CreateChild(),
+                    ScaleX = -3,
+                    ScaleY = 2
+                })
+                .DrawElement(new Size(900, 800))
+                .ExpectCanvasTranslate(900, 0)
+                .ExpectCanvasScale(-3, 2)
+                .ExpectChildDraw(new Size(300, 400))
+                .ExpectCanvasScale(-1/3f, 1/2f)
+                .ExpectCanvasTranslate(-900, 0)
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Draw_NegativeScaleY()
+        {
+            TestPlan
+                .For(x => new Scale
+                {
+                    Child = x.CreateChild(),
+                    ScaleX = 3,
+                    ScaleY = -2
+                })
+                .DrawElement(new Size(900, 800))
+                .ExpectCanvasTranslate(0, 800)
+                .ExpectCanvasScale(3, -2)
+                .ExpectChildDraw(new Size(300, 400))
+                .ExpectCanvasScale(1/3f, -1/2f)
+                .ExpectCanvasTranslate(0, -800)
+                .CheckDrawResult();
+        }
+        
+        #endregion
+    }
+}

+ 3 - 0
QuestPDF.UnitTests/ShowEntireTests.cs

@@ -47,5 +47,8 @@ namespace QuestPDF.UnitTests
                 .ExpectChildMeasure(new Size(400, 300), SpacePlan.FullRender(300, 200))
                 .CheckMeasureResult(SpacePlan.FullRender(300, 200));
         }
+        
+        [Test]
+        public void Draw() => SimpleContainerTests.Draw<ShowEntire>();
     }
 }

+ 1 - 27
QuestPDF.UnitTests/ShowOnceTest.cs

@@ -11,33 +11,7 @@ namespace QuestPDF.UnitTests
     public class ShowOnceTest
     {
         [Test]
-        public void Measure_ShouldHandleNullChild() => new ShowOnce().MeasureWithoutChild();
-        
-        [Test]
-        public void Draw_ShouldHandleNullChild() => new ShowOnce().DrawWithoutChild();
-
-        [Test]
-        public void ShouldRenderOnce_WhenRenderingCalledMultipleTimes()
-        {
-            var child = new Mock<Element>();
-            
-            child
-                .Setup(x => x.Measure(It.IsAny<Size>()))
-                .Returns(() => SpacePlan.FullRender(0, 0));
-
-            var element = new ShowOnce()
-            {
-                Child = child.Object
-            };
-
-            element.Draw(Size.Zero);
-            element.Draw(Size.Zero);
-            
-            child.Verify(x => x.Draw(It.IsAny<Size>()), Times.Once);
-        }
-        
-        [Test]
-        public void Draw_HorizontalRight_VerticalTop()
+        public void Draw()
         {
             TestPlan
                 .For(x => new ShowOnce()

+ 162 - 0
QuestPDF.UnitTests/SimpleRotateTests.cs

@@ -0,0 +1,162 @@
+using NUnit.Framework;
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+using QuestPDF.UnitTests.TestEngine;
+
+namespace QuestPDF.UnitTests
+{
+    [TestFixture]
+    public class SimpleRotateTests
+    {
+        #region measure
+        
+        [Test]
+        public void Measure_Wrap()
+        {
+            TestPlan
+                .For(x => new SimpleRotate
+                {
+                    Child = x.CreateChild(),
+                    TurnCount = 0
+                })
+                .MeasureElement(new Size(400, 300))
+                .ExpectChildMeasure(new Size(400, 300), new Wrap())
+                .CheckMeasureResult(new Wrap());
+        }
+        
+        [Test]
+        public void Measure_PartialRender()
+        {
+            TestPlan
+                .For(x => new SimpleRotate
+                {
+                    Child = x.CreateChild(),
+                    TurnCount = 0
+                })
+                .MeasureElement(new Size(400, 300))
+                .ExpectChildMeasure(new Size(400, 300), new PartialRender(300, 200))
+                .CheckMeasureResult(new PartialRender(300, 200));
+        }
+        
+        [Test]
+        public void Measure_RotateRight()
+        {
+            TestPlan
+                .For(x => new SimpleRotate
+                {
+                    Child = x.CreateChild(),
+                    TurnCount = 1
+                })
+                .MeasureElement(new Size(400, 300))
+                .ExpectChildMeasure(new Size(300, 400), new FullRender(200, 300))
+                .CheckMeasureResult(new FullRender(300, 200));
+        }
+        
+        [Test]
+        public void Measure_RotateFlip()
+        {
+            TestPlan
+                .For(x => new SimpleRotate
+                {
+                    Child = x.CreateChild(),
+                    TurnCount = 2
+                })
+                .MeasureElement(new Size(400, 300))
+                .ExpectChildMeasure(new Size(400, 300), new FullRender(200, 100))
+                .CheckMeasureResult(new FullRender(200, 100));
+        }
+        
+        [Test]
+        public void Measure_RotateLeft()
+        {
+            TestPlan
+                .For(x => new SimpleRotate
+                {
+                    Child = x.CreateChild(),
+                    TurnCount = 3 // or -1
+                })
+                .MeasureElement(new Size(500, 400))
+                .ExpectChildMeasure(new Size(400, 500), new FullRender(300, 350))
+                .CheckMeasureResult(new FullRender(350, 300));
+        }
+        
+        #endregion
+        
+        #region draw
+        
+        [Test]
+        public void Draw_Simple()
+        {
+            TestPlan
+                .For(x => new SimpleRotate
+                {
+                    Child = x.CreateChild(),
+                    TurnCount = 0
+                })
+                .DrawElement(new Size(640, 480))
+                .ExpectCanvasTranslate(0, 0)
+                .ExpectCanvasRotate(0)
+                .ExpectChildDraw(new Size(640, 480))
+                .ExpectCanvasRotate(0)
+                .ExpectCanvasTranslate(0, 0)
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Draw_RotateRight()
+        {
+            TestPlan 
+                .For(x => new SimpleRotate
+                {
+                    Child = x.CreateChild(),
+                    TurnCount = 1
+                })
+                .DrawElement(new Size(640, 480))
+                .ExpectCanvasTranslate(640, 0)
+                .ExpectCanvasRotate(90)
+                .ExpectChildDraw(new Size(480, 640))
+                .ExpectCanvasRotate(-90)
+                .ExpectCanvasTranslate(-640, 0)
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Draw_RotateFlip()
+        {
+            TestPlan 
+                .For(x => new SimpleRotate
+                {
+                    Child = x.CreateChild(),
+                    TurnCount = 2
+                })
+                .DrawElement(new Size(640, 480))
+                .ExpectCanvasTranslate(640, 480)
+                .ExpectCanvasRotate(180)
+                .ExpectChildDraw(new Size(640, 480))
+                .ExpectCanvasRotate(-180)
+                .ExpectCanvasTranslate(-640, -480)
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Draw_RotateLeft()
+        {
+            TestPlan 
+                .For(x => new SimpleRotate
+                {
+                    Child = x.CreateChild(),
+                    TurnCount = 3 // or -1
+                })
+                .DrawElement(new Size(640, 480))
+                .ExpectCanvasTranslate(0, 480)
+                .ExpectCanvasRotate(270)
+                .ExpectChildDraw(new Size(480, 640))
+                .ExpectCanvasRotate(-270)
+                .ExpectCanvasTranslate(0, -480)
+                .CheckDrawResult();
+        }
+        
+        #endregion
+    }
+}

+ 76 - 2
QuestPDF.UnitTests/StackTests.cs

@@ -229,7 +229,81 @@ namespace QuestPDF.UnitTests
         
         #endregion
         
-        // TODO: add tests for the spacing property
-        // TODO: add tests for the tree builder method
+        #region Structure
+        
+        [Test]
+        public void Structure_Simple()
+        { 
+            // arrange
+            var childA = TestPlan.CreateUniqueElement();
+            var childB = TestPlan.CreateUniqueElement();
+            var childC = TestPlan.CreateUniqueElement();
+            var childD = TestPlan.CreateUniqueElement();
+            var childE = TestPlan.CreateUniqueElement();
+
+            const int spacing = 20;
+            
+            // act
+            var structure = new Container();
+            
+            structure.Stack(stack =>
+            {
+                stack.Spacing(spacing);
+                
+                stack.Item().Element(childA);
+                stack.Item().Element(childB);
+                stack.Item().Element(childC);
+                stack.Item().Element(childD);
+                stack.Item().Element(childE);
+            });
+            
+            // assert
+            var expected = new Padding
+            {
+                Bottom = -spacing,
+
+                Child = new BinaryStack
+                {
+                    First = new BinaryStack
+                    {
+                        First = new Padding
+                        {
+                            Bottom = spacing,
+                            Child = childA
+                        },
+                        Second = new Padding
+                        {
+                            Bottom = spacing,
+                            Child = childB
+                        }
+                    },
+                    Second = new BinaryStack
+                    {
+                        First = new Padding
+                        {
+                            Bottom = spacing,
+                            Child = childC
+                        },
+                        Second = new BinaryStack
+                        {
+                            First = new Padding
+                            {
+                                Bottom = spacing,
+                                Child = childD
+                            },
+                            Second = new Padding
+                            {
+                                Bottom = spacing,
+                                Child = childE
+                            }
+                        }
+                    }
+                }
+            };
+
+            TestPlan.CompareOperations(structure, expected);
+        }
+        
+        #endregion
     }
 }

+ 9 - 25
QuestPDF.UnitTests/TestEngine/CanvasMock.cs → QuestPDF.UnitTests/TestEngine/MockCanvas.cs

@@ -4,41 +4,25 @@ using SkiaSharp;
 
 namespace QuestPDF.UnitTests.TestEngine
 {
-    internal class CanvasMock : ICanvas
+    internal class MockCanvas : ICanvas
     {
         public Action<Position> TranslateFunc { get; set; }
+        public Action<float> RotateFunc { get; set; }
+        public Action<float, float> ScaleFunc { 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 void Translate(Position vector) => TranslateFunc(vector);
+        public void Rotate(float angle) => RotateFunc(angle);
+        public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);
+
         public void 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 DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
         
-        public void DrawExternalLink(string url, Size size)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void DrawLocationLink(string locationName, Size size)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void DrawLocation(string locationName)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void DrawLink(string url, Size size)
-        {
-            throw new NotImplementedException();
-        }
-
-        public Size MeasureText(string text, TextStyle style)
-        {
-            return new Size(text.Length * style.Size, style.Size);
-        }
+        public void DrawExternalLink(string url, Size size) => throw new NotImplementedException();
+        public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException();
+        public void DrawLocation(string locationName) => throw new NotImplementedException();
     }
 }

+ 25 - 0
QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using QuestPDF.Infrastructure;
+using QuestPDF.UnitTests.TestEngine.Operations;
+using SkiaSharp;
+
+namespace QuestPDF.UnitTests.TestEngine
+{
+    internal class OperationRecordingCanvas : ICanvas
+    {
+        public ICollection<OperationBase> Operations { get; } = new List<OperationBase>();
+
+        public void Translate(Position vector) => Operations.Add(new CanvasTranslateOperation(vector));
+        public void Rotate(float angle) => Operations.Add(new CanvasRotateOperation(angle));
+        public void Scale(float scaleX, float scaleY) => Operations.Add(new CanvasScaleOperation(scaleX, scaleY));
+
+        public void 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 DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
+        
+        public void DrawExternalLink(string url, Size size) => throw new NotImplementedException();
+        public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException();
+        public void DrawLocation(string locationName) => throw new NotImplementedException();
+    }
+}

+ 2 - 2
QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawImageOperation.cs

@@ -2,12 +2,12 @@
 
 namespace QuestPDF.UnitTests.TestEngine.Operations
 {
-    internal class CanvasDrawImageOperationBase : OperationBase
+    internal class CanvasDrawImageOperation : OperationBase
     {
         public Position Position { get; }
         public Size Size { get; }
 
-        public CanvasDrawImageOperationBase(Position position, Size size)
+        public CanvasDrawImageOperation(Position position, Size size)
         {
             Position = position;
             Size = size;

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

@@ -2,13 +2,13 @@
 
 namespace QuestPDF.UnitTests.TestEngine.Operations
 {
-    internal class CanvasDrawRectangleOperationBase : OperationBase
+    internal class CanvasDrawRectangleOperation : OperationBase
     {
         public Position Position { get; } 
         public Size Size { get; }
         public string Color { get; }
 
-        public CanvasDrawRectangleOperationBase(Position position, Size size, string color)
+        public CanvasDrawRectangleOperation(Position position, Size size, string color)
         {
             Position = position;
             Size = size;

+ 2 - 2
QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawTextOperation.cs

@@ -2,13 +2,13 @@
 
 namespace QuestPDF.UnitTests.TestEngine.Operations
 {
-    internal class CanvasDrawTextOperationBase : OperationBase
+    internal class CanvasDrawTextOperation : OperationBase
     {
         public string Text { get; }
         public Position Position { get; }
         public TextStyle Style { get; }
 
-        public CanvasDrawTextOperationBase(string text, Position position, TextStyle style)
+        public CanvasDrawTextOperation(string text, Position position, TextStyle style)
         {
             Text = text;
             Position = position;

+ 12 - 0
QuestPDF.UnitTests/TestEngine/Operations/CanvasRotateOperation.cs

@@ -0,0 +1,12 @@
+namespace QuestPDF.UnitTests.TestEngine.Operations
+{
+    public class CanvasRotateOperation : OperationBase
+    {
+        public float Angle { get; }
+
+        public CanvasRotateOperation(float angle)
+        {
+            Angle = angle;
+        }
+    }
+}

+ 14 - 0
QuestPDF.UnitTests/TestEngine/Operations/CanvasScaleOperation.cs

@@ -0,0 +1,14 @@
+namespace QuestPDF.UnitTests.TestEngine.Operations
+{
+    public class CanvasScaleOperation : OperationBase
+    {
+        public float ScaleX { get; }
+        public float ScaleY { get; }
+
+        public CanvasScaleOperation(float scaleX, float scaleY)
+        {
+            ScaleX = scaleX;
+            ScaleY = scaleY;
+        }
+    }
+}

+ 2 - 2
QuestPDF.UnitTests/TestEngine/Operations/CanvasTranslateOperation.cs

@@ -2,11 +2,11 @@
 
 namespace QuestPDF.UnitTests.TestEngine.Operations
 {
-    internal class CanvasTranslateOperationBase : OperationBase
+    internal class CanvasTranslateOperation : OperationBase
     {
         public Position Position { get; }
 
-        public CanvasTranslateOperationBase(Position position)
+        public CanvasTranslateOperation(Position position)
         {
             Position = position;
         }

+ 2 - 2
QuestPDF.UnitTests/TestEngine/Operations/ChildDrawOperation.cs

@@ -2,12 +2,12 @@
 
 namespace QuestPDF.UnitTests.TestEngine.Operations
 {
-    public class ChildDrawOperationBase : OperationBase
+    public class ChildDrawOperation : OperationBase
     {
         public string ChildId { get; }
         public Size Input { get; }
 
-        public ChildDrawOperationBase(string childId, Size input)
+        public ChildDrawOperation(string childId, Size input)
         {
             ChildId = childId;
             Input = input;

+ 2 - 2
QuestPDF.UnitTests/TestEngine/Operations/ElementMeasureOperation.cs

@@ -2,9 +2,9 @@
 
 namespace QuestPDF.UnitTests.TestEngine.Operations
 {
-    public class ElementMeasureOperationBase : OperationBase
+    public class ElementMeasureOperation : OperationBase
     {
-        public ElementMeasureOperationBase(Size input)
+        public ElementMeasureOperation(Size input)
         {
             
         }

+ 68 - 0
QuestPDF.UnitTests/TestEngine/SimpleContainerTests.cs

@@ -0,0 +1,68 @@
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.UnitTests.TestEngine
+{
+    internal static class SimpleContainerTests
+    {
+        #region measure
+        
+        public static void Measure<TElement>() where TElement : Element, IContainer, new()
+        {
+            Measure_Wrap<TElement>();
+            Measure_PartialRender<TElement>();
+            Measure_FullRender<TElement>();
+        }
+        
+        private static void Measure_Wrap<TElement>() where TElement : Element, IContainer, new()
+        {
+            TestPlan
+                .For(x => new TElement
+                {
+                    Child = x.CreateChild()
+                })
+                .MeasureElement(new Size(400, 300))
+                .ExpectChildMeasure(new Size(400, 300), new Wrap())
+                .CheckMeasureResult(new Wrap());
+        }
+        
+        private static void Measure_PartialRender<TElement>() where TElement : Element, IContainer, new()
+        {
+            TestPlan
+                .For(x => new TElement
+                {
+                    Child = x.CreateChild()
+                })
+                .MeasureElement(new Size(400, 300))
+                .ExpectChildMeasure(new Size(400, 300), new PartialRender(200, 100))
+                .CheckMeasureResult(new PartialRender(200, 100));
+        }
+        
+        private static void Measure_FullRender<TElement>() where TElement : Element, IContainer, new()
+        {
+            TestPlan
+                .For(x => new TElement
+                {
+                    Child = x.CreateChild()
+                })
+                .MeasureElement(new Size(400, 300))
+                .ExpectChildMeasure(new Size(400, 300), new FullRender(250, 150))
+                .CheckMeasureResult(new FullRender(250, 150));
+        }
+        
+        #endregion
+        
+        public static void Draw<TElement>() where TElement : Element, IContainer, new()
+        {
+            TestPlan
+                .For(x => new TElement
+                {
+                    Child = x.CreateChild()
+                })
+                .DrawElement(new Size(1200, 900))
+                .ExpectChildDraw(new Size(1200, 900))
+                .CheckDrawResult();
+        }
+    }
+}

+ 0 - 31
QuestPDF.UnitTests/TestEngine/SingleChildTests.cs

@@ -1,31 +0,0 @@
-using FluentAssertions;
-using NUnit.Framework;
-using QuestPDF.Drawing;
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.UnitTests.TestEngine
-{
-    public static class SingleChildTests
-    {
-        internal static void MeasureWithoutChild<T>(this T element) where T : ContainerElement
-        {
-            element.Child = null;
-            element.Measure(Size.Zero).Should().BeEquivalentTo(SpacePlan.FullRender(0, 0));
-        }
-        
-        internal static void DrawWithoutChild<T>(this T element) where T : ContainerElement
-        {
-            // component does not throw an exception when called with null child
-            Assert.DoesNotThrow(() =>
-            {
-                element.Child = null;
-                
-                // component does not perform any canvas operation when called with null child
-                TestPlan
-                    .For(x => element)
-                    .DrawElement(new Size(200, 100))
-                    .CheckDrawResult();
-            });
-        }
-    }
-}

+ 84 - 37
QuestPDF.UnitTests/TestEngine/TestPlan.cs

@@ -1,17 +1,19 @@
 using System;
 using System.Collections.Generic;
 using System.Text.Json;
+using FluentAssertions;
 using NUnit.Framework;
 using QuestPDF.Drawing;
 using QuestPDF.Elements;
-using QuestPDF.Infrastructure;
-using QuestPDF.UnitTests.TestEngine.Operations;
+using QuestPDF.Helpers;
 
 namespace QuestPDF.UnitTests.TestEngine
 {
     internal class TestPlan
     {
         private const string DefaultChildName = "child";
+
+        private static Random Random { get; } = new Random();
         
         private Element Element { get; set; }
         private ICanvas Canvas { get; }
@@ -44,20 +46,31 @@ namespace QuestPDF.UnitTests.TestEngine
         
         private ICanvas CreateCanvas()
         {
-            return new CanvasMock
+            return new MockCanvas
             {
                 TranslateFunc = position =>
                 {
-                    var expected = GetExpected<CanvasTranslateOperationBase>();
+                    var expected = GetExpected<CanvasTranslateOperation>();
 
                     Assert.AreEqual(expected.Position.X, position.X, "Translate X");
                     Assert.AreEqual(expected.Position.Y, position.Y, "Translate Y");
-                    
-                    //position.Should().BeEquivalentTo(expected.Position);
+                },
+                RotateFunc = angle =>
+                {
+                    var expected = GetExpected<CanvasRotateOperation>();
+
+                    Assert.AreEqual(expected.Angle, angle, "Rotate angle");
+                },
+                ScaleFunc = (scaleX, scaleY) =>
+                {
+                    var expected = GetExpected<CanvasScaleOperation>();
+
+                    Assert.AreEqual(expected.ScaleX, scaleX, "Scale X");
+                    Assert.AreEqual(expected.ScaleY, scaleY, "Scale Y");
                 },
                 DrawRectFunc = (position, size, color) =>
                 {
-                    var expected = GetExpected<CanvasDrawRectangleOperationBase>();
+                    var expected = GetExpected<CanvasDrawRectangleOperation>();
                     
                     Assert.AreEqual(expected.Position.X, position.X, "Draw rectangle: X");
                     Assert.AreEqual(expected.Position.Y, position.Y, "Draw rectangle: Y");
@@ -66,14 +79,10 @@ namespace QuestPDF.UnitTests.TestEngine
                     Assert.AreEqual(expected.Size.Height, size.Height, "Draw rectangle: height");
                     
                     Assert.AreEqual(expected.Color, color, "Draw rectangle: color");
-                    
-                    /*position.Should().BeEquivalentTo(expected.Position);
-                    size.Should().BeEquivalentTo(expected.Size);
-                    color.Should().Be(expected.Color);*/
                 },
                 DrawTextFunc = (text, position, style) => 
                 {
-                    var expected = GetExpected<CanvasDrawTextOperationBase>();
+                    var expected = GetExpected<CanvasDrawTextOperation>();
                     
                     Assert.AreEqual(expected.Text, text);
                     
@@ -83,23 +92,16 @@ namespace QuestPDF.UnitTests.TestEngine
                     Assert.AreEqual(expected.Style.Color, style.Color, "Draw text: color");
                     Assert.AreEqual(expected.Style.FontType, style.FontType, "Draw text: font");
                     Assert.AreEqual(expected.Style.Size, style.Size, "Draw text: size");
-
-                    /*text.Should().Be(expected.Text);
-                    position.Should().BeEquivalentTo(expected.Position);
-                    style.Should().BeEquivalentTo(expected.Style);*/
                 },
                 DrawImageFunc = (image, position, size) =>
                 {
-                    var expected = GetExpected<CanvasDrawImageOperationBase>();
+                    var expected = GetExpected<CanvasDrawImageOperation>();
                     
                     Assert.AreEqual(expected.Position.X, position.X, "Draw image: X");
                     Assert.AreEqual(expected.Position.Y, position.Y, "Draw image: Y");
                     
                     Assert.AreEqual(expected.Size.Width, size.Width, "Draw image: width");
                     Assert.AreEqual(expected.Size.Height, size.Height, "Draw image: height");
-                    
-                    /*position.Should().BeEquivalentTo(expected.Position);
-                    size.Should().BeEquivalentTo(expected.Size);*/
                 }
             };
         }
@@ -113,29 +115,23 @@ namespace QuestPDF.UnitTests.TestEngine
                 Id = id,
                 MeasureFunc = availableSpace =>
                 {
-                    var expected = GetExpected<ChildMeasureOperationBase>();
+                    var expected = GetExpected<ChildMeasureOperation>();
 
                     Assert.AreEqual(expected.ChildId, id);
                     
                     Assert.AreEqual(expected.Input.Width, availableSpace.Width, $"Measure: width of child '{expected.ChildId}'");
                     Assert.AreEqual(expected.Input.Height, availableSpace.Height, $"Measure: height of child '{expected.ChildId}'");
 
-                    // id.Should().Be(expected.ChildId);
-                    // availableSpace.Should().Be(expected.Input);
-
                     return expected.Output;
                 },
                 DrawFunc = availableSpace =>
                 {
-                    var expected = GetExpected<ChildDrawOperationBase>();
+                    var expected = GetExpected<ChildDrawOperation>();
 
                     Assert.AreEqual(expected.ChildId, id);
                     
                     Assert.AreEqual(expected.Input.Width, availableSpace.Width, $"Draw: width of child '{expected.ChildId}'");
                     Assert.AreEqual(expected.Input.Height, availableSpace.Height, $"Draw: width of child '{expected.ChildId}'");
-                    
-                    /*id.Should().Be(expected.ChildId);
-                    availableSpace.Should().Be(expected.Input);*/
                 }
             };
         }
@@ -165,7 +161,7 @@ namespace QuestPDF.UnitTests.TestEngine
         
         public TestPlan ExpectChildMeasure(string child, Size expectedInput, SpacePlan returns)
         {
-            return AddOperation(new ChildMeasureOperationBase(child, expectedInput, returns));
+            return AddOperation(new ChildMeasureOperation(child, expectedInput, returns));
         }
         
         public TestPlan ExpectChildDraw(Size expectedInput)
@@ -175,32 +171,42 @@ namespace QuestPDF.UnitTests.TestEngine
         
         public TestPlan ExpectChildDraw(string child, Size expectedInput)
         {
-            return AddOperation(new ChildDrawOperationBase(child, expectedInput));
+            return AddOperation(new ChildDrawOperation(child, expectedInput));
         }
 
         public TestPlan ExpectCanvasTranslate(Position position)
         {
-            return AddOperation(new CanvasTranslateOperationBase(position));
+            return AddOperation(new CanvasTranslateOperation(position));
         }
         
         public TestPlan ExpectCanvasTranslate(float left, float top)
         {
-            return AddOperation(new CanvasTranslateOperationBase(new Position(left, top)));
+            return AddOperation(new CanvasTranslateOperation(new Position(left, top)));
         }
 
+        public TestPlan ExpectCanvasScale(float scaleX, float scaleY)
+        {
+            return AddOperation(new CanvasScaleOperation(scaleX, scaleY));
+        }
+        
+        public TestPlan ExpectCanvasRotate(float angle)
+        {
+            return AddOperation(new CanvasRotateOperation(angle));
+        }
+        
         public TestPlan ExpectCanvasDrawRectangle(Position position, Size size, string color)
         {
-            return AddOperation(new CanvasDrawRectangleOperationBase(position, size, color));
+            return AddOperation(new CanvasDrawRectangleOperation(position, size, color));
         }
         
         public TestPlan ExpectCanvasDrawText(string text, Position position, TextStyle style)
         {
-            return AddOperation(new CanvasDrawTextOperationBase(text, position, style));
+            return AddOperation(new CanvasDrawTextOperation(text, position, style));
         }
         
         public TestPlan ExpectCanvasDrawImage(Position position, Size size)
         {
-            return AddOperation(new CanvasDrawImageOperationBase(position, size));
+            return AddOperation(new CanvasDrawImageOperation(position, size));
         }
         
         public TestPlan CheckMeasureResult(SpacePlan expected)
@@ -240,10 +246,51 @@ namespace QuestPDF.UnitTests.TestEngine
         
         public static Element CreateUniqueElement()
         {
-            return new Text
+            return new Constrained
             {
-                Value = Guid.NewGuid().ToString("N")
+                MinWidth = 90,
+                MinHeight = 60,
+                
+                Child = new DynamicImage
+                {
+                    Source = Placeholders.Image
+                }
             };
         }
+
+        public static void CompareOperations(Element value, Element expected, Size? availableSpace = null)
+        {
+            CompareMeasureOperations(value, expected, availableSpace);
+            CompareDrawOperations(value, expected, availableSpace);
+        }
+        
+        private static void CompareMeasureOperations(Element value, Element expected, Size? availableSpace = null)
+        {
+            availableSpace ??= new Size(400, 300);
+            
+            var canvas = new FreeCanvas();
+            value.HandleVisitor(x => x.Initialize(null, canvas));
+            var valueMeasure = value.Measure(availableSpace);
+            
+            expected.HandleVisitor(x => x.Initialize(null, canvas));
+            var expectedMeasure = expected.Measure(availableSpace);
+            
+            valueMeasure.Should().BeEquivalentTo(expectedMeasure);
+        }
+        
+        private static void CompareDrawOperations(Element value, Element expected, Size? availableSpace = null)
+        {
+            availableSpace ??= new Size(400, 300);
+            
+            var valueCanvas = new OperationRecordingCanvas();
+            value.HandleVisitor(x => x.Initialize(null, valueCanvas));
+            value.Draw(availableSpace);
+            
+            var expectedCanvas = new OperationRecordingCanvas();
+            expected.HandleVisitor(x => x.Initialize(null, expectedCanvas));
+            expected.Draw(availableSpace);
+            
+            valueCanvas.Operations.Should().BeEquivalentTo(expectedCanvas.Operations);
+        }
     }
 }

+ 22 - 0
QuestPDF.UnitTests/TestsBase.cs

@@ -0,0 +1,22 @@
+using FluentAssertions;
+using NUnit.Framework;
+
+namespace QuestPDF.UnitTests
+{
+    [SetUpFixture]
+    public class TestsBase
+    {
+        [OneTimeSetUp]
+        public void RunBeforeAnyTests()
+        {
+            AssertionOptions.AssertEquivalencyUsing(options => options
+                .IncludingNestedObjects()
+                .IncludingInternalProperties()
+                .IncludingInternalFields()
+                .AllowingInfiniteRecursion()
+                .RespectingRuntimeTypes()
+                .WithTracing()
+                .WithStrictOrdering());
+        }
+    }
+}

+ 0 - 10
QuestPDF.UnitTests/TextTests.cs

@@ -1,10 +0,0 @@
-using NUnit.Framework;
-
-namespace QuestPDF.UnitTests
-{
-    [TestFixture]
-    public class TextTests
-    {
-        
-    }
-}

+ 32 - 0
QuestPDF.UnitTests/TranslateTests.cs

@@ -0,0 +1,32 @@
+using NUnit.Framework;
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+using QuestPDF.UnitTests.TestEngine;
+
+namespace QuestPDF.UnitTests
+{
+    [TestFixture]
+    public class TranslateTests
+    {
+        [Test]
+        public void Measure() => SimpleContainerTests.Measure<Translate>();
+        
+        [Test]
+        public void Draw()
+        {
+            TestPlan
+                .For(x => new Translate
+                {
+                    Child = x.CreateChild(),
+                    TranslateX = 50,
+                    TranslateY = 75
+                })
+                .DrawElement(new Size(400, 300))
+                .ExpectCanvasTranslate(50, 75)
+                .ExpectChildDraw(new Size(400, 300))
+                .ExpectCanvasTranslate(-50, -75)
+                .CheckDrawResult();
+        }
+    }
+}

+ 100 - 0
QuestPDF.UnitTests/UnconstrainedTests.cs

@@ -0,0 +1,100 @@
+using NUnit.Framework;
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+using QuestPDF.UnitTests.TestEngine;
+
+namespace QuestPDF.UnitTests
+{
+    [TestFixture]
+    public class UnconstrainedTests
+    {
+        #region measure
+        
+        [Test]
+        public void Measure_Wrap()
+        {
+            TestPlan
+                .For(x => new Unconstrained
+                {
+                    Child = x.CreateChild()
+                })
+                .MeasureElement(new Size(900, 800))
+                .ExpectChildMeasure(Size.Max, new Wrap())
+                .CheckMeasureResult(new Wrap());
+        }
+        
+        [Test]
+        public void Measure_PartialRender()
+        {
+            TestPlan
+                .For(x => new Unconstrained
+                {
+                    Child = x.CreateChild()
+                })
+                .MeasureElement(new Size(900, 800))
+                .ExpectChildMeasure(Size.Max, new PartialRender(1200, 1600))
+                .CheckMeasureResult(new PartialRender(Size.Zero));
+        }
+        
+        [Test]
+        public void Measure_FullRender()
+        {
+            TestPlan
+                .For(x => new Unconstrained
+                {
+                    Child = x.CreateChild()
+                })
+                .MeasureElement(new Size(900, 800))
+                .ExpectChildMeasure(Size.Max, new FullRender(1200, 1600))
+                .CheckMeasureResult(new FullRender(Size.Zero));
+        }
+        
+        #endregion
+        
+        #region draw
+        
+        [Test]
+        public void Draw_SkipWhenChildWraps()
+        {
+            TestPlan
+                .For(x => new Unconstrained
+                {
+                    Child = x.CreateChild()
+                })
+                .DrawElement(new Size(900, 800))
+                .ExpectChildMeasure(Size.Max, new Wrap())
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Draw_WhenChildPartiallyRenders()
+        {
+            TestPlan
+                .For(x => new Unconstrained
+                {
+                    Child = x.CreateChild()
+                })
+                .DrawElement(new Size(900, 800))
+                .ExpectChildMeasure(Size.Max, new PartialRender(1200, 1600))
+                .ExpectChildDraw(new Size(1200, 1600))
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Draw_WhenChildFullyRenders()
+        {
+            TestPlan
+                .For(x => new Unconstrained
+                {
+                    Child = x.CreateChild()
+                })
+                .DrawElement(new Size(900, 800))
+                .ExpectChildMeasure(Size.Max, new FullRender(1600, 1000))
+                .ExpectChildDraw(new Size(1600, 1000))
+                .CheckDrawResult();
+        }
+        
+        #endregion
+    }
+}

+ 4 - 17
QuestPDF/Drawing/FontManager.cs

@@ -9,6 +9,7 @@ namespace QuestPDF.Drawing
     public static class FontManager
     {
         private static ConcurrentDictionary<string, SKTypeface> Typefaces = new ConcurrentDictionary<string, SKTypeface>();
+        private static ConcurrentDictionary<string, SKFontMetrics> FontMetrics = new ConcurrentDictionary<string, SKFontMetrics>();
         private static ConcurrentDictionary<string, SKPaint> Paints = new ConcurrentDictionary<string, SKPaint>();
         private static ConcurrentDictionary<string, SKPaint> ColorPaint = new ConcurrentDictionary<string, SKPaint>();
 
@@ -41,15 +42,7 @@ namespace QuestPDF.Drawing
                     Color = SKColor.Parse(style.Color),
                     Typeface = GetTypeface(style),
                     TextSize = style.Size,
-                    TextEncoding = SKTextEncoding.Utf32,
-                    
-                    TextAlign = style.Alignment switch
-                    {
-                        HorizontalAlignment.Left => SKTextAlign.Left,
-                        HorizontalAlignment.Center => SKTextAlign.Center,
-                        HorizontalAlignment.Right => SKTextAlign.Right,
-                        _ => SKTextAlign.Left
-                    }
+                    TextEncoding = SKTextEncoding.Utf32
                 };
             }
 
@@ -65,15 +58,9 @@ namespace QuestPDF.Drawing
             }
         }
 
-        internal static TextMeasurement BreakText(this TextStyle style, string text, float availableWidth)
+        internal static SKFontMetrics ToFontMetrics(this TextStyle style)
         {
-            var index = (int)style.ToPaint().BreakText(text, availableWidth, out var width);
-            
-            return new TextMeasurement()
-            {
-                LineIndex = index,
-                FragmentWidth = width
-            };
+            return FontMetrics.GetOrAdd(style.ToString(), key => style.ToPaint().FontMetrics);
         }
     }
 }

+ 10 - 0
QuestPDF/Drawing/FreeCanvas.cs

@@ -66,6 +66,16 @@ namespace QuestPDF.Drawing
             
         }
 
+        public void Rotate(float angle)
+        {
+            
+        }
+
+        public void Scale(float scaleX, float scaleY)
+        {
+            
+        }
+
         #endregion
     }
 }

+ 10 - 0
QuestPDF/Drawing/SkiaCanvasBase.cs

@@ -51,5 +51,15 @@ namespace QuestPDF.Drawing
         {
             Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), locationName);
         }
+
+        public void Rotate(float angle)
+        {
+            Canvas.RotateDegrees(angle);
+        }
+
+        public void Scale(float scaleX, float scaleY)
+        {
+            Canvas.Scale(scaleX, scaleY);
+        }
     }
 }

+ 13 - 0
QuestPDF/Drawing/TextRender.cs

@@ -0,0 +1,13 @@
+namespace QuestPDF.Drawing.SpacePlan
+{
+    internal class TextRender : FullRender
+    {
+        public float Ascent { get; set; }
+        public float Descent { get; set; }
+        
+        public TextRender(float width, float height) : base(width, height)
+        {
+            
+        }
+    }
+}

+ 1 - 1
QuestPDF/Elements/Grid.cs

@@ -16,7 +16,7 @@ namespace QuestPDF.Elements
     {
         public const int DefaultColumnsCount = 12;
         
-        public List<GridElement> Children { get; set; } = new List<GridElement>();
+        public List<GridElement> Children { get; } = new List<GridElement>();
         public Queue<GridElement> ChildrenQueue { get; set; } = new Queue<GridElement>();
         public int ColumnsCount { get; set; } = DefaultColumnsCount;
 

+ 0 - 52
QuestPDF/Elements/PageNumber.cs

@@ -1,52 +0,0 @@
-using System;
-using System.Text.RegularExpressions;
-using QuestPDF.Drawing;
-using QuestPDF.Infrastructure;
-using Size = QuestPDF.Infrastructure.Size;
-
-namespace QuestPDF.Elements
-{
-    internal class PageNumber : Element
-    {
-        public string TextFormat { get; set; } = "";
-        private Text TextElement { get; set; } = new Text();
-
-        public TextStyle? TextStyle
-        {
-            get => TextElement?.Style;
-            set => TextElement.Style = value;
-        }
-
-        internal override void HandleVisitor(Action<Element?> visit)
-        {
-            TextElement?.HandleVisitor(visit);
-            base.HandleVisitor(visit);
-        }
-
-        internal override SpacePlan Measure(Size availableSpace)
-        {
-            TextElement.Value = GetText();
-            return TextElement.Measure(availableSpace);
-        }
-
-        internal override void Draw(Size availableSpace)
-        {
-            TextElement.Value = GetText();
-            TextElement.Draw(availableSpace);
-        }
-
-        private string GetText()
-        {
-            var result = TextFormat;
-            
-            // replace known locations
-            foreach (var location in PageContext.GetRegisteredLocations())
-                result = result.Replace($"{{pdf:{location}}}", PageContext.GetLocationPage(location).ToString());
-
-            // placeholder unknown locations
-            result = Regex.Replace(result, @"{pdf:[ \w]+}", "123");
-            
-            return result;
-        }
-    }
-}

+ 3 - 4
QuestPDF/Elements/Placeholder.cs

@@ -18,16 +18,15 @@ namespace QuestPDF.Elements
         {
             container
                 .Background(Colors.Grey.Lighten2)
+                .Padding(5)
                 .AlignMiddle()
                 .AlignCenter()
-                .Padding(5)
-                .MaxHeight(32)
                 .Element(x =>
                 {
                     if (string.IsNullOrWhiteSpace(Text))
-                        x.Image(ImageData, ImageScaling.FitArea);
+                        x.MaxHeight(32).Image(ImageData, ImageScaling.FitArea);
                     else
-                        x.Text(Text, TextStyle.Default.Size(14).SemiBold());
+                        x.Text(Text, TextStyle.Default.Size(14));
                 });
         }
     }

+ 3 - 10
QuestPDF/Elements/Rotate.cs

@@ -8,16 +8,9 @@ namespace QuestPDF.Elements
 
         internal override void Draw(Size availableSpace)
         {
-            var skiaCanvas = (Canvas as Drawing.SkiaCanvasBase)?.Canvas;
-            
-            if (skiaCanvas == null)
-                return;
-            
-            var currentMatrix = skiaCanvas.TotalMatrix;
-            
-            skiaCanvas.RotateDegrees(Angle);
-            base.Draw(availableSpace);
-            skiaCanvas.SetMatrix(currentMatrix);
+            Canvas.Rotate(Angle);
+            Child?.Draw(availableSpace);
+            Canvas.Rotate(-Angle);
         }
     }
 }

+ 0 - 1
QuestPDF/Elements/Row.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using QuestPDF.Drawing;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Elements

+ 9 - 14
QuestPDF/Elements/Scale.cs

@@ -35,26 +35,21 @@ namespace QuestPDF.Elements
         
         internal override void Draw(Size availableSpace)
         {
-            var skiaCanvas = (Canvas as Drawing.SkiaCanvasBase)?.Canvas;
-            
-            if (skiaCanvas == null)
-                return;
-            
             var targetSpace = new Size(
                 Math.Abs(availableSpace.Width / ScaleX), 
                 Math.Abs(availableSpace.Height / ScaleY));
 
-            var currentMatrix = skiaCanvas.TotalMatrix;
-            
-            if (ScaleX < 0)
-                skiaCanvas.Translate(availableSpace.Width, 0);
+            var translate = new Position(
+                ScaleX < 0 ? availableSpace.Width : 0,
+                ScaleY < 0 ? availableSpace.Height : 0);
             
-            if (ScaleY < 0)
-                skiaCanvas.Translate(0, availableSpace.Height);
+            Canvas.Translate(translate);
+            Canvas.Scale(ScaleX, ScaleY);
             
-            skiaCanvas.Scale(ScaleX, ScaleY);
-            base.Draw(targetSpace);
-            skiaCanvas.SetMatrix(currentMatrix);
+            Child?.Draw(targetSpace);
+             
+            Canvas.Scale(1/ScaleX, 1/ScaleY);
+            Canvas.Translate(translate.Reverse());
         }
     }
 }

+ 10 - 14
QuestPDF/Elements/SimpleRotate.cs

@@ -33,26 +33,22 @@ namespace QuestPDF.Elements
         
         internal override void Draw(Size availableSpace)
         {
-            var skiaCanvas = (Canvas as Drawing.SkiaCanvasBase)?.Canvas;
-            
-            if (skiaCanvas == null)
-                return;
-
-            var currentMatrix = skiaCanvas.TotalMatrix;
+            var translate = new Position(
+                (NormalizedTurnCount == 1 || NormalizedTurnCount == 2) ? availableSpace.Width : 0,
+                (NormalizedTurnCount == 2 || NormalizedTurnCount == 3) ? availableSpace.Height : 0);
 
-            if (NormalizedTurnCount == 1 || NormalizedTurnCount == 2)
-                skiaCanvas.Translate(availableSpace.Width, 0);
+            var rotate = NormalizedTurnCount * 90;
             
-            if (NormalizedTurnCount == 2  || NormalizedTurnCount == 3)
-                skiaCanvas.Translate(0, availableSpace.Height);
-
-            skiaCanvas.RotateDegrees(NormalizedTurnCount * 90);
+            Canvas.Translate(translate);
+            Canvas.Rotate(rotate);
             
             if (NormalizedTurnCount == 1 || NormalizedTurnCount == 3)
                 availableSpace = new Size(availableSpace.Height, availableSpace.Width);
             
-            base.Draw(availableSpace);
-            skiaCanvas.SetMatrix(currentMatrix);
+            Child?.Draw(availableSpace);
+            
+            Canvas.Rotate(-rotate);
+            Canvas.Translate(translate.Reverse());
         }
     }
 }

+ 1 - 1
QuestPDF/Elements/Stack.cs

@@ -97,7 +97,7 @@ namespace QuestPDF.Elements
     
     internal class Stack : IComponent
     {
-        public ICollection<Element> Children { get; internal set; } = new List<Element>();
+        public ICollection<Element> Children { get; } = new List<Element>();
         public float Spacing { get; set; } = 0;
         
         public void Compose(IContainer container)

+ 0 - 116
QuestPDF/Elements/Text.cs

@@ -1,116 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using QuestPDF.Drawing;
-using QuestPDF.Infrastructure;
-using Size = QuestPDF.Infrastructure.Size;
-
-namespace QuestPDF.Elements
-{
-    internal class Text : Element
-    {
-        public string? Value { get; set; }
-        public TextStyle? Style { get; set; } = new TextStyle();
-
-        private float LineHeight => Style.Size * Style.LineHeight;
-
-        internal override SpacePlan Measure(Size availableSpace)
-        {
-            var lines = BreakLines(availableSpace.Width);
-            
-            var realWidth = lines
-                .Select(line => Style.BreakText(line, availableSpace.Width).FragmentWidth)
-                .DefaultIfEmpty(0)
-                .Max();
-            
-            var realHeight = lines.Count * LineHeight;
-            
-            if (realHeight > availableSpace.Height + Size.Epsilon)
-                return SpacePlan.Wrap();
-            
-            return SpacePlan.FullRender(realWidth, realHeight);
-        }
-
-        internal override void Draw(Size availableSpace)
-        {
-            var lines = BreakLines(availableSpace.Width);
-            
-            var offsetTop = 0f;
-            var offsetLeft = GetLeftOffset();
-
-            Canvas.Translate(new Position(0, Style.Size));
-            
-            foreach (var line in lines)
-            {
-                Canvas.DrawText(line, new Position(offsetLeft, offsetTop), Style);
-                offsetTop += LineHeight;
-            }
-            
-            Canvas.Translate(new Position(0, -Style.Size));
-
-            float GetLeftOffset()
-            {
-                return Style.Alignment switch
-                {
-                    HorizontalAlignment.Left => 0,
-                    HorizontalAlignment.Center => availableSpace.Width / 2,
-                    HorizontalAlignment.Right => availableSpace.Width,
-                    _ => throw new NotSupportedException()
-                };
-            }
-        }
-        
-        #region Word Wrap
-
-        private List<string> BreakLines(float maxWidth)
-        {
-            var lines = new List<string> ();
-
-            var remainingText = Value.Trim();
-
-            while(true)
-            {
-                if (string.IsNullOrEmpty(remainingText))
-                    break;
-                
-                var breakPoint = BreakLinePoint(remainingText, maxWidth);
-                
-                if (breakPoint == 0)
-                    break;
-                
-                var lastLine = remainingText.Substring(0, breakPoint).Trim();
-                lines.Add(lastLine);
-                
-                remainingText = remainingText.Substring(breakPoint).Trim();
-            }
-
-            return lines;
-        }
-
-        private int BreakLinePoint(string text, float width)
-        {
-            var index = 0;
-            var lengthBreak = Style.BreakText(text, width).LineIndex;
-            
-            while (index <= text.Length)
-            {
-                var next = text.IndexOfAny (new [] { ' ', '\n' }, index);
-                
-                if (next <= 0)
-                    return index == 0 || lengthBreak == text.Length ? lengthBreak : index;
-
-                if (next > lengthBreak)
-                    return index;
-
-                if (text[next] == '\n')
-                    return next;
-
-                index = next + 1;
-            }
-
-            return index;
-        }
-
-        #endregion
-    }
-}

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

@@ -0,0 +1,16 @@
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements.Text.Calculation
+{
+    internal 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; }
+    }
+}

+ 47 - 0
QuestPDF/Elements/Text/Calculation/TextLine.cs

@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using QuestPDF.Elements.Text.Items;
+
+namespace QuestPDF.Elements.Text.Calculation
+{
+    internal 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)
+            };
+        }
+    }
+}

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

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

+ 14 - 0
QuestPDF/Elements/Text/Calculation/TextMeasurementRequest.cs

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

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

@@ -0,0 +1,22 @@
+using System;
+
+namespace QuestPDF.Elements.Text.Calculation
+{
+    internal 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;
+    }
+}

+ 10 - 0
QuestPDF/Elements/Text/Items/ITextBlockItem.cs

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

+ 48 - 0
QuestPDF/Elements/Text/Items/TextBlockElement.cs

@@ -0,0 +1,48 @@
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Elements.Text.Calculation;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements.Text.Items
+{
+    internal class TextBlockElement : ITextBlockItem
+    {
+        public Element Element { get; set; } = Empty.Instance;
+        
+        public TextMeasurementResult? Measure(TextMeasurementRequest request)
+        {
+            Element.HandleVisitor(x => (x as IStateResettable)?.ResetState());
+            Element.HandleVisitor(x => x.Initialize(request.PageContext, request.Canvas));
+
+            var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height));
+
+            if (measurement is Wrap || measurement is PartialRender)
+                return null;
+
+            var elementSize = measurement as Size;
+            
+            return new TextMeasurementResult
+            {
+                Width = elementSize.Width,
+                
+                Ascent = -elementSize.Height,
+                Descent = 0,
+                
+                LineHeight = 1,
+                
+                StartIndex = 0,
+                EndIndex = 0,
+                TotalIndex = 0
+            };
+        }
+
+        public void Draw(TextDrawingRequest request)
+        {
+            Element.HandleVisitor(x => (x as IStateResettable)?.ResetState());
+            Element.HandleVisitor(x => x.Initialize(request.PageContext, request.Canvas));
+            
+            request.Canvas.Translate(new Position(0, request.TotalAscent));
+            Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
+            request.Canvas.Translate(new Position(0, -request.TotalAscent));
+        }
+    }
+}

+ 35 - 0
QuestPDF/Elements/Text/Items/TextBlockExternalLink.cs

@@ -0,0 +1,35 @@
+using QuestPDF.Elements.Text.Calculation;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements.Text.Items
+{
+    internal class TextBlockExternalLink : ITextBlockItem
+    {
+        public TextStyle Style { get; set; } = new TextStyle();
+        public string Text { get; set; }
+        public string Url { get; set; }
+        
+        public TextMeasurementResult? Measure(TextMeasurementRequest request)
+        {
+            return GetItem().MeasureWithoutCache(request);
+        }
+
+        public void Draw(TextDrawingRequest request)
+        {
+            request.Canvas.Translate(new Position(0, request.TotalAscent));
+            request.Canvas.DrawExternalLink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
+            request.Canvas.Translate(new Position(0, -request.TotalAscent));
+            
+            GetItem().Draw(request);
+        }
+
+        private TextBlockSpan GetItem()
+        {
+            return new TextBlockSpan
+            {
+                Style = Style,
+                Text = Text
+            };
+        }
+    }
+}

+ 35 - 0
QuestPDF/Elements/Text/Items/TextBlockInternalLink.cs

@@ -0,0 +1,35 @@
+using QuestPDF.Elements.Text.Calculation;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements.Text.Items
+{
+    internal class TextBlockInternalLink : ITextBlockItem
+    {
+        public TextStyle Style { get; set; } = new TextStyle();
+        public string Text { get; set; }
+        public string LocationName { get; set; }
+        
+        public TextMeasurementResult? Measure(TextMeasurementRequest request)
+        {
+            return GetItem().MeasureWithoutCache(request);
+        }
+
+        public void Draw(TextDrawingRequest request)
+        {
+            request.Canvas.Translate(new Position(0, request.TotalAscent));
+            request.Canvas.DrawLocationLink(LocationName, new Size(request.TextSize.Width, request.TextSize.Height));
+            request.Canvas.Translate(new Position(0, -request.TotalAscent));
+            
+            GetItem().Draw(request);
+        }
+
+        private TextBlockSpan GetItem()
+        {
+            return new TextBlockSpan
+            {
+                Style = Style,
+                Text = Text
+            };
+        }
+    }
+}

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

@@ -0,0 +1,36 @@
+using QuestPDF.Elements.Text.Calculation;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements.Text.Items
+{
+    internal class TextBlockPageNumber : ITextBlockItem
+    {
+        public TextStyle Style { get; set; } = new TextStyle();
+        public string SlotName { get; set; }
+        
+        public TextMeasurementResult? Measure(TextMeasurementRequest request)
+        {
+            return GetItem(request.PageContext).MeasureWithoutCache(request);
+        }
+
+        public void Draw(TextDrawingRequest request)
+        {
+            GetItem(request.PageContext).Draw(request);
+        }
+
+        private TextBlockSpan GetItem(IPageContext context)
+        {
+            var pageNumberPlaceholder = 123;
+            
+            var pageNumber = context.GetRegisteredLocations().Contains(SlotName)
+                ? context.GetLocationPage(SlotName)
+                : pageNumberPlaceholder;
+            
+            return new TextBlockSpan
+            {
+                Style = Style,
+                Text = pageNumber.ToString()
+            };
+        }
+    }
+}

+ 131 - 0
QuestPDF/Elements/Text/Items/TextBlockSpan.cs

@@ -0,0 +1,131 @@
+using System.Collections.Generic;
+using QuestPDF.Drawing;
+using QuestPDF.Elements.Text.Calculation;
+using QuestPDF.Infrastructure;
+using Size = QuestPDF.Infrastructure.Size;
+
+namespace QuestPDF.Elements.Text.Items
+{
+    internal class TextBlockSpan : ITextBlockItem
+    {
+        public string Text { get; set; }
+        public TextStyle Style { get; set; } = new TextStyle();
+
+        private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache =
+            new Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?>();
+
+        public TextMeasurementResult? Measure(TextMeasurementRequest request)
+        {
+            var cacheKey = (request.StartIndex, request.AvailableWidth);
+            
+            if (!MeasureCache.ContainsKey(cacheKey))
+                MeasureCache[cacheKey] = MeasureWithoutCache(request);
+            
+            return MeasureCache[cacheKey];
+        }
+        
+        internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
+        {
+            const char space = ' ';
+            
+            var paint = Style.ToPaint();
+            var fontMetrics = Style.ToFontMetrics();
+
+            var startIndex = request.StartIndex;
+            
+            if (request.IsFirstLineElement)
+            {
+                while (startIndex + 1 < Text.Length && Text[startIndex] == space)
+                    startIndex++;
+            }
+
+            if (Text.Length == 0)
+            {
+                return new TextMeasurementResult
+                {
+                    Width = 0,
+                    
+                    LineHeight = Style.LineHeight,
+                    Ascent = fontMetrics.Ascent,
+                    Descent = fontMetrics.Descent
+                };
+            }
+            
+            // start breaking text from requested position
+            var text = Text.Substring(startIndex);
+            
+            var textLength = (int)paint.BreakText(text, request.AvailableWidth);
+
+            if (textLength <= 0)
+                return null;
+
+            if (textLength < text.Length && text[textLength] == space)
+                textLength++;
+            
+            // break text only on spaces
+            if (textLength < text.Length)
+            {
+                var lastSpaceIndex = text.Substring(0, textLength).LastIndexOf(space) - 1;
+
+                if (lastSpaceIndex <= 0)
+                {
+                    if (!request.IsFirstLineElement)
+                        return null;
+                }
+                else
+                {
+                    textLength = lastSpaceIndex + 1;
+                }
+            }
+
+            text = text.Substring(0, textLength);
+
+            var endIndex = startIndex + textLength;
+            var nextIndex = endIndex;
+
+            while (nextIndex + 1 < Text.Length && Text[nextIndex] == space)
+                nextIndex++;
+            
+            // measure final text
+            var width = paint.MeasureText(text);
+            
+            return new TextMeasurementResult
+            {
+                Width = width,
+                
+                Ascent = fontMetrics.Ascent,
+                Descent = fontMetrics.Descent,
+     
+                LineHeight = Style.LineHeight,
+                
+                StartIndex = startIndex,
+                EndIndex = endIndex,
+                NextIndex = nextIndex,
+                TotalIndex = Text.Length
+            };
+        }
+        
+        public void Draw(TextDrawingRequest request)
+        {
+            var fontMetrics = Style.ToFontMetrics();
+
+            var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
+            
+            request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
+            request.Canvas.DrawText(text, Position.Zero, Style);
+
+            // draw underline
+            if (Style.HasUnderline && fontMetrics.UnderlinePosition.HasValue)
+                DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness.Value);
+            
+            // draw stroke
+            if (Style.HasStrikethrough && fontMetrics.StrikeoutPosition.HasValue)
+                DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness.Value);
+
+            void DrawLine(float offset, float thickness)
+            {
+                request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
+            }
+        }
+    }
+}

+ 199 - 0
QuestPDF/Elements/Text/TextBlock.cs

@@ -0,0 +1,199 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Elements.Text.Calculation;
+using QuestPDF.Elements.Text.Items;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements.Text
+{
+    internal class TextBlock : Element, IStateResettable
+    {
+        public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
+        public List<ITextBlockItem> Children { get; set; } = new List<ITextBlockItem>();
+
+        public Queue<ITextBlockItem> RenderingQueue { get; set; }
+        public int CurrentElementIndex { get; set; }
+
+        public void ResetState()
+        {
+            RenderingQueue = new Queue<ITextBlockItem>(Children);
+            CurrentElementIndex = 0;
+        }
+
+        internal override ISpacePlan Measure(Size availableSpace)
+        {
+            if (!RenderingQueue.Any())
+                return new FullRender(Size.Zero);
+            
+            var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
+
+            if (!lines.Any())
+                return new PartialRender(Size.Zero);
+            
+            var width = lines.Max(x => x.Width);
+            var height = lines.Sum(x => x.LineHeight);
+
+            if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
+                return new Wrap();
+
+            var fullyRenderedItemsCount = lines
+                .SelectMany(x => x.Elements)
+                .GroupBy(x => x.Item)
+                .Count(x => x.Any(y => y.Measurement.IsLast));
+            
+            if (fullyRenderedItemsCount == RenderingQueue.Count)
+                return new FullRender(width, height);
+            
+            return new PartialRender(width, height);
+        }
+
+        internal override void Draw(Size availableSpace)
+        {
+            var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
+            
+            if (!lines.Any())
+                return;
+            
+            var heightOffset = 0f;
+            var widthOffset = 0f;
+            
+            foreach (var line in lines)
+            {
+                widthOffset = 0f;
+
+                var alignmentOffset = GetAlignmentOffset(line.Width);
+                
+                Canvas.Translate(new Position(alignmentOffset, 0));
+                Canvas.Translate(new Position(0, -line.Ascent));
+            
+                foreach (var item in line.Elements)
+                {
+                    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
+                    };
+                
+                    item.Item.Draw(textDrawingRequest);
+                
+                    Canvas.Translate(new Position(item.Measurement.Width, 0));
+                    widthOffset += item.Measurement.Width;
+                }
+            
+                Canvas.Translate(new Position(-alignmentOffset, 0));
+                Canvas.Translate(new Position(-line.Width, line.Ascent));
+                Canvas.Translate(new Position(0, line.LineHeight));
+                
+                heightOffset += line.LineHeight;
+            }
+            
+            Canvas.Translate(new Position(0, -heightOffset));
+            
+            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();
+            
+            float GetAlignmentOffset(float lineWidth)
+            {
+                if (Alignment == HorizontalAlignment.Left)
+                    return 0;
+
+                var emptySpace = availableSpace.Width - lineWidth;
+
+                if (Alignment == HorizontalAlignment.Right)
+                    return emptySpace;
+
+                if (Alignment == HorizontalAlignment.Center)
+                    return emptySpace / 2;
+
+                throw new ArgumentException();
+            }
+        }
+
+        public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
+        {
+            var queue = new Queue<ITextBlockItem>(RenderingQueue);
+            var currentItemIndex = CurrentElementIndex;
+            var currentHeight = 0f;
+
+            while (queue.Any())
+            {
+                var line = GetNextLine();
+                
+                if (!line.Elements.Any())
+                    yield break;
+                
+                if (currentHeight + line.LineHeight > availableHeight + Size.Epsilon)
+                    yield break;
+
+                currentHeight += line.LineHeight;
+                yield return line;
+            }
+
+            TextLine GetNextLine()
+            {
+                var currentWidth = 0f;
+
+                var currentLineElements = new List<TextLineElement>();
+            
+                while (true)
+                {
+                    if (!queue.Any())
+                        break;
+
+                    var currentElement = queue.Peek();
+                    
+                    var measurementRequest = new TextMeasurementRequest
+                    {
+                        Canvas = Canvas,
+                        PageContext = PageContext,
+                        
+                        StartIndex = currentItemIndex,
+                        AvailableWidth = availableWidth - currentWidth,
+                        IsFirstLineElement = !currentLineElements.Any()
+                    };
+                
+                    var measurementResponse = currentElement.Measure(measurementRequest);
+                
+                    if (measurementResponse == null)
+                        break;
+                    
+                    currentLineElements.Add(new TextLineElement
+                    {
+                        Item = currentElement,
+                        Measurement = measurementResponse
+                    });
+
+                    currentWidth += measurementResponse.Width;
+                    currentItemIndex = measurementResponse.NextIndex;
+                    
+                    if (!measurementResponse.IsLast)
+                        break;
+
+                    currentItemIndex = 0;
+                    queue.Dequeue();
+                }
+
+                return TextLine.From(currentLineElements);
+            }
+        }
+    }
+}

+ 3 - 6
QuestPDF/Elements/Translate.cs

@@ -10,14 +10,11 @@ namespace QuestPDF.Elements
 
         internal override void Draw(Size availableSpace)
         {
-            var skiaCanvas = (Canvas as Drawing.SkiaCanvasBase)?.Canvas;
+            var translate = new Position(TranslateX, TranslateY);
             
-            if (skiaCanvas == null)
-                return;
-            
-            skiaCanvas.Translate(TranslateX, TranslateY);
+            Canvas.Translate(translate);
             base.Draw(availableSpace);
-            skiaCanvas.Translate(-TranslateX, -TranslateY);
+            Canvas.Translate(translate.Reverse());
         }
     }
 }

+ 0 - 27
QuestPDF/Fluent/ElementExtensions.cs

@@ -40,15 +40,6 @@ namespace QuestPDF.Fluent
         {
             return handler(parent.Container()).Container();
         }
-
-        public static void PageNumber(this IContainer element, string textFormat = "{pdf:currentPage} / {pdf:totalPages}", TextStyle? style = null)
-        {
-            element.Element(new PageNumber
-            {
-                TextFormat = textFormat,
-                TextStyle = style ?? TextStyle.Default
-            });
-        }
         
         public static IContainer AspectRatio(this IContainer element, float ratio, AspectRatioOption option = AspectRatioOption.FitWidth)
         {
@@ -92,25 +83,7 @@ namespace QuestPDF.Fluent
                 MinHeight = minHeight
             });
         }
-        
-        public static void Text(this IContainer element, object text, TextStyle? style = null)
-        {
-            text ??= string.Empty;
-            style ??= TextStyle.Default;
 
-            if (element is Alignment alignment)
-            {
-                style = style.Clone();
-                style.Alignment = alignment.Horizontal;
-            }
-            
-            element.Element(new Text
-            {
-                Value = text.ToString(),
-                Style = style
-            });
-        }
-        
         public static void PageBreak(this IContainer element)
         {
             element.Element(new PageBreak());

+ 188 - 0
QuestPDF/Fluent/TextExtensions.cs

@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using QuestPDF.Elements;
+using QuestPDF.Elements.Text;
+using QuestPDF.Elements.Text.Items;
+using QuestPDF.Infrastructure;
+using static System.String;
+
+namespace QuestPDF.Fluent
+{
+    public class TextDescriptor
+    {
+        private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
+        private TextStyle DefaultStyle { get; set; } = TextStyle.Default;
+        private HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
+        private float Spacing { get; set; } = 0f;
+
+        public void DefaultTextStyle(TextStyle style)
+        {
+            DefaultStyle = style;
+        }
+        
+        public void AlignLeft()
+        {
+            Alignment = HorizontalAlignment.Left;
+        }
+        
+        public void AlignCenter()
+        {
+            Alignment = HorizontalAlignment.Center;
+        }
+        
+        public void AlignRight()
+        {
+            Alignment = HorizontalAlignment.Right;
+        }
+
+        public void ParagraphSpacing(float value)
+        {
+            Spacing = value;
+        }
+
+        private void AddItemToLastTextBlock(ITextBlockItem item)
+        {
+            if (!TextBlocks.Any())
+                TextBlocks.Add(new TextBlock());
+            
+            TextBlocks.Last().Children.Add(item);
+        }
+        
+        public void Span(string text, TextStyle? style = null)
+        {
+            style ??= DefaultStyle;
+ 
+            var items = text
+                .Replace("\r", string.Empty)
+                .Split(new[] { '\n' }, StringSplitOptions.None)
+                .Select(x => new TextBlockSpan
+                {
+                    Text = x,
+                    Style = style
+                })
+                .ToList();
+
+            AddItemToLastTextBlock(items.First());
+
+            items
+                .Skip(1)
+                .Select(x => new TextBlock
+                {   
+                    Children = new List<ITextBlockItem> { x }
+                })
+                .ToList()
+                .ForEach(TextBlocks.Add);
+        }
+
+        public void Line(string text, TextStyle? style = null)
+        {
+            Span(text + Environment.NewLine, style);
+        }
+        
+        public void Line(string text)
+        {
+            Span(text + Environment.NewLine);
+        }
+        
+        public void EmptyLine()
+        {
+            Span(Environment.NewLine);
+        }
+
+        private void PageNumber(string slotName, TextStyle? style = null)
+        {
+            style ??= DefaultStyle;
+            
+            AddItemToLastTextBlock(new TextBlockPageNumber()
+            {
+                Style = style,
+                SlotName = slotName
+            });
+        }
+        
+        public void CurrentPageNumber(TextStyle? style = null)
+        {
+            PageNumber(PageContext.CurrentPageSlot, style);
+        }
+        
+        public void TotalPages(TextStyle? style = null)
+        {
+            PageNumber(PageContext.TotalPagesSlot, style);
+        }
+        
+        public void PageNumberOfLocation(string locationName, TextStyle? style = null)
+        {
+            PageNumber(locationName, style);
+        }
+        
+        public void InternalLocation(string text, string locationName, TextStyle? style = null)
+        {
+            style ??= DefaultStyle;
+            
+            AddItemToLastTextBlock(new TextBlockInternalLink
+            {
+                Style = style,
+                Text = text,
+                LocationName = locationName
+            });
+        }
+        
+        public void ExternalLocation(string text, string url, TextStyle? style = null)
+        {
+            style ??= DefaultStyle;
+            
+            AddItemToLastTextBlock(new TextBlockExternalLink
+            {
+                Style = style,
+                Text = text,
+                Url = url
+            });
+        }
+        
+        public IContainer Element()
+        {
+            var container = new Container();
+                
+            AddItemToLastTextBlock(new TextBlockElement
+            {
+                Element = container
+            });
+            
+            return container.AlignBottom().Box();
+        }
+        
+        internal void Compose(IContainer container)
+        {
+            TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
+            
+            container.Stack(stack =>
+            {
+                stack.Spacing(Spacing);
+
+                foreach (var textBlock in TextBlocks)
+                    stack.Item().Element(textBlock);
+            });
+        }
+    }
+    
+    public static class TextExtensions
+    {
+        public static void Text(this IContainer element, Action<TextDescriptor> content)
+        {
+            var textBlock = new TextBlock();
+
+            if (element is Alignment alignment)
+                textBlock.Alignment = alignment.Horizontal;
+            
+            var descriptor = new TextDescriptor();
+            content?.Invoke(descriptor);
+            descriptor.Compose(element);
+        }
+        
+        public static void Text(this IContainer element, object text, TextStyle? style = null)
+        {
+            element.Text(x => x.Span(text.ToString(), style));
+        }
+    }
+}

+ 10 - 19
QuestPDF/Fluent/TextStyleExtensions.cs

@@ -18,6 +18,11 @@ namespace QuestPDF.Fluent
             return style.Mutate(x => x.Color = value);
         }
         
+        public static TextStyle BackgroundColor(this TextStyle style, string value)
+        {
+            return style.Mutate(x => x.BackgroundColor = value);
+        }
+        
         public static TextStyle FontType(this TextStyle style, string value)
         {
             return style.Mutate(x => x.FontType = value);
@@ -37,31 +42,17 @@ namespace QuestPDF.Fluent
         {
             return style.Mutate(x => x.IsItalic = value);
         }
-
-        #region Alignmnet
-        
-        public static TextStyle Alignment(this TextStyle style, HorizontalAlignment value)
-        {
-            return style.Mutate(x => x.Alignment = value);
-        }
         
-        public static TextStyle AlignLeft(this TextStyle style)
+        public static TextStyle Strikethrough(this TextStyle style, bool value = true)
         {
-            return style.Alignment(HorizontalAlignment.Left);
+            return style.Mutate(x => x.HasStrikethrough = value);
         }
         
-        public static TextStyle AlignCenter(this TextStyle style)
+        public static TextStyle Underline(this TextStyle style, bool value = true)
         {
-            return style.Alignment(HorizontalAlignment.Center);
+            return style.Mutate(x => x.HasUnderline = value);
         }
-        
-        public static TextStyle AlignRight(this TextStyle style)
-        {
-            return style.Alignment(HorizontalAlignment.Right);
-        }
-        
-        #endregion
-        
+
         #region Weight
         
         public static TextStyle Weight(this TextStyle style, FontWeight weight)

+ 19 - 19
QuestPDF/Helpers/Placeholders.cs

@@ -155,25 +155,25 @@ namespace QuestPDF.Helpers
 
         private static readonly string[] BackgroundColors =
         {
-            Colors.Red.Lighten2,
-            Colors.Pink.Lighten2,
-            Colors.Purple.Lighten2,
-            Colors.DeepPurple.Lighten2,
-            Colors.Indigo.Lighten2,
-            Colors.Blue.Lighten2,
-            Colors.LightBlue.Lighten2,
-            Colors.Cyan.Lighten2,
-            Colors.Teal.Lighten2,
-            Colors.Green.Lighten2,
-            Colors.LightGreen.Lighten2,
-            Colors.Lime.Lighten2,
-            Colors.Yellow.Lighten2,
-            Colors.Amber.Lighten2,
-            Colors.Orange.Lighten2,
-            Colors.DeepOrange.Lighten2,
-            Colors.Brown.Lighten2,
-            Colors.Grey.Lighten2,
-            Colors.BlueGrey.Lighten2
+            Colors.Red.Lighten3,
+            Colors.Pink.Lighten3,
+            Colors.Purple.Lighten3,
+            Colors.DeepPurple.Lighten3,
+            Colors.Indigo.Lighten3,
+            Colors.Blue.Lighten3,
+            Colors.LightBlue.Lighten3,
+            Colors.Cyan.Lighten3,
+            Colors.Teal.Lighten3,
+            Colors.Green.Lighten3,
+            Colors.LightGreen.Lighten3,
+            Colors.Lime.Lighten3,
+            Colors.Yellow.Lighten3,
+            Colors.Amber.Lighten3,
+            Colors.Orange.Lighten3,
+            Colors.DeepOrange.Lighten3,
+            Colors.Brown.Lighten3,
+            Colors.Grey.Lighten3,
+            Colors.BlueGrey.Lighten3
         };
         
         public static string BackgroundColor()

+ 3 - 0
QuestPDF/Infrastructure/ICanvas.cs

@@ -13,5 +13,8 @@ namespace QuestPDF.Infrastructure
         void DrawExternalLink(string url, Size size);
         void DrawLocationLink(string locationName, Size size);
         void DrawLocation(string locationName);
+        
+        void Rotate(float angle);
+        void Scale(float scaleX, float scaleY);
     }
 }

+ 7 - 2
QuestPDF/Infrastructure/TextStyle.cs

@@ -5,18 +5,23 @@ namespace QuestPDF.Infrastructure
     public class TextStyle
     {
         internal string Color { get; set; } = Colors.Black;
+        internal string BackgroundColor { get; set; } = Colors.Transparent;
         internal string FontType { get; set; } = "Calibri";
         internal float Size { get; set; } = 12;
         internal float LineHeight { get; set; } = 1.2f;
-        internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
         internal FontWeight FontWeight { get; set; } = FontWeight.Normal;
         internal bool IsItalic { get; set; } = false;
+        internal bool HasStrikethrough { get; set; } = false;
+        internal bool HasUnderline { get; set; } = false;
 
         public static TextStyle Default => new TextStyle();
+
+        private string? KeyCache { get; set; }
         
         public override string ToString()
         {
-            return $"{Color}|{FontType}|{Size}|{LineHeight}|{Alignment}|{FontWeight}|{IsItalic}";
+            KeyCache ??= $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{FontWeight}|{IsItalic}|{HasStrikethrough}|{HasUnderline}";
+            return KeyCache;
         }
 
         internal TextStyle Clone() => (TextStyle)MemberwiseClone();

+ 4 - 4
QuestPDF/QuestPDF.csproj

@@ -4,9 +4,9 @@
         <Authors>MarcinZiabek</Authors>
         <Company>CodeFlint</Company>
         <PackageId>QuestPDF</PackageId>
-        <Version>2021.9.2</Version>
+        <Version>2021.10.1</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>
-        <PackageReleaseNotes>Added support for registering custom fonts from a stream. Fixed continuous page setting. Improved exception messages.</PackageReleaseNotes>
+        <PackageReleaseNotes>Enhanced text rendering capabilities. Improved rendering performance.</PackageReleaseNotes>
         <LangVersion>8</LangVersion>
         <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
         <PackageIcon>Logo.png</PackageIcon>
@@ -15,14 +15,14 @@
         <RepositoryUrl>https://github.com/QuestPDF/library.git</RepositoryUrl>
         <RepositoryType>git</RepositoryType>
         <Copyright>Marcin Ziąbek, QuestPDF contributors</Copyright>
-        <PackageTags>pdf file export generate generation tool create creation render portable document format quest html library converter free</PackageTags>
+        <PackageTags>pdf file export generate generation tool create creation render portable document format quest html library converter open source free standard core</PackageTags>
         <PackageLicenseExpression>MIT</PackageLicenseExpression>
         <Nullable>enable</Nullable>
         <TargetFrameworks>net462;netstandard2.0;netcoreapp2.0;netcoreapp3.0</TargetFrameworks>
     </PropertyGroup>
 
     <ItemGroup>
-      <PackageReference Include="SkiaSharp" Version="2.80.2" />
+      <PackageReference Include="SkiaSharp" Version="2.80.3" />
     </ItemGroup>
 
     <ItemGroup>

+ 166 - 21
readme.md

@@ -4,17 +4,17 @@
 
 ## Overview
 
-1) **Rely on solid fundamentals** - This library is created specifically for designing and arranging document layouts, with full paging support.  Alternative solutions, such as HTML-based converters, are not designed for this purpose and therefore are often unpredictable and do not produce desired results.
+**Rely on solid fundamentals** - This library is created specifically for designing and arranging document layouts, with full paging support.  Alternative solutions, such as HTML-based converters, are not designed for this purpose and therefore are often unpredictable and do not produce desired results.
 
-2) **Work with organized self-explanatory code** - The entire process of implementing PDF document, takes place in your code. Free yourself from slow visual designers and strange technological limitations. Follow simple yet highly effective approaches to create maintainable, high-quality code.
+**Work with organized self-explanatory code** - The entire process of implementing PDF document, takes place in your code. Free yourself from slow visual designers and strange technological limitations. Follow simple yet highly effective approaches to create maintainable, high-quality code.
 
-3) **Compose simple components into complex documents** - Do you remember the feeling when your code just works? When your ideas are becoming real without any effort? Working with simple, easy to understand, self-explanatory and highly composable layout elements is the key here!
+**Compose simple components into complex documents** - Do you remember the feeling when your code just works? When your ideas are becoming real without any effort? Working with simple, easy to understand, self-explanatory and highly composable layout elements is the key here!
 
-4) **Create and reuse components** - Feel no fear of complex documents! Create custom, reusable components and divide the document's layout into easy to maintain pieces. Inject data to customize content and use slots to enhance composability. Decide how complex approaches your solution needs and follow the best path.
+**Create and reuse components** - Feel no fear of complex documents! Create custom, reusable components and divide the document's layout into easy to maintain pieces. Inject data to customize content and use slots to enhance composability. Decide how complex approaches your solution needs and follow the best path.
 
-5) **Prototype with ease** - We understand that document generation is often tricky and require multiple iterations. The library offers additional prototyping tools such as random text generator or image placeholder element. By following best practices, you can develop a document without having data.
+**Prototype with ease** - We understand that document generation is often tricky and require multiple iterations. The library offers additional prototyping tools such as random text generator or image placeholder element. By following best practices, you can develop a document without having data.
 
-6) **Enjoy fast PDF generation** - QuestPDF is created upon SkiaSharp, a well-known graphical library, and converts your data into PDF documents. It offers a highly optimized layouting engine capable of generating over 1000 PDF files per minute per core. The entire process is thread-safe.
+**Enjoy fast PDF generation** - QuestPDF is created upon SkiaSharp, a well-known graphical library, and converts your data into PDF documents. It offers a highly optimized layouting engine capable of generating over 1000 PDF files per minute per core. The entire process is thread-safe.
 
 ## Support QuestPDF
 
@@ -36,10 +36,10 @@ The library is available as a nuget package. You can install it as any other nug
 
 ## Documentation
 
-1. [Release notes and roadmap](https://www.questpdf.com/documentation/releases.html) - everything that is planned for future library iterations, description of new features and information about potential breaking changes.
-2. [Getting started tutorial](https://www.questpdf.com/documentation/getting-started.html) - a short and easy to follow tutorial showing how to design an invoice document under 200 lines of code.
-3. [API Reference](https://www.questpdf.com/documentation/api-reference.html) - a detailed description of behavior of all available components and how to use them with C# Fluent API.
-4. [Patterns and practices](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata) - everything that may help you designing great reports and reusable code that is easy to maintain.
+**[Release notes and roadmap](https://www.questpdf.com/documentation/releases.html)** - everything that is planned for future library iterations, description of new features and information about potential breaking changes.
+**[Getting started tutorial](https://www.questpdf.com/documentation/getting-started.html)** - a short and easy to follow tutorial showing how to design an invoice document under 200 lines of code.
+**[API Reference](https://www.questpdf.com/documentation/api-reference.html)** - a detailed description of behavior of all available components and how to use them with C# Fluent API.
+**[Patterns and practices](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)** - everything that may help you designing great reports and reusable code that is easy to maintain.
 
 ## Example invoice
 
@@ -51,39 +51,184 @@ For tutorial, documentation and API reference, please visit [the QuestPDF docume
   <img src="https://github.com/QuestPDF/example-invoice/raw/main/images/invoice.png" width="595px">
 </a>
 
-Here you can find an example code showing how easy is to write and understand the fluent API:
+Here you can find an example code showing how easy is to write and understand the fluent API.
+
+**General document structure** with header, content and footer:
 
 ```csharp
 public void Compose(IDocumentContainer container)
-{               
+{
     container
         .Page(page =>
         {
-            page.MarginVertical(60);
-            page.MarginHorizontal(40);
+            page.Margin(50);
             
-            page.Size(PageSizes.A4);
-                
             page.Header().Element(ComposeHeader);
             page.Content().Element(ComposeContent);
-            page.Footer().AlignCenter().PageNumber();
+            
+            page.Footer().AlignCenter().Text(x =>
+            {
+                x.CurrentPageNumber();
+                x.Span(" / ");
+                x.TotalPages();
+            });
         });
 }
+```
 
+**The header area** consists of basic invoice information along with a logo placeholder.
+
+```csharp
 void ComposeHeader(IContainer container)
 {
+    var titleTextStyle = TextStyle.Default.Size(20).SemiBold().Color(Colors.Blue.Medium);
+    
     container.Row(row =>
     {
         row.RelativeColumn().Stack(stack =>
         {
-            stack.Item().Text($"Invoice #{Model.InvoiceNumber}", TextStyle.Default.Size(20).Bold());
-            stack.Item().Text($"Issue date: {Model.IssueDate:d}");
-            stack.Item().Text($"Due date: {Model.DueDate:d}");
+            stack.Item().Text($"Invoice #{Model.InvoiceNumber}", titleStyle);
+
+            stack.Item().Text(text =>
+            {
+                text.Span("Issue date: ", TextStyle.Default.SemiBold());
+                text.Span($"{Model.IssueDate:d}");
+            });
+
+            stack.Item().Text(text =>
+            {
+                text.Span("Due date: ", TextStyle.Default.SemiBold());
+                text.Span($"{Model.DueDate:d}");
+            });
         });
         
         row.ConstantColumn(100).Height(50).Placeholder();
     });
 }
+```
+
+Implementation of **the content area** that contains seller and customer details, then listing of all bought products, then a comments section.
+
+```csharp
+void ComposeContent(IContainer container)
+{
+    container.PaddingVertical(40).Stack(column => 
+    {
+        column.Spacing(20);
+        
+        column.Item().Row(row =>
+        {
+            row.RelativeColumn().Component(new AddressComponent("From", Model.SellerAddress));
+            row.ConstantColumn(50);
+            row.RelativeColumn().Component(new AddressComponent("For", Model.CustomerAddress));
+        });
+
+        column.Item().Element(ComposeTable);
+
+        var totalPrice = Model.Items.Sum(x => x.Price * x.Quantity);
+        
+        column
+            .Item()
+            .PaddingRight(5)
+            .AlignRight()
+            .Text($"Grand total: {totalPrice}$", TextStyle.Default.SemiBold());
+
+        if (!string.IsNullOrWhiteSpace(Model.Comments))
+            column.Item().PaddingTop(25).Element(ComposeComments);
+    });
+}
+```
+
+**The table and comments** codes are extracted into separate methods to increase clarity:
+
+```csharp
+void ComposeTable(IContainer container)
+{
+    var headerStyle = TextStyle.Default.SemiBold();
+    
+    container.Decoration(decoration =>
+    {
+        // header
+        decoration.Header().BorderBottom(1).Padding(5).Row(row => 
+        {
+            row.ConstantColumn(25).Text("#", headerStyle);
+            row.RelativeColumn(3).Text("Product", headerStyle);
+            row.RelativeColumn().AlignRight().Text("Unit price", headerStyle);
+            row.RelativeColumn().AlignRight().Text("Quantity", headerStyle);
+            row.RelativeColumn().AlignRight().Text("Total", headerStyle);
+        });
+
+        // content
+        decoration
+            .Content()
+            .Stack(column =>
+            {
+                foreach (var item in Model.Items)
+                {
+                    column
+                    .Item()
+                    .ShowEntire()
+                    .BorderBottom(1)
+                    .BorderColor(Colors.Grey.Lighten2)
+                    .Padding(5)
+                    .Row(row => 
+                    {
+                        row.ConstantColumn(25).Text(Model.Items.IndexOf(item) + 1);
+                        row.RelativeColumn(3).Text(item.Name);
+                        row.RelativeColumn().AlignRight().Text($"{item.Price}$");
+                        row.RelativeColumn().AlignRight().Text(item.Quantity);
+                        row.RelativeColumn().AlignRight().Text($"{item.Price * item.Quantity}$");
+                    });
+                }
+            });
+    });
+}
+```
 
-// code describing content
+```csharp
+void ComposeComments(IContainer container)
+{
+    container.ShowEntire().Background(Colors.Grey.Lighten3).Padding(10).Stack(message => 
+    {
+        message.Spacing(5);
+        message.Item().Text("Comments", TextStyle.Default.Size(14).SemiBold());
+        message.Item().Text(Model.Comments);
+    });
+}
+```
+
+**The address details section** is implemented using components. This way the code can be easily reused for both seller and customer:
+
+```csharp
+public class AddressComponent : IComponent
+{
+    private string Title { get; }
+    private Address Address { get; }
+
+    public AddressComponent(string title, Address address)
+    {
+        Title = title;
+        Address = address;
+    }
+    
+    public void Compose(IContainer container)
+    {
+        container.ShowEntire().Stack(column =>
+        {
+            column.Spacing(5);
+
+            column
+                .Item()
+                .BorderBottom(1)
+                .PaddingBottom(5)
+                .Text(Title, TextStyle.Default.SemiBold());
+            
+            column.Item().Text(Address.CompanyName);
+            column.Item().Text(Address.Street);
+            column.Item().Text($"{Address.City}, {Address.State}");
+            column.Item().Text(Address.Email);
+            column.Item().Text(Address.Phone);
+        });
+    }
+}
 ```

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.