Browse Source

Implemented text rendering benchmark

Marcin Ziąbek 4 years ago
parent
commit
f17860f519

+ 35 - 0
QuestPDF.Examples/BarcodeExamples.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Linq;
+using NUnit.Framework;
+using QuestPDF.Examples.Engine;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+using SkiaSharp;
+
+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));
+                        });
+                });
+        }
+    }
+}

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

@@ -19,6 +19,7 @@ namespace QuestPDF.Examples.Engine
     {
         private string FileNamePrefix = "test";
         private Size Size { get; set; }
+        private bool ShowResult { get; set; }
         private RenderingTestResult ResultType { get; set; } = RenderingTestResult.Images;
         
         private RenderingTest()
@@ -59,26 +60,37 @@ namespace QuestPDF.Examples.Engine
             ResultType = RenderingTestResult.Images;
             return this;
         }
+
+        public RenderingTest ShowResults()
+        {
+            ShowResult = true;
+            return this;
+        }
         
         public void Render(Action<IContainer> content)
         {
             var container = new Container();
             content(container);
-            
-            var document = new SimpleDocument(container, Size);
+
+            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);
-                Process.Start("explorer", fileNameSchema(0));
+                
+                if (ShowResult)
+                    Process.Start("explorer", fileNameSchema(0));
             }
 
             if (ResultType == RenderingTestResult.Pdf)
             {
                 var fileName = $"{FileNamePrefix}.pdf";
                 document.GeneratePdf(fileName);
-                Process.Start("explorer", 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
             };
         }
         

+ 1 - 1
QuestPDF.Examples/QuestPDF.Examples.csproj

@@ -17,7 +17,7 @@
     </ItemGroup>
 
     <ItemGroup>
-      <None Update="LibreBarcode39-Regular.ttf">
+      <None Update="quo-vadis.txt">
         <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       </None>
     </ItemGroup>

+ 177 - 0
QuestPDF.Examples/TextBenchmark.cs

@@ -0,0 +1,177 @@
+using System;
+using System.Collections;
+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 Benchmark()
+        {
+            var subtitleStyle = TextStyle.Default.Size(24).SemiBold().Color(Colors.Blue.Medium);
+            var normalStyle = TextStyle.Default.Size(14);
+            
+            var chapters = GetChapters().ToList();
+
+            var results = PerformTest(16).ToList();
+            
+            Console.WriteLine($"Min: {results.Min():F}");
+            Console.WriteLine($"Max: {results.Max():F}");
+            Console.WriteLine($"Avg: {results.Average():F}");
+
+            IEnumerable<(string title, string content)> GetChapters()
+            {
+                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);
+                    var content = string.Join(Environment.NewLine, lines);
+
+                    yield return (title, content);
+                }
+            }
+            
+            void GenerateDocument()
+            {
+                RenderingTest
+                    .Create()
+                    .PageSize(PageSizes.A4)
+                    .FileName()
+                    .ProducePdf()
+                    .Render(ComposePage);
+            }
+
+            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;
+                }
+            }
+
+            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);
+                            });
+
+                        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 =>
+                {
+                    stack.Item().Text("Table of contents", subtitleStyle);
+                    stack.Item().PaddingTop(10).PaddingBottom(50).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).ExtendHorizontal();
+                    
+                    foreach (var chapter in chapters)
+                    {
+                        stack.Item().InternalLink(chapter.title).Row(row =>
+                        {
+                            row.RelativeColumn().Text(chapter.title);
+                            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 =>
+                {
+                    stack.Item().Location(title).Text(title, subtitleStyle);
+                    stack.Item().PaddingTop(10).PaddingBottom(50).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).ExtendHorizontal();
+  
+                    stack.Item().Text(text =>
+                    {
+                        text.ParagraphSpacing(10);
+                        text.Span(content, normalStyle);
+                    });
+                    
+                    stack.Item().PageBreak();
+                });
+            }
+
+            void Footer(IContainer container)
+            {
+                container
+                    .AlignCenter()
+                    .Text(text =>
+                    {
+                        text.CurrentPageNumber();
+                        text.Span(" / ");
+                        text.TotalPages();
+                    });
+            }
+        }
+    }
+}

+ 5 - 1
QuestPDF.Examples/TextExamples.cs

@@ -1,4 +1,6 @@
-using System.Linq;
+using System;
+using System.IO;
+using System.Linq;
 using NUnit.Framework;
 using QuestPDF.Examples.Engine;
 using QuestPDF.Fluent;
@@ -30,9 +32,11 @@ namespace QuestPDF.Examples
                             text.Span("Then something bigger. ", TextStyle.Default.Size(28).Color(Colors.DeepOrange.Darken2).BackgroundColor(Colors.Yellow.Lighten3).Underlined());
                             text.Span("And tiny teeny-tiny. ", TextStyle.Default.Size(6));
                             text.Span("Stroked text also works fine. ", TextStyle.Default.Size(14).Stroked().BackgroundColor(Colors.Grey.Lighten4));
+                            //text.NewLine();
                             text.Span("Is it time for lorem  ipsum? ", TextStyle.Default.Size(12).Underlined().BackgroundColor(Colors.Grey.Lighten3));
                             text.Span(Placeholders.LoremIpsum(), TextStyle.Default.Size(12));
                             
+                            //text.NewLine();
                             text.Span("And now some colors: ", TextStyle.Default.Size(16).Color(Colors.Green.Medium));
                             
                             foreach (var i in Enumerable.Range(1, 100))

+ 34 - 0
QuestPDF.Examples/optimization.md

@@ -0,0 +1,34 @@
+# Text optimization
+
+## Initial state
+
+Attempts:
+
+```angular2html
+Attempt 0: 18389,00
+Attempt 1: 18627,00
+Attempt 2: 19745,00
+Attempt 3: 19690,00
+Attempt 4: 19032,00
+Attempt 5: 17773,00
+Attempt 6: 17570,00
+Attempt 7: 17691,00
+Attempt 8: 17642,00
+Attempt 9: 17945,00
+Attempt 10: 19876,00
+Attempt 11: 19731,00
+Attempt 12: 19158,00
+Attempt 13: 18004,00
+Attempt 14: 17734,00
+Attempt 15: 19352,00
+```
+
+Results:
+
+```
+Min: 17570,00
+Max: 19876,00
+Avg: 18622,44
+```
+
+

File diff suppressed because it is too large
+ 285 - 0
QuestPDF.Examples/quo-vadis.txt


+ 16 - 19
QuestPDF/Elements/TextBlock.cs → QuestPDF/Elements/Text/TextBlock.cs

@@ -1,17 +1,14 @@
 using System;
-using System.Collections;
 using System.Collections.Generic;
-using System.Collections.Specialized;
 using System.Linq;
 using QuestPDF.Drawing.SpacePlan;
-using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 
-namespace QuestPDF.Elements
+namespace QuestPDF.Elements.Text
 {
     internal class TextLineElement
     {
-        public TextItem Element { get; set; }
+        public ITextElement Element { get; set; }
         public TextMeasurementResult Measurement { get; set; }
     }
 
@@ -20,7 +17,7 @@ namespace QuestPDF.Elements
         public ICollection<TextLineElement> Elements { get; set; }
 
         public float TextHeight => Elements.Max(x => x.Measurement.Height);
-        public float LineHeight => Elements.Max(x => x.Element.Style.LineHeight * x.Measurement.Height);
+        public float LineHeight => Elements.Max(x => x.Measurement.LineHeight * x.Measurement.Height);
         
         public float Ascent => Elements.Min(x => x.Measurement.Ascent) - (LineHeight - TextHeight) / 2;
         public float Descent => Elements.Max(x => x.Measurement.Descent) + (LineHeight - TextHeight) / 2;
@@ -31,23 +28,17 @@ namespace QuestPDF.Elements
     internal class TextBlock : Element, IStateResettable
     {
         public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
-        public List<TextItem> Children { get; set; } = new List<TextItem>();
+        public List<ITextElement> Children { get; set; } = new List<ITextElement>();
 
-        public Queue<TextItem> RenderingQueue { get; set; }
+        public Queue<ITextElement> RenderingQueue { get; set; }
         public int CurrentElementIndex { get; set; }
 
         public void ResetState()
         {
-            RenderingQueue = new Queue<TextItem>(Children);
+            RenderingQueue = new Queue<ITextElement>(Children);
             CurrentElementIndex = 0;
         }
-        
-        internal override void HandleVisitor(Action<Element?> visit)
-        {
-            Children.ForEach(x => x?.HandleVisitor(visit));
-            base.HandleVisitor(visit);
-        }
-        
+
         internal override ISpacePlan Measure(Size availableSpace)
         {
             if (!RenderingQueue.Any())
@@ -103,6 +94,9 @@ namespace QuestPDF.Elements
                 {
                     var textDrawingRequest = new TextDrawingRequest
                     {
+                        Canvas = Canvas,
+                        PageContext = PageContext,
+                        
                         StartIndex = item.Measurement.StartIndex,
                         EndIndex = item.Measurement.EndIndex,
                         
@@ -144,7 +138,7 @@ namespace QuestPDF.Elements
 
         public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
         {
-            var queue = new Queue<TextItem>(RenderingQueue);
+            var queue = new Queue<ITextElement>(RenderingQueue);
             var currentItemIndex = CurrentElementIndex;
             var currentHeight = 0f;
 
@@ -177,11 +171,14 @@ namespace QuestPDF.Elements
                     
                     var measurementRequest = new TextMeasurementRequest
                     {
+                        Canvas = Canvas,
+                        PageContext = PageContext,
+                        
                         StartIndex = currentItemIndex,
                         AvailableWidth = availableWidth - currentWidth
                     };
                 
-                    var measurementResponse = currentElement.MeasureText(measurementRequest);
+                    var measurementResponse = currentElement.Measure(measurementRequest);
                 
                     if (measurementResponse == null)
                         break;
@@ -193,7 +190,7 @@ namespace QuestPDF.Elements
                     });
 
                     currentWidth += measurementResponse.Width;
-                    currentItemIndex = measurementResponse.EndIndex + 1;
+                    currentItemIndex = measurementResponse.EndIndex;
                     
                     if (!measurementResponse.IsLast)
                         break;

+ 77 - 73
QuestPDF/Elements/TextItem.cs → QuestPDF/Elements/Text/TextItem.cs

@@ -1,16 +1,16 @@
 using System;
-using System.Drawing;
-using System.Runtime.InteropServices;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing.SpacePlan;
 using QuestPDF.Infrastructure;
-using SkiaSharp;
 using Size = QuestPDF.Infrastructure.Size;
 
-namespace QuestPDF.Elements
+namespace QuestPDF.Elements.Text
 {
     internal class TextMeasurementRequest
     {
+        public ICanvas Canvas { get; set; }
+        public IPageContext PageContext { get; set; }
+        
         public int StartIndex { get; set; }
         public float AvailableWidth { get; set; }
     }
@@ -19,10 +19,12 @@ namespace QuestPDF.Elements
     {
         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; }
         
@@ -32,86 +34,30 @@ namespace QuestPDF.Elements
         public bool IsLast => EndIndex == TotalIndex;
     }
 
-    public class TextDrawingRequest
+    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; }
     }
+
+    internal interface ITextElement
+    {
+        TextMeasurementResult? Measure(TextMeasurementRequest request);
+        void Draw(TextDrawingRequest request);
+    }
     
-    internal class TextItem : Element, IStateResettable
+    internal class TextItem : ITextElement
     {
         public string Text { get; set; }
-
         public TextStyle Style { get; set; } = new TextStyle();
-        internal int PointerIndex { get; set; }
-        
-        public void ResetState()
-        {
-            PointerIndex = 0;
-        }
-        
-        internal override ISpacePlan Measure(Size availableSpace)
-        {
-            return new FullRender(Size.Zero);
-
-            // if (VirtualPointer >= Text.Length)
-            //     return new FullRender(Size.Zero);
-            //
-            // var paint = Style.ToPaint();
-            // var metrics = paint.FontMetrics;
-            //
-            // var length = (int)paint.BreakText(Text, availableSpace.Width);
-            // length = VirtualPointer + Text.Substring(VirtualPointer, length).LastIndexOf(" ");
-            //
-            // var textFragment = Text.Substring(VirtualPointer, length);
-            //
-            // var width = paint.MeasureText(textFragment);
-            // var height = Math.Abs(metrics.Descent) + Math.Abs(metrics.Ascent);
-            //
-            // if (availableSpace.Width < width || availableSpace.Height < height)
-            //     return new Wrap();
-            //
-            // VirtualPointer += length;
-            //
-            // return new TextRender(width, height)
-            // {
-            //     Descent = metrics.Descent,
-            //     Ascent = metrics.Ascent
-            // };
-        }
-
-        internal override void Draw(Size availableSpace)
-        {
-            
-        }
-        
-        internal void Draw(TextDrawingRequest request)
-        {
-            var fontMetrics = Style.ToPaint().FontMetrics;
-
-            var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
-            
-            Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
-            Canvas.DrawText(text, Position.Zero, Style);
-
-            // draw underline
-            if (Style.IsUnderlined && fontMetrics.UnderlinePosition.HasValue)
-                DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness.Value);
-            
-            // draw stroke
-            if (Style.IsStroked && fontMetrics.StrikeoutPosition.HasValue)
-                DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness.Value);
-
-            void DrawLine(float offset, float thickness)
-            {
-                Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
-            }
-        }
 
-        internal TextMeasurementResult? MeasureText(TextMeasurementRequest request)
+        public TextMeasurementResult? Measure(TextMeasurementRequest request)
         {
             var paint = Style.ToPaint();
             
@@ -129,6 +75,8 @@ namespace QuestPDF.Elements
 
                 if (breakingIndex <= 0)
                     return null;
+
+                breakingIndex += 1;
             }
 
             text = text.Substring(0, breakingIndex);
@@ -143,10 +91,66 @@ namespace QuestPDF.Elements
                 Ascent = paint.FontMetrics.Ascent,
                 Descent = paint.FontMetrics.Descent,
      
+                LineHeight = Style.LineHeight,
+                
                 StartIndex = request.StartIndex,
                 EndIndex = request.StartIndex + breakingIndex,
                 TotalIndex = Text.Length
             };
         }
+        
+        public void Draw(TextDrawingRequest request)
+        {
+            var fontMetrics = Style.ToPaint().FontMetrics;
+
+            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.IsUnderlined && fontMetrics.UnderlinePosition.HasValue)
+                DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness.Value);
+            
+            // draw stroke
+            if (Style.IsStroked && 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);
+            }
+        }
+    }
+
+    internal class PageNumberTextItem : ITextElement
+    {
+        public TextStyle Style { get; set; } = new TextStyle();
+        public string SlotName { get; set; }
+        
+        public TextMeasurementResult? Measure(TextMeasurementRequest request)
+        {
+            return GetItem(request.PageContext).Measure(request);
+        }
+
+        public void Draw(TextDrawingRequest request)
+        {
+            GetItem(request.PageContext).Draw(request);
+        }
+
+        private TextItem GetItem(IPageContext context)
+        {
+            var pageNumberPlaceholder = 123;
+            
+            var pageNumber = context.GetRegisteredLocations().Contains(SlotName)
+                ? context.GetLocationPage(SlotName)
+                : pageNumberPlaceholder;
+            
+            return new TextItem
+            {
+                Style = Style,
+                Text = pageNumber.ToString()
+            };
+        }
     }
 }

+ 85 - 16
QuestPDF/Fluent/TextExtensions.cs

@@ -2,40 +2,110 @@
 using System.Collections.Generic;
 using System.Linq;
 using QuestPDF.Elements;
+using QuestPDF.Elements.Text;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Fluent
 {
     public class TextDescriptor
     {
-        private TextBlock TextBlock { get; }
-        
-        internal TextDescriptor(TextBlock textBlock)
-        {
-            TextBlock = textBlock;
-        }
-        
+        private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
+        private HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
+        private float Spacing { get; set; } = 0f;
+
         public void AlignLeft()
         {
-            TextBlock.Alignment = HorizontalAlignment.Left;
+            Alignment = HorizontalAlignment.Left;
         }
         
         public void AlignCenter()
         {
-            TextBlock.Alignment = HorizontalAlignment.Center;
+            Alignment = HorizontalAlignment.Center;
         }
         
         public void AlignRight()
         {
-            TextBlock.Alignment = HorizontalAlignment.Right;
+            Alignment = HorizontalAlignment.Right;
+        }
+
+        public void ParagraphSpacing(float value)
+        {
+            Spacing = value;
         }
         
         public void Span(string text, TextStyle? style = null)
         {
-            TextBlock.Children.Add(new TextItem
+            style ??= TextStyle.Default;
+            
+            if (string.IsNullOrWhiteSpace(text))
+                return;
+            
+            var items = text
+                .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
+                .Select(x => new TextItem
+                {
+                    Text = x,
+                    Style = style
+                })
+                .ToList();
+
+            if (!TextBlocks.Any())
+                TextBlocks.Add(new TextBlock());
+            
+            TextBlocks.First().Children.Add(items.First());
+
+            items
+                .Skip(1)
+                .Select(x => new TextBlock
+                {   
+                    Children = new List<ITextElement> { x }
+                })
+                .ToList()
+                .ForEach(TextBlocks.Add);
+        }
+
+        public void NewLine()
+        {
+            Span(Environment.NewLine);
+        }
+
+        private void PageNumber(string slotName, TextStyle? style = null)
+        {
+            style ??= TextStyle.Default;
+            
+            if (!TextBlocks.Any())
+                TextBlocks.Add(new TextBlock());
+            
+            TextBlocks.First().Children.Add(new PageNumberTextItem()
+            {
+                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);
+        }
+        
+        internal void Compose(IContainer container)
+        {
+            container.Stack(stack =>
             {
-                Text = text,
-                Style = style ?? TextStyle.Default
+                stack.Spacing(Spacing);
+
+                foreach (var textBlock in TextBlocks)
+                    stack.Item().Element(textBlock);
             });
         }
     }
@@ -49,10 +119,9 @@ namespace QuestPDF.Fluent
             if (element is Alignment alignment)
                 textBlock.Alignment = alignment.Horizontal;
             
-            var descriptor = new TextDescriptor(textBlock);
+            var descriptor = new TextDescriptor();
             content?.Invoke(descriptor);
-            
-            element.Element(textBlock);
+            descriptor.Compose(element);
         }
         
         public static void Text(this IContainer element, object text, TextStyle? style = null)

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