Browse Source

Merge branch '2022.5'

MarcinZiabek 3 years ago
parent
commit
cd181b1e2e

+ 185 - 0
QuestPDF.Examples/DynamicExamples.cs

@@ -0,0 +1,185 @@
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using QuestPDF.Elements;
+using QuestPDF.Examples.Engine;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Examples
+{
+    public class OrderItem
+    {
+        public string ItemName { get; set; } = Placeholders.Label();
+        public int Price { get; set; } = Placeholders.Random.Next(1, 11) * 10;
+        public int Count { get; set; } = Placeholders.Random.Next(1, 11);
+    }
+
+    public struct OrdersTableState
+    {
+        public int ShownItemsCount { get; set; }
+    }
+    
+    public class OrdersTable : IDynamicComponent<OrdersTableState>
+    {
+        private ICollection<OrderItem> Items { get; }
+        public OrdersTableState State { get; set; }
+
+        public OrdersTable(ICollection<OrderItem> items)
+        {
+            Items = items;
+
+            State = new OrdersTableState
+            {
+                ShownItemsCount = 0
+            };
+        }
+        
+        public DynamicComponentComposeResult Compose(DynamicContext context)
+        {
+            var header = ComposeHeader(context);
+            var sampleFooter = ComposeFooter(context, Enumerable.Empty<OrderItem>());
+            var decorationHeight = header.Size.Height + sampleFooter.Size.Height;
+            
+            var rows = GetItemsForPage(context, decorationHeight).ToList();
+            var footer = ComposeFooter(context, rows.Select(x => x.Item));
+
+            var content = context.CreateElement(container =>
+            {
+                container.MinimalBox().Decoration(decoration =>
+                {
+                    decoration.Header().Element(header);
+
+                    decoration.Content().Box().Stack(stack =>
+                    {
+                        foreach (var row in rows)
+                            stack.Item().Element(row.Element);
+                    });
+
+                    decoration.Footer().Element(footer);
+                });
+            });
+
+            State = new OrdersTableState
+            {
+                ShownItemsCount = State.ShownItemsCount + rows.Count
+            };
+
+            return new DynamicComponentComposeResult
+            {
+                Content = content,
+                HasMoreContent = State.ShownItemsCount + rows.Count < Items.Count
+            };
+        }
+
+        private IDynamicElement ComposeHeader(DynamicContext context)
+        {
+            return context.CreateElement(element =>
+            {
+                element
+                    .Width(context.AvailableSize.Width)
+                    .BorderBottom(1)
+                    .BorderColor(Colors.Grey.Darken2)
+                    .Padding(5)
+                    .Row(row =>
+                    {
+                        var textStyle = TextStyle.Default.SemiBold();
+
+                        row.ConstantItem(30).Text("#", textStyle);
+                        row.RelativeItem().Text("Item name", textStyle);
+                        row.ConstantItem(50).AlignRight().Text("Count", textStyle);
+                        row.ConstantItem(50).AlignRight().Text("Price", textStyle);
+                        row.ConstantItem(50).AlignRight().Text("Total", textStyle);
+                    });
+            });
+        }
+        
+        private IDynamicElement ComposeFooter(DynamicContext context, IEnumerable<OrderItem> items)
+        {
+            var total = items.Sum(x => x.Count * x.Price);
+
+            return context.CreateElement(element =>
+            {
+                element
+                    .Width(context.AvailableSize.Width)
+                    .Padding(5)
+                    .AlignRight()
+                    .Text($"Subtotal: {total}$", TextStyle.Default.Size(14).SemiBold());
+            });
+        }
+        
+        private IEnumerable<(OrderItem Item, IDynamicElement Element)> GetItemsForPage(DynamicContext context, float decorationHeight)
+        {
+            var totalHeight = decorationHeight;
+
+            foreach (var index in Enumerable.Range(State.ShownItemsCount, Items.Count - State.ShownItemsCount))
+            {
+                var item = Items.ElementAt(index);
+                
+                var element = context.CreateElement(content =>
+                {
+                    content
+                        .Width(context.AvailableSize.Width)
+                        .BorderBottom(1)
+                        .BorderColor(Colors.Grey.Lighten2)
+                        .Padding(5)
+                        .Row(row =>
+                        {
+                            row.ConstantItem(30).Text(index + 1);
+                            row.RelativeItem().Text(item.ItemName);
+                            row.ConstantItem(50).AlignRight().Text(item.Count);
+                            row.ConstantItem(50).AlignRight().Text($"{item.Price}$");
+                            row.ConstantItem(50).AlignRight().Text($"{item.Count*item.Price}$");
+                        });
+                });
+
+                var elementHeight = element.Size.Height;
+                    
+                if (totalHeight + elementHeight > context.AvailableSize.Height)
+                    break;
+                    
+                totalHeight += elementHeight;
+                yield return (item, element);
+            }
+        }
+    }
+    
+    public static class DynamicExamples
+    {
+        [Test]
+        public static void Dynamic()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(PageSizes.A5)
+                .ShowResults()
+                .Render(container =>
+                {
+                    var items = Enumerable.Range(0, 25).Select(x => new OrderItem()).ToList();
+                    
+                    container
+                        .Background(Colors.White)
+                        .Padding(25)
+                        .Decoration(decoration =>
+                        {
+                            decoration
+                                .Header()
+                                .PaddingBottom(5)
+                                .Text(text =>
+                                {
+                                    text.DefaultTextStyle(TextStyle.Default.SemiBold().FontColor(Colors.Blue.Darken2).FontSize(16));
+                                    text.Span("Page ");
+                                    text.CurrentPageNumber();
+                                    text.Span(" of ");
+                                    text.TotalPages();
+                                });
+                            
+                            decoration
+                                .Content()
+                                .Dynamic(new OrdersTable(items));
+                        });
+                });
+        }
+    }
+}

+ 46 - 0
QuestPDF.Examples/TableBenchmark.cs

@@ -0,0 +1,46 @@
+using System.Linq;
+using NUnit.Framework;
+using QuestPDF.Examples.Engine;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+
+namespace QuestPDF.Examples
+{
+    public class TableBenchmark
+    {
+        [Test]
+        public void Benchmark()
+        {
+            RenderingTest
+                .Create()
+                .ProducePdf()
+                .PageSize(PageSizes.A4)
+                .ShowResults()
+                .MaxPages(10_000)
+                .EnableCaching(true)
+                .EnableDebugging(false)
+                .Render(container =>
+                {
+                    container
+                        .Padding(10)
+                        .MinimalBox()
+                        .Border(1)
+                        .Table(table =>
+                        {
+                            const int numberOfRows = 100_000;
+                            const int numberOfColumns = 10;
+                            
+                            table.ColumnsDefinition(columns =>
+                            {
+                                foreach (var _ in Enumerable.Range(0, numberOfColumns))
+                                    columns.RelativeColumn();
+                            });
+
+                            foreach (var row in Enumerable.Range(0, numberOfRows))
+                            foreach (var column in Enumerable.Range(0, numberOfColumns))
+                                table.Cell().Background(Placeholders.BackgroundColor()).Padding(5).Text($"{row}_{column}");
+                        });
+                });
+        }
+    }
+}

+ 93 - 1
QuestPDF.Examples/TextExamples.cs

@@ -36,7 +36,99 @@ namespace QuestPDF.Examples
                         });
                         });
                 });
                 });
         }
         }
+
+        [Test]
+        public void SuperscriptSubscript_General()
+        {
+            RenderingTest
+               .Create()
+               .PageSize(500, 500)
+               .ProduceImages()
+               .ShowResults()
+               .Render(container =>
+               {
+                   container
+                        .Padding(5)
+                        .MinimalBox()
+                        .Border(1)
+                        .Padding(10)
+                        .Text(text =>
+                        {
+                            text.DefaultTextStyle(x => x.FontSize(20));
+                            text.ParagraphSpacing(2);
+                            
+                            
+                            text.Span("In physics, mass–energy equivalence is the relationship between mass and energy in a system's rest frame, where the two values differ only by a constant and the units of measurement.");
+                            text.Span("[1][2]").Superscript();
+                            text.Span(" The principle is described by the physicist Albert Einstein's famous formula: E = mc");
+                            text.Span("2").Superscript();
+                            text.Span(". ");
+                            text.Span("[3]").Superscript();
+                            
+                            text.EmptyLine();
+                            
+                            text.Span("H");
+                            text.Span("2").Subscript();
+                            text.Span("O is the chemical formula for water, meaning that each of its molecules contains one oxygen and two hydrogen atoms.");
+
+                            text.EmptyLine();
+
+                            text.Span("H");
+                            text.Span("2").Subscript();
+                            text.Span("O");
+                        });
+               });
+        }
         
         
+        [Test]
+        public void SuperscriptSubscript_Effects()
+        {
+            RenderingTest
+               .Create()
+               .PageSize(800, 400)
+               .ProduceImages()
+               .ShowResults()
+               .Render(container =>
+               {
+                   container
+                        .Padding(25)
+                        .DefaultTextStyle(x => x.FontSize(30))
+                        .Column(column =>
+                        {
+                            column.Spacing(25);
+                            
+                            column.Item().Text(text =>
+                            {
+                                text.DefaultTextStyle(x => x.Underline());
+                                
+                                text.Span("Underline of the superscript (E = mc");
+                                text.Span("2").Superscript();
+                                text.Span(") should be at the same height as for normal text.");
+                            });
+                            
+                            column.Item().Text(text =>
+                            {
+                                text.DefaultTextStyle(x => x.Underline());
+                                
+                                text.Span("Underline of the subscript(H");
+                                text.Span("2").Subscript();
+                                text.Span("O) should be slightly lower than a normal text.");
+                            });
+                            
+                            column.Item().Text(text =>
+                            {
+                                text.DefaultTextStyle(x => x.Strikethrough());
+                                
+                                text.Span("Strikethrough of both superscript (E=mc");
+                                text.Span("2").Superscript();
+                                text.Span(") and subscript(H");
+                                text.Span("2").Subscript();
+                                text.Span("O) should be visible in the middle of the text.");
+                            });
+                        });
+               });
+        }
+
         [Test]
         [Test]
         public void ParagraphSpacing()
         public void ParagraphSpacing()
         {
         {
@@ -397,7 +489,7 @@ namespace QuestPDF.Examples
                                 text.DefaultTextStyle(x => x.BackgroundColor(Colors.Red.Lighten3).FontSize(24));
                                 text.DefaultTextStyle(x => x.BackgroundColor(Colors.Red.Lighten3).FontSize(24));
                                 
                                 
                                 text.Span("       " + Placeholders.LoremIpsum());
                                 text.Span("       " + Placeholders.LoremIpsum());
-                                text.Span(" 0123456789012345678901234567890123456789012345678901234567890123456789         ").WrapAnywhere();
+                                text.Span(" 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789         ").WrapAnywhere();
                             });
                             });
                         });
                         });
                     });
                     });

+ 1 - 4
QuestPDF.Previewer/Helpers.cs

@@ -8,16 +8,13 @@ class Helpers
     {
     {
         using var stream = File.Create(filePath);
         using var stream = File.Create(filePath);
             
             
-        var document = SKDocument.CreatePdf(stream);
+        using var document = SKDocument.CreatePdf(stream);
             
             
         foreach (var page in pages)
         foreach (var page in pages)
         {
         {
             using var canvas = document.BeginPage(page.Width, page.Height);
             using var canvas = document.BeginPage(page.Width, page.Height);
             canvas.DrawPicture(page.Picture);
             canvas.DrawPicture(page.Picture);
             document.EndPage();
             document.EndPage();
-            canvas.Dispose();
         }
         }
-        
-        document.Close();
     }
     }
 }
 }

+ 1 - 1
QuestPDF.Previewer/PreviewerWindowViewModel.cs

@@ -63,7 +63,7 @@ namespace QuestPDF.Previewer
         
         
         private void OpenLink(string path)
         private void OpenLink(string path)
         {
         {
-            var openBrowserProcess = new Process
+            using var openBrowserProcess = new Process
             {
             {
                 StartInfo = new()
                 StartInfo = new()
                 {
                 {

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

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

+ 4 - 1
QuestPDF/Drawing/DocumentGenerator.cs

@@ -164,7 +164,7 @@ namespace QuestPDF.Drawing
             return debuggingState;
             return debuggingState;
         }
         }
 
 
-        private static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
+        internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
         {
         {
             if (content == null)
             if (content == null)
                 return;
                 return;
@@ -186,6 +186,9 @@ namespace QuestPDF.Drawing
                 return;
                 return;
             }
             }
 
 
+            if (content is DynamicHost dynamicHost)
+                dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
+            
             var targetTextStyle = documentDefaultTextStyle;
             var targetTextStyle = documentDefaultTextStyle;
             
             
             if (content is DefaultTextStyle defaultTextStyleElement)
             if (content is DefaultTextStyle defaultTextStyleElement)

+ 24 - 2
QuestPDF/Drawing/FontManager.cs

@@ -2,6 +2,7 @@
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using QuestPDF.Fluent;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using SkiaSharp;
 using SkiaSharp;
 
 
@@ -68,7 +69,7 @@ namespace QuestPDF.Drawing
                 {
                 {
                     Color = SKColor.Parse(style.Color),
                     Color = SKColor.Parse(style.Color),
                     Typeface = GetTypeface(style),
                     Typeface = GetTypeface(style),
-                    TextSize = style.Size ?? 12,
+                    TextSize = (style.Size ?? 12) * GetTextScale(style),
                     IsAntialias = true,
                     IsAntialias = true,
                 };
                 };
             }
             }
@@ -76,6 +77,16 @@ namespace QuestPDF.Drawing
             static SKTypeface GetTypeface(TextStyle style)
             static SKTypeface GetTypeface(TextStyle style)
             {
             {
                 var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal);
                 var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal);
+
+                // superscript and subscript use slightly bolder font to match visually line thickness
+                if (style.FontPosition is FontPosition.Superscript or FontPosition.Subscript)
+                {
+                    var weightValue = (int)weight;
+                    weightValue = Math.Min(weightValue + 100, 1000);
+                    
+                    weight = (SKFontStyleWeight) (weightValue);
+                }
+
                 var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
                 var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
 
 
                 var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant);
                 var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant);
@@ -94,11 +105,22 @@ namespace QuestPDF.Drawing
                     $"1) install the font on your operating system or execution environment. " +
                     $"1) install the font on your operating system or execution environment. " +
                     $"2) load a font file specifically for QuestPDF usage via the QuestPDF.Drawing.FontManager.RegisterFontType(Stream fileContentStream) static method.");
                     $"2) load a font file specifically for QuestPDF usage via the QuestPDF.Drawing.FontManager.RegisterFontType(Stream fileContentStream) static method.");
             }
             }
+            
+            static float GetTextScale(TextStyle style)
+            {
+                return style.FontPosition switch
+                {
+                    FontPosition.Normal => 1f,
+                    FontPosition.Subscript => 0.625f,
+                    FontPosition.Superscript => 0.625f,
+                    _ => throw new ArgumentOutOfRangeException()
+                };
+            }
         }
         }
 
 
         internal static SKFontMetrics ToFontMetrics(this TextStyle style)
         internal static SKFontMetrics ToFontMetrics(this TextStyle style)
         {
         {
-            return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics);
+            return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics);
         }
         }
     }
     }
 }
 }

+ 104 - 0
QuestPDF/Elements/Dynamic.cs

@@ -0,0 +1,104 @@
+using System;
+using QuestPDF.Drawing;
+using QuestPDF.Drawing.Exceptions;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements
+{
+    internal class DynamicHost : Element, IStateResettable
+    {
+        private DynamicComponentProxy Child { get; }
+        private object InitialComponentState { get; set; }
+
+        internal TextStyle TextStyle { get; } = new();
+
+        public DynamicHost(DynamicComponentProxy child)
+        {
+            Child = child;
+            
+            InitialComponentState = Child.GetState();
+        }
+
+        public void ResetState()
+        {
+            Child.SetState(InitialComponentState);
+        }
+        
+        internal override SpacePlan Measure(Size availableSpace)
+        {
+            var result = GetContent(availableSpace, acceptNewState: false);
+            var content = result.Content as Element ?? Empty.Instance;
+            var measurement = content.Measure(availableSpace);
+
+            if (measurement.Type != SpacePlanType.FullRender)
+                throw new DocumentLayoutException("Dynamic component generated content that does not fit on a single page.");
+            
+            return result.HasMoreContent 
+                ? SpacePlan.PartialRender(measurement) 
+                : SpacePlan.FullRender(measurement);
+        }
+
+        internal override void Draw(Size availableSpace)
+        {
+            var content = GetContent(availableSpace, acceptNewState: true).Content as Element; 
+            content?.Draw(availableSpace);
+        }
+
+        private DynamicComponentComposeResult GetContent(Size availableSize, bool acceptNewState)
+        {
+            var componentState = Child.GetState();
+            
+            var context = new DynamicContext
+            {
+                PageNumber = PageContext.CurrentPage,
+                PageContext = PageContext,
+                Canvas = Canvas,
+                TextStyle = TextStyle,
+                
+                AvailableSize = availableSize
+            };
+            
+            var result = Child.Compose(context);
+
+            if (!acceptNewState)
+                Child.SetState(componentState);
+
+            return result;
+        }
+    }
+
+    public class DynamicContext
+    {
+        internal IPageContext PageContext { get; set; }
+        internal ICanvas Canvas { get; set; }
+        internal TextStyle TextStyle { get; set; }
+    
+        public int PageNumber { get; internal set; }
+        public Size AvailableSize { get; internal set; }
+
+        public IDynamicElement CreateElement(Action<IContainer> content)
+        {
+            var container = new DynamicElement();
+            content(container);
+            
+            container.ApplyDefaultTextStyle(TextStyle);
+            container.VisitChildren(x => x?.Initialize(PageContext, Canvas));
+            container.VisitChildren(x => (x as IStateResettable)?.ResetState());
+
+            container.Size = container.Measure(Size.Max);
+            
+            return container;
+        }
+    }
+
+    public interface IDynamicElement : IElement
+    {
+        Size Size { get; }
+    }
+
+    internal class DynamicElement : ContainerElement, IDynamicElement
+    {
+        public Size Size { get; internal set; }
+    }
+}

+ 37 - 2
QuestPDF/Elements/Table/Table.cs

@@ -19,15 +19,22 @@ namespace QuestPDF.Elements.Table
         private int RowsCount { get; set; }
         private int RowsCount { get; set; }
         private int CurrentRow { get; set; }
         private int CurrentRow { get; set; }
         
         
+        // cache that stores all cells
+        // first index: row number
+        // inner table: list of all cells that ends at the corresponding row
+        private TableCell[][] CellsCache { get; set; }
+        private int MaxRow { get; set; }
+        
         internal override void Initialize(IPageContext pageContext, ICanvas canvas)
         internal override void Initialize(IPageContext pageContext, ICanvas canvas)
         {
         {
             StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
             StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
             RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
             RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
             Cells = Cells.OrderBy(x => x.Row).ThenBy(x => x.Column).ToList();
             Cells = Cells.OrderBy(x => x.Row).ThenBy(x => x.Column).ToList();
+            BuildCache();
 
 
             base.Initialize(pageContext, canvas);
             base.Initialize(pageContext, canvas);
         }
         }
-        
+
         internal override IEnumerable<Element?> GetChildren()
         internal override IEnumerable<Element?> GetChildren()
         {
         {
             return Cells;
             return Cells;
@@ -38,6 +45,31 @@ namespace QuestPDF.Elements.Table
             Cells.ForEach(x => x.IsRendered = false);
             Cells.ForEach(x => x.IsRendered = false);
             CurrentRow = 1;
             CurrentRow = 1;
         }
         }
+
+        private void BuildCache()
+        {
+            if (CellsCache != null)
+                return;
+
+            if (Cells.Count == 0)
+            {
+                MaxRow = 0;
+                CellsCache = Array.Empty<TableCell[]>();
+                
+                return;
+            }
+            
+            var groups = Cells
+                .GroupBy(x => x.Row + x.RowSpan - 1)
+                .ToDictionary(x => x.Key, x => x.OrderBy(x => x.Column).ToArray());
+
+            MaxRow = groups.Max(x => x.Key);
+
+            CellsCache = Enumerable
+                .Range(0, MaxRow + 1)
+                .Select(x => groups.ContainsKey(x) ? groups[x] : Array.Empty<TableCell>())
+                .ToArray();
+        }
         
         
         internal override SpacePlan Measure(Size availableSpace)
         internal override SpacePlan Measure(Size availableSpace)
         {
         {
@@ -143,7 +175,10 @@ namespace QuestPDF.Elements.Table
                 var rowBottomOffsets = new DynamicDictionary<int, float>();
                 var rowBottomOffsets = new DynamicDictionary<int, float>();
                 var commands = new List<TableCellRenderingCommand>();
                 var commands = new List<TableCellRenderingCommand>();
                 
                 
-                var cellsToTry = Cells.Where(x => x.Row + x.RowSpan - 1 >= CurrentRow);
+                var cellsToTry = Enumerable
+                    .Range(CurrentRow, MaxRow - CurrentRow + 1)
+                    .SelectMany(x => CellsCache[x]);
+                
                 var currentRow = CurrentRow;
                 var currentRow = CurrentRow;
                 var maxRenderingRow = RowsCount;
                 var maxRenderingRow = RowsCount;
                 
                 

+ 31 - 7
QuestPDF/Elements/Text/Items/TextBlockSpan.cs

@@ -12,7 +12,7 @@ namespace QuestPDF.Elements.Text.Items
         private const char Space = ' ';
         private const char Space = ' ';
         
         
         public string Text { get; set; }
         public string Text { get; set; }
-        public TextStyle Style { get; set; } = new TextStyle();
+        public TextStyle Style { get; set; } = new();
 
 
         private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
         private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
 
 
@@ -119,29 +119,53 @@ namespace QuestPDF.Elements.Text.Items
             // there is no available space to wrap text
             // there is no available space to wrap text
             // if the item is first within the line, perform safe mode and chop the word
             // if the item is first within the line, perform safe mode and chop the word
             // otherwise, move the item into the next line
             // otherwise, move the item into the next line
-            return isFirstElementInLine ? (textLength, textLength + 1) : null;
+            return isFirstElementInLine ? (textLength, textLength) : null;
         }
         }
         
         
         public virtual void Draw(TextDrawingRequest request)
         public virtual void Draw(TextDrawingRequest request)
         {
         {
             var fontMetrics = Style.ToFontMetrics();
             var fontMetrics = Style.ToFontMetrics();
 
 
+            var glyphOffset = GetGlyphOffset();
             var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
             var 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.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
-            request.Canvas.DrawText(text, Position.Zero, Style);
+            request.Canvas.DrawText(text, new Position(0, glyphOffset), Style);
 
 
             // draw underline
             // draw underline
             if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
             if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
-                DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness ?? 1);
+            {
+                var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffset;
+                DrawLine(fontMetrics.UnderlinePosition.Value + underlineOffset, fontMetrics.UnderlineThickness ?? 1);
+            }
             
             
             // draw stroke
             // draw stroke
             if ((Style.HasStrikethrough ?? false) && fontMetrics.StrikeoutPosition.HasValue)
             if ((Style.HasStrikethrough ?? false) && fontMetrics.StrikeoutPosition.HasValue)
-                DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness ?? 1);
-
+            {
+                var strikeoutThickness = fontMetrics.StrikeoutThickness ?? 1;
+                strikeoutThickness *= Style.FontPosition == FontPosition.Normal ? 1f : 0.625f;
+                
+                DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffset, strikeoutThickness);
+            }
+            
             void DrawLine(float offset, float thickness)
             void DrawLine(float offset, float thickness)
             {
             {
-                request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
+                request.Canvas.DrawRectangle(new Position(0, offset), new Size(request.TextSize.Width, thickness), Style.Color);
+            }
+
+            float GetGlyphOffset()
+            {
+                var fontSize = Style.Size ?? 12f;
+
+                var offsetFactor = Style.FontPosition switch
+                {
+                    FontPosition.Normal => 0,
+                    FontPosition.Subscript => 0.1f,
+                    FontPosition.Superscript => -0.35f,
+                    _ => throw new ArgumentOutOfRangeException()
+                };
+
+                return fontSize * offsetFactor;
             }
             }
         }
         }
     }
     }

+ 14 - 0
QuestPDF/Fluent/DynamicComponentExtensions.cs

@@ -0,0 +1,14 @@
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Fluent
+{
+    public static class DynamicComponentExtensions
+    {
+        public static void Dynamic<TState>(this IContainer element, IDynamicComponent<TState> dynamicElement) where TState : struct
+        {
+            var componentProxy = DynamicComponentProxy.CreateFrom(dynamicElement);
+            element.Element(new DynamicHost(componentProxy));
+        }
+    }
+}

+ 1 - 1
QuestPDF/Fluent/TextExtensions.cs

@@ -135,7 +135,7 @@ namespace QuestPDF.Fluent
         private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
         private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
         {
         {
             var style = DefaultStyle.Clone();
             var style = DefaultStyle.Clone();
-            var descriptor = new TextPageNumberDescriptor(DefaultStyle);
+            var descriptor = new TextPageNumberDescriptor(style);
             
             
             AddItemToLastTextBlock(new TextBlockPageNumber
             AddItemToLastTextBlock(new TextBlockPageNumber
             {
             {

+ 24 - 1
QuestPDF/Fluent/TextSpanDescriptorExtensions.cs

@@ -126,7 +126,30 @@ namespace QuestPDF.Fluent
         {
         {
             return descriptor.Weight(FontWeight.ExtraBlack);
             return descriptor.Weight(FontWeight.ExtraBlack);
         }
         }
-        
+
+        #endregion
+
+        #region Position
+        public static T NormalPosition<T>(this T descriptor) where T : TextSpanDescriptor
+        {
+            return descriptor.Position(FontPosition.Normal);
+        }
+
+        public static T Subscript<T>(this T descriptor) where T : TextSpanDescriptor
+        {
+            return descriptor.Position(FontPosition.Subscript);
+        }
+
+        public static T Superscript<T>(this T descriptor) where T : TextSpanDescriptor
+        {
+            return descriptor.Position(FontPosition.Superscript);
+        }
+
+        private static T Position<T>(this T descriptor, FontPosition fontPosition) where T : TextSpanDescriptor
+        {
+            descriptor.TextStyle.FontPosition = fontPosition;
+            return descriptor;
+        }
         #endregion
         #endregion
     }
     }
 }
 }

+ 26 - 1
QuestPDF/Fluent/TextStyleExtensions.cs

@@ -133,7 +133,32 @@ namespace QuestPDF.Fluent
         {
         {
             return style.Weight(FontWeight.ExtraBlack);
             return style.Weight(FontWeight.ExtraBlack);
         }
         }
-        
+
+        #endregion
+
+        #region Position
+        public static TextStyle NormalPosition(this TextStyle style)
+        {
+            return style.Position(FontPosition.Normal);
+        }
+
+        public static TextStyle Subscript(this TextStyle style)
+        {
+            return style.Position(FontPosition.Subscript);
+        }
+
+        public static TextStyle Superscript(this TextStyle style)
+        {
+            return style.Position(FontPosition.Superscript);
+        }
+
+        private static TextStyle Position(this TextStyle style, FontPosition fontPosition)
+        {
+            if (style.FontPosition == fontPosition)
+                return style;
+
+            return style.Mutate(t => t.FontPosition = fontPosition);
+        }
         #endregion
         #endregion
     }
     }
 }
 }

+ 9 - 0
QuestPDF/Infrastructure/FontPosition.cs

@@ -0,0 +1,9 @@
+namespace QuestPDF.Infrastructure
+{
+    internal enum FontPosition
+    {
+        Normal,
+        Subscript,
+        Superscript,
+    }
+}

+ 35 - 0
QuestPDF/Infrastructure/IDynamicComponent.cs

@@ -0,0 +1,35 @@
+using System;
+using QuestPDF.Elements;
+using QuestPDF.Helpers;
+
+namespace QuestPDF.Infrastructure
+{
+    internal class DynamicComponentProxy
+    {
+        internal Action<object> SetState { get; private set; }
+        internal Func<object> GetState { get; private set; }
+        internal Func<DynamicContext, DynamicComponentComposeResult> Compose { get; private set; }
+        
+        internal static DynamicComponentProxy CreateFrom<TState>(IDynamicComponent<TState> component) where TState : struct
+        {
+            return new DynamicComponentProxy
+            {
+                GetState = () => component.State,
+                SetState = x => component.State = (TState)x,
+                Compose = component.Compose
+            };
+        }
+    }
+
+    public class DynamicComponentComposeResult
+    {
+        public IElement Content { get; set; }
+        public bool HasMoreContent { get; set; }
+    }
+    
+    public interface IDynamicComponent<TState> where TState : struct
+    {
+        TState State { get; set; }
+        DynamicComponentComposeResult Compose(DynamicContext context);
+    }
+}

+ 5 - 1
QuestPDF/Infrastructure/TextStyle.cs

@@ -13,6 +13,7 @@ namespace QuestPDF.Infrastructure
         internal float? Size { get; set; }
         internal float? Size { get; set; }
         internal float? LineHeight { get; set; }
         internal float? LineHeight { get; set; }
         internal FontWeight? FontWeight { get; set; }
         internal FontWeight? FontWeight { get; set; }
+        internal FontPosition? FontPosition { get; set; }
         internal bool? IsItalic { get; set; }
         internal bool? IsItalic { get; set; }
         internal bool? HasStrikethrough { get; set; }
         internal bool? HasStrikethrough { get; set; }
         internal bool? HasUnderline { get; set; }
         internal bool? HasUnderline { get; set; }
@@ -29,6 +30,7 @@ namespace QuestPDF.Infrastructure
             Size = 12,
             Size = 12,
             LineHeight = 1.2f,
             LineHeight = 1.2f,
             FontWeight = Infrastructure.FontWeight.Normal,
             FontWeight = Infrastructure.FontWeight.Normal,
+            FontPosition = Infrastructure.FontPosition.Normal,
             IsItalic = false,
             IsItalic = false,
             HasStrikethrough = false,
             HasStrikethrough = false,
             HasUnderline = false,
             HasUnderline = false,
@@ -45,7 +47,7 @@ namespace QuestPDF.Infrastructure
             HasGlobalStyleApplied = true;
             HasGlobalStyleApplied = true;
 
 
             ApplyParentStyle(globalStyle);
             ApplyParentStyle(globalStyle);
-            PaintKey ??= (FontFamily, Size, FontWeight, IsItalic, Color);
+            PaintKey ??= (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color);
             FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic);
             FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic);
         }
         }
         
         
@@ -57,6 +59,7 @@ namespace QuestPDF.Infrastructure
             Size ??= parentStyle.Size;
             Size ??= parentStyle.Size;
             LineHeight ??= parentStyle.LineHeight;
             LineHeight ??= parentStyle.LineHeight;
             FontWeight ??= parentStyle.FontWeight;
             FontWeight ??= parentStyle.FontWeight;
+            FontPosition ??= parentStyle.FontPosition;
             IsItalic ??= parentStyle.IsItalic;
             IsItalic ??= parentStyle.IsItalic;
             HasStrikethrough ??= parentStyle.HasStrikethrough;
             HasStrikethrough ??= parentStyle.HasStrikethrough;
             HasUnderline ??= parentStyle.HasUnderline;
             HasUnderline ??= parentStyle.HasUnderline;
@@ -71,6 +74,7 @@ namespace QuestPDF.Infrastructure
             Size = parentStyle.Size ?? Size;
             Size = parentStyle.Size ?? Size;
             LineHeight = parentStyle.LineHeight ?? LineHeight;
             LineHeight = parentStyle.LineHeight ?? LineHeight;
             FontWeight = parentStyle.FontWeight ?? FontWeight;
             FontWeight = parentStyle.FontWeight ?? FontWeight;
+            FontPosition = parentStyle.FontPosition ?? FontPosition;
             IsItalic = parentStyle.IsItalic ?? IsItalic;
             IsItalic = parentStyle.IsItalic ?? IsItalic;
             HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
             HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
             HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
             HasUnderline = parentStyle.HasUnderline ?? HasUnderline;

+ 1 - 1
QuestPDF/Previewer/PreviewerExtensions.cs

@@ -20,7 +20,7 @@ namespace QuestPDF.Previewer
         {
         {
             var previewerService = new PreviewerService(port);
             var previewerService = new PreviewerService(port);
             
             
-            var cancellationTokenSource = new CancellationTokenSource();
+            using var cancellationTokenSource = new CancellationTokenSource();
             previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
             previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
             
             
             await previewerService.Connect();
             await previewerService.Connect();

+ 9 - 8
QuestPDF/Previewer/PreviewerService.cs

@@ -24,7 +24,7 @@ namespace QuestPDF.Previewer
             HttpClient = new()
             HttpClient = new()
             {
             {
                 BaseAddress = new Uri($"http://localhost:{port}/"), 
                 BaseAddress = new Uri($"http://localhost:{port}/"), 
-                Timeout = TimeSpan.FromMilliseconds(250)
+                Timeout = TimeSpan.FromSeconds(1)
             };
             };
         }
         }
 
 
@@ -46,7 +46,7 @@ namespace QuestPDF.Previewer
         {
         {
             try
             try
             {
             {
-                var result = await HttpClient.GetAsync("/ping");
+                using var result = await HttpClient.GetAsync("/ping");
                 return result.IsSuccessStatusCode;
                 return result.IsSuccessStatusCode;
             }
             }
             catch
             catch
@@ -57,7 +57,7 @@ namespace QuestPDF.Previewer
         
         
         private async Task<Version> GetPreviewerVersion()
         private async Task<Version> GetPreviewerVersion()
         {
         {
-            var result = await HttpClient.GetAsync("/version");
+            using var result = await HttpClient.GetAsync("/version");
             return await result.Content.ReadFromJsonAsync<Version>();
             return await result.Content.ReadFromJsonAsync<Version>();
         }
         }
         
         
@@ -81,6 +81,7 @@ namespace QuestPDF.Previewer
                 Task.Run(async () =>
                 Task.Run(async () =>
                 {
                 {
                     await process.WaitForExitAsync();
                     await process.WaitForExitAsync();
+                    process.Dispose();
                     OnPreviewerStopped?.Invoke();
                     OnPreviewerStopped?.Invoke();
                 });
                 });
             }
             }
@@ -93,17 +94,17 @@ namespace QuestPDF.Previewer
 
 
         private void CheckVersionCompatibility(Version version)
         private void CheckVersionCompatibility(Version version)
         {
         {
-            if (version.Major == 2022 && version.Minor == 4)
+            if (version.Major == 2022 && version.Minor == 5)
                 return;
                 return;
             
             
             throw new Exception($"Previewer version is not compatible. Possible solutions: " +
             throw new Exception($"Previewer version is not compatible. Possible solutions: " +
                                 $"1) Update the QuestPDF library to newer version. " +
                                 $"1) Update the QuestPDF library to newer version. " +
-                                $"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version 2022.4'");
+                                $"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version 2022.5'");
         }
         }
         
         
         private async Task WaitForConnection()
         private async Task WaitForConnection()
         {
         {
-            var cancellationTokenSource = new CancellationTokenSource();
+            using var cancellationTokenSource = new CancellationTokenSource();
             cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
             cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
             
             
             var cancellationToken = cancellationTokenSource.Token; 
             var cancellationToken = cancellationTokenSource.Token; 
@@ -124,7 +125,7 @@ namespace QuestPDF.Previewer
         
         
         public async Task RefreshPreview(ICollection<PreviewerPicture> pictures)
         public async Task RefreshPreview(ICollection<PreviewerPicture> pictures)
         {
         {
-            var multipartContent = new MultipartFormDataContent();
+            using var multipartContent = new MultipartFormDataContent();
 
 
             var pages = new List<PreviewerRefreshCommand.Page>();
             var pages = new List<PreviewerRefreshCommand.Page>();
             
             
@@ -149,7 +150,7 @@ namespace QuestPDF.Previewer
             
             
             multipartContent.Add(JsonContent.Create(command), "command");
             multipartContent.Add(JsonContent.Create(command), "command");
 
 
-            await HttpClient.PostAsync("/update/preview", multipartContent);
+            using var _ = await HttpClient.PostAsync("/update/preview", multipartContent);
 
 
             foreach (var picture in pictures)
             foreach (var picture in pictures)
                 picture.Picture.Dispose();
                 picture.Picture.Dispose();

+ 1 - 1
QuestPDF/QuestPDF.csproj

@@ -4,7 +4,7 @@
         <Authors>MarcinZiabek</Authors>
         <Authors>MarcinZiabek</Authors>
         <Company>CodeFlint</Company>
         <Company>CodeFlint</Company>
         <PackageId>QuestPDF</PackageId>
         <PackageId>QuestPDF</PackageId>
-        <Version>2022.4.1</Version>
+        <Version>2022.5.0</Version>
         <PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
         <PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
         <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
         <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
         <LangVersion>9</LangVersion>
         <LangVersion>9</LangVersion>

+ 4 - 6
QuestPDF/Resources/ReleaseNotes.txt

@@ -1,6 +1,4 @@
-Release theme:
-Introduced the QuestPDF Previewer tool - a hot-reload powered, cross-platform program that visualizes your PDF document and updates its preview every time you make a code change. You don't need to recompile your code after every small adjustment. Save time and enjoy the design process! (available only for dotnet 6 and beyond)
-
-Other changes:
-- Improved default word-wrapping algorithm to better handle words which do not fit on the available width,
-- Introduced new word-wrapping option 'WrapAnywhere' that wraps word at the last possible character instead of moving it into new line.
+- Implemented the DynamicComponent element (useful for most advanced cases, e.g. per-page totals),
+- Extend text rendering capabilities by adding subscript and superscript effects,
+- Improved table rendering performance,
+- Previewer tool stability fixes.