From a6f4fd7f497b75d50687d577cb7ab6cdb1296f5d Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Mon, 11 Apr 2022 21:23:16 +0200 Subject: [PATCH 01/16] explicit disposal --- QuestPDF.Previewer/Helpers.cs | 5 +---- QuestPDF.Previewer/PreviewerWindowViewModel.cs | 2 +- QuestPDF/Previewer/PreviewerExtensions.cs | 2 +- QuestPDF/Previewer/PreviewerService.cs | 13 +++++++------ 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/QuestPDF.Previewer/Helpers.cs b/QuestPDF.Previewer/Helpers.cs index fe0a809..b01c2f3 100644 --- a/QuestPDF.Previewer/Helpers.cs +++ b/QuestPDF.Previewer/Helpers.cs @@ -8,16 +8,13 @@ class Helpers { using var stream = File.Create(filePath); - var document = SKDocument.CreatePdf(stream); + using var document = SKDocument.CreatePdf(stream); foreach (var page in pages) { using var canvas = document.BeginPage(page.Width, page.Height); canvas.DrawPicture(page.Picture); document.EndPage(); - canvas.Dispose(); } - - document.Close(); } } \ No newline at end of file diff --git a/QuestPDF.Previewer/PreviewerWindowViewModel.cs b/QuestPDF.Previewer/PreviewerWindowViewModel.cs index 4246ed5..fa353c9 100644 --- a/QuestPDF.Previewer/PreviewerWindowViewModel.cs +++ b/QuestPDF.Previewer/PreviewerWindowViewModel.cs @@ -63,7 +63,7 @@ namespace QuestPDF.Previewer private void OpenLink(string path) { - var openBrowserProcess = new Process + using var openBrowserProcess = new Process { StartInfo = new() { diff --git a/QuestPDF/Previewer/PreviewerExtensions.cs b/QuestPDF/Previewer/PreviewerExtensions.cs index b3c401c..17eadbc 100644 --- a/QuestPDF/Previewer/PreviewerExtensions.cs +++ b/QuestPDF/Previewer/PreviewerExtensions.cs @@ -20,7 +20,7 @@ namespace QuestPDF.Previewer { var previewerService = new PreviewerService(port); - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel(); await previewerService.Connect(); diff --git a/QuestPDF/Previewer/PreviewerService.cs b/QuestPDF/Previewer/PreviewerService.cs index 5884003..1cc0f6b 100644 --- a/QuestPDF/Previewer/PreviewerService.cs +++ b/QuestPDF/Previewer/PreviewerService.cs @@ -46,7 +46,7 @@ namespace QuestPDF.Previewer { try { - var result = await HttpClient.GetAsync("/ping"); + using var result = await HttpClient.GetAsync("/ping"); return result.IsSuccessStatusCode; } catch @@ -57,7 +57,7 @@ namespace QuestPDF.Previewer private async Task GetPreviewerVersion() { - var result = await HttpClient.GetAsync("/version"); + using var result = await HttpClient.GetAsync("/version"); return await result.Content.ReadFromJsonAsync(); } @@ -81,6 +81,7 @@ namespace QuestPDF.Previewer Task.Run(async () => { await process.WaitForExitAsync(); + process.Dispose(); OnPreviewerStopped?.Invoke(); }); } @@ -103,14 +104,14 @@ namespace QuestPDF.Previewer private async Task WaitForConnection() { - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10)); var cancellationToken = cancellationTokenSource.Token; while (true) { - await Task.Delay(TimeSpan.FromMilliseconds(250)); + await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken); if (cancellationToken.IsCancellationRequested) throw new Exception($"Cannot connect to the QuestPDF Previewer tool. Please make sure that your Operating System does not block HTTP connections on port {Port}."); @@ -124,7 +125,7 @@ namespace QuestPDF.Previewer public async Task RefreshPreview(ICollection pictures) { - var multipartContent = new MultipartFormDataContent(); + using var multipartContent = new MultipartFormDataContent(); var pages = new List(); @@ -149,7 +150,7 @@ namespace QuestPDF.Previewer 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) picture.Picture.Dispose(); From 3ffab6e93329003cf39a11a116c98a9bc42933e3 Mon Sep 17 00:00:00 2001 From: Bebo-Maker Date: Tue, 12 Apr 2022 18:48:28 +0200 Subject: [PATCH 02/16] Implement superscript and subscript. --- QuestPDF/Drawing/FontManager.cs | 15 ++++++++++- QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 18 ++++++++++--- .../Fluent/TextSpanDescriptorExtensions.cs | 25 ++++++++++++++++- QuestPDF/Fluent/TextStyleExtensions.cs | 27 ++++++++++++++++++- QuestPDF/Infrastructure/FontPosition.cs | 9 +++++++ QuestPDF/Infrastructure/TextStyle.cs | 6 ++++- 6 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 QuestPDF/Infrastructure/FontPosition.cs diff --git a/QuestPDF/Drawing/FontManager.cs b/QuestPDF/Drawing/FontManager.cs index 98688a4..213a005 100644 --- a/QuestPDF/Drawing/FontManager.cs +++ b/QuestPDF/Drawing/FontManager.cs @@ -68,7 +68,7 @@ namespace QuestPDF.Drawing { Color = SKColor.Parse(style.Color), Typeface = GetTypeface(style), - TextSize = style.Size ?? 12, + TextSize = (style.Size ?? 12) * GetTextScale(style), IsAntialias = true, }; } @@ -76,6 +76,11 @@ namespace QuestPDF.Drawing static SKTypeface GetTypeface(TextStyle style) { var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal); + + //Extra weight for superscript and subscript + if (style.FontPosition == FontPosition.Superscript || style.FontPosition == FontPosition.Subscript) + weight = (SKFontStyleWeight)((int)weight + 100); + var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright; var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant); @@ -100,5 +105,13 @@ namespace QuestPDF.Drawing { return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics); } + + private static float GetTextScale(TextStyle style) + { + if (style.FontPosition == FontPosition.Superscript || style.FontPosition == FontPosition.Subscript) + return 0.65f; + + return 1; + } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 39d493e..7a5e500 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -126,23 +126,35 @@ namespace QuestPDF.Elements.Text.Items { var fontMetrics = Style.ToFontMetrics(); + var glyphOffsetY = GetVerticalGlyphOffsetForStyle(Style); + 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); + request.Canvas.DrawText(text, new Position(0, glyphOffsetY), Style); // draw underline if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue) - DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness ?? 1); + DrawLine(glyphOffsetY + fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness ?? 1); // draw stroke if ((Style.HasStrikethrough ?? false) && fontMetrics.StrikeoutPosition.HasValue) - DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness ?? 1); + DrawLine(glyphOffsetY + fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness ?? 1); void DrawLine(float offset, float thickness) { request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color); } } + + private static float GetVerticalGlyphOffsetForStyle(TextStyle style) + { + if (style.FontPosition == FontPosition.Superscript) + return (style.Size ?? 12f) * -0.35f; + if (style.FontPosition == FontPosition.Subscript) + return (style.Size ?? 12f) * 0.1f; + + return 0; + } } } \ No newline at end of file diff --git a/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs index f30653b..41af136 100644 --- a/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs +++ b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs @@ -126,7 +126,30 @@ namespace QuestPDF.Fluent { return descriptor.Weight(FontWeight.ExtraBlack); } - + + #endregion + + #region Position + public static T NormalPosition(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Position(FontPosition.Normal); + } + + public static T Subscript(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Position(FontPosition.Subscript); + } + + public static T Superscript(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Position(FontPosition.Superscript); + } + + private static T Position(this T descriptor, FontPosition fontPosition) where T : TextSpanDescriptor + { + descriptor.TextStyle.FontPosition = fontPosition; + return descriptor; + } #endregion } } \ No newline at end of file diff --git a/QuestPDF/Fluent/TextStyleExtensions.cs b/QuestPDF/Fluent/TextStyleExtensions.cs index 2686432..7823908 100644 --- a/QuestPDF/Fluent/TextStyleExtensions.cs +++ b/QuestPDF/Fluent/TextStyleExtensions.cs @@ -133,7 +133,32 @@ namespace QuestPDF.Fluent { 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 } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/FontPosition.cs b/QuestPDF/Infrastructure/FontPosition.cs new file mode 100644 index 0000000..d332740 --- /dev/null +++ b/QuestPDF/Infrastructure/FontPosition.cs @@ -0,0 +1,9 @@ +namespace QuestPDF.Infrastructure +{ + internal enum FontPosition + { + Normal = 0, + Subscript, + Superscript, + } +} diff --git a/QuestPDF/Infrastructure/TextStyle.cs b/QuestPDF/Infrastructure/TextStyle.cs index a0a72df..d734988 100644 --- a/QuestPDF/Infrastructure/TextStyle.cs +++ b/QuestPDF/Infrastructure/TextStyle.cs @@ -13,6 +13,7 @@ namespace QuestPDF.Infrastructure internal float? Size { get; set; } internal float? LineHeight { get; set; } internal FontWeight? FontWeight { get; set; } + internal FontPosition? FontPosition { get; set; } internal bool? IsItalic { get; set; } internal bool? HasStrikethrough { get; set; } internal bool? HasUnderline { get; set; } @@ -29,6 +30,7 @@ namespace QuestPDF.Infrastructure Size = 12, LineHeight = 1.2f, FontWeight = Infrastructure.FontWeight.Normal, + FontPosition = Infrastructure.FontPosition.Normal, IsItalic = false, HasStrikethrough = false, HasUnderline = false, @@ -45,7 +47,7 @@ namespace QuestPDF.Infrastructure HasGlobalStyleApplied = true; ApplyParentStyle(globalStyle); - PaintKey ??= (FontFamily, Size, FontWeight, IsItalic, Color); + PaintKey ??= (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color); FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic); } @@ -57,6 +59,7 @@ namespace QuestPDF.Infrastructure Size ??= parentStyle.Size; LineHeight ??= parentStyle.LineHeight; FontWeight ??= parentStyle.FontWeight; + FontPosition ??= parentStyle.FontPosition; IsItalic ??= parentStyle.IsItalic; HasStrikethrough ??= parentStyle.HasStrikethrough; HasUnderline ??= parentStyle.HasUnderline; @@ -71,6 +74,7 @@ namespace QuestPDF.Infrastructure Size = parentStyle.Size ?? Size; LineHeight = parentStyle.LineHeight ?? LineHeight; FontWeight = parentStyle.FontWeight ?? FontWeight; + FontPosition = parentStyle.FontPosition ?? FontPosition; IsItalic = parentStyle.IsItalic ?? IsItalic; HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough; HasUnderline = parentStyle.HasUnderline ?? HasUnderline; From 82cd1d783511cfb928d17beccc440b92767a3efc Mon Sep 17 00:00:00 2001 From: Bebo-Maker Date: Tue, 12 Apr 2022 18:49:34 +0200 Subject: [PATCH 03/16] Add example for superscript/subscript. --- QuestPDF.Examples/TextExamples.cs | 46 ++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index 1b26a24..0987ba9 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -36,7 +36,51 @@ namespace QuestPDF.Examples }); }); } - + + [Test] + public void SuperscriptSubscript() + { + RenderingTest + .Create() + .PageSize(500, 300) + + .ProduceImages() + .ShowResults() + .Render(container => + { + container + .Padding(5) + .MinimalBox() + .Border(1) + .Padding(10) + .Text(text => + { + text.Span("E=mc"); + text.Span("2").Superscript(); + + text.EmptyLine(); + + text.Span("H"); + text.Span("2").Subscript(); + text.Span("O"); + + text.EmptyLine(); + + text.ParagraphSpacing(2); + + var style = TextStyle.Default.Underline(); + + text.Span("Subscript").Style(style).Subscript().BackgroundColor(Colors.Green.Medium); + text.Span("Normal").Style(style).NormalPosition().BackgroundColor(Colors.Blue.Medium); + text.Line("Superscript").Style(style).Superscript().BackgroundColor(Colors.Red.Medium); + + text.Line("Superscript").Style(style).Superscript().BackgroundColor(Colors.Green.Medium); + text.Line("Normal").Style(style).NormalPosition().BackgroundColor(Colors.Blue.Medium); + text.Line("Subscript").Style(style).Subscript().BackgroundColor(Colors.Red.Medium); + }); + }); + } + [Test] public void ParagraphSpacing() { From c63b0658f352c56732659b3ec2390ade90596481 Mon Sep 17 00:00:00 2001 From: Bebo-Maker Date: Tue, 12 Apr 2022 19:14:00 +0200 Subject: [PATCH 04/16] Ignore FontPosition when calculating font metrics. --- QuestPDF/Drawing/FontManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/QuestPDF/Drawing/FontManager.cs b/QuestPDF/Drawing/FontManager.cs index 213a005..281028f 100644 --- a/QuestPDF/Drawing/FontManager.cs +++ b/QuestPDF/Drawing/FontManager.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; +using QuestPDF.Fluent; using QuestPDF.Infrastructure; using SkiaSharp; @@ -103,7 +104,7 @@ namespace QuestPDF.Drawing 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); } private static float GetTextScale(TextStyle style) From bc8a9e5dc569a9ee5d94bca31901150729bf1b9a Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Sat, 23 Apr 2022 23:13:38 +0200 Subject: [PATCH 05/16] Fixed mutating text style for page-related text items --- QuestPDF/Fluent/TextExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuestPDF/Fluent/TextExtensions.cs b/QuestPDF/Fluent/TextExtensions.cs index 9b7de06..2f425ed 100644 --- a/QuestPDF/Fluent/TextExtensions.cs +++ b/QuestPDF/Fluent/TextExtensions.cs @@ -135,7 +135,7 @@ namespace QuestPDF.Fluent private TextPageNumberDescriptor PageNumber(Func pageNumber) { var style = DefaultStyle.Clone(); - var descriptor = new TextPageNumberDescriptor(DefaultStyle); + var descriptor = new TextPageNumberDescriptor(style); AddItemToLastTextBlock(new TextBlockPageNumber { From 8a1be90c4fb99a1ed70ca1478c73813faabe4ce5 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Tue, 26 Apr 2022 00:36:50 +0200 Subject: [PATCH 06/16] Ported dynamic component implementation prototype --- QuestPDF.Examples/DynamicExamples.cs | 169 ++++++++++++++++++ QuestPDF/Drawing/DocumentGenerator.cs | 5 +- QuestPDF/Elements/Dynamic.cs | 125 +++++++++++++ QuestPDF/Fluent/DynamicComponentExtensions.cs | 18 ++ QuestPDF/Infrastructure/IDynamicComponent.cs | 9 + 5 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 QuestPDF.Examples/DynamicExamples.cs create mode 100644 QuestPDF/Elements/Dynamic.cs create mode 100644 QuestPDF/Fluent/DynamicComponentExtensions.cs create mode 100644 QuestPDF/Infrastructure/IDynamicComponent.cs diff --git a/QuestPDF.Examples/DynamicExamples.cs b/QuestPDF.Examples/DynamicExamples.cs new file mode 100644 index 0000000..7353198 --- /dev/null +++ b/QuestPDF.Examples/DynamicExamples.cs @@ -0,0 +1,169 @@ +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 class OrdersTable : IDynamicComponent + { + private ICollection Items { get; } + private ICollection ItemsLeft { get; set; } + + public OrdersTable(ICollection items) + { + Items = items; + } + + public void Compose(DynamicContext context, IDynamicContainer container) + { + if (context.Operation == DynamicLayoutOperation.Reset) + { + ItemsLeft = new List(Items); + return; + } + + var header = ComposeHeader(context); + var sampleFooter = ComposeFooter(context, Enumerable.Empty()); + var decorationHeight = header.Size.Height + sampleFooter.Size.Height; + + var rows = GetItemsForPage(context, decorationHeight).ToList(); + var footer = ComposeFooter(context, rows.Select(x => x.Item)); + + if (ItemsLeft.Count > rows.Count) + container.HasMoreContent(); + + if (context.Operation == DynamicLayoutOperation.Draw) + ItemsLeft = ItemsLeft.Skip(rows.Count).ToList(); + + 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); + }); + } + + private IDynamicElement ComposeHeader(DynamicContext context) + { + return context.CreateElement(element => + { + element + .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 items) + { + var total = items.Sum(x => x.Count * x.Price); + + return context.CreateElement(element => + { + element + .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; + var counter = Items.Count - ItemsLeft.Count + 1; + + foreach (var orderItem in ItemsLeft) + { + var element = context.CreateElement(content => + { + content + .BorderBottom(1) + .BorderColor(Colors.Grey.Lighten2) + .Padding(5) + .Row(row => + { + row.ConstantItem(30).Text(counter++); + row.RelativeItem().Text(orderItem.ItemName); + row.ConstantItem(50).AlignRight().Text(orderItem.Count); + row.ConstantItem(50).AlignRight().Text($"{orderItem.Price}$"); + row.ConstantItem(50).AlignRight().Text($"{orderItem.Count*orderItem.Price}$"); + }); + }); + + var elementHeight = element.Size.Height; + + if (totalHeight + elementHeight > context.AvailableSize.Height) + break; + + totalHeight += elementHeight; + yield return (orderItem, 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)); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Drawing/DocumentGenerator.cs b/QuestPDF/Drawing/DocumentGenerator.cs index 6e43689..6974470 100644 --- a/QuestPDF/Drawing/DocumentGenerator.cs +++ b/QuestPDF/Drawing/DocumentGenerator.cs @@ -164,7 +164,7 @@ namespace QuestPDF.Drawing return debuggingState; } - private static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle) + internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle) { if (content == null) return; @@ -186,6 +186,9 @@ namespace QuestPDF.Drawing return; } + if (content is DynamicHost dynamicHost) + dynamicHost.TextStyle = documentDefaultTextStyle; + var targetTextStyle = documentDefaultTextStyle; if (content is DefaultTextStyle defaultTextStyleElement) diff --git a/QuestPDF/Elements/Dynamic.cs b/QuestPDF/Elements/Dynamic.cs new file mode 100644 index 0000000..a62c8cb --- /dev/null +++ b/QuestPDF/Elements/Dynamic.cs @@ -0,0 +1,125 @@ +using System; +using QuestPDF.Drawing; +using QuestPDF.Drawing.Exceptions; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class DynamicHost : Element, IStateResettable + { + private IDynamicComponent Child { get; } + internal TextStyle TextStyle { get; set; } + + public DynamicHost(IDynamicComponent child) + { + Child = child; + } + + public void ResetState() + { + GetContent(Size.Zero, DynamicLayoutOperation.Reset); + } + + internal override SpacePlan Measure(Size availableSpace) + { + var content = GetContent(availableSpace, DynamicLayoutOperation.Measure); + 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 content.HasMoreContent + ? SpacePlan.PartialRender(measurement) + : SpacePlan.FullRender(measurement); + } + + internal override void Draw(Size availableSpace) + { + GetContent(availableSpace, DynamicLayoutOperation.Draw).Draw(availableSpace); + } + + DynamicContainer GetContent(Size availableSize, DynamicLayoutOperation operation) + { + var context = new DynamicContext + { + PageNumber = PageContext.CurrentPage, + PageContext = PageContext, + Canvas = Canvas, + TextStyle = TextStyle, + + AvailableSize = availableSize, + Operation = operation + }; + + var container = new DynamicContainer(); + Child.Compose(context, container); + + container.VisitChildren(x => x?.Initialize(PageContext, Canvas)); + container.VisitChildren(x => (x as IStateResettable)?.ResetState()); + + return container; + } + } + + public enum DynamicLayoutOperation + { + Reset, + Measure, + Draw + } + + 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 DynamicLayoutOperation Operation { get; internal set; } + + public IDynamicElement CreateElement(Action 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(AvailableSize); + + return container; + } + } + + public interface IDynamicContainer : IContainer + { + + } + + internal class DynamicContainer : Container, IDynamicContainer + { + internal bool HasMoreContent { get; set; } + } + + public static class DynamicContainerExtensions + { + public static IDynamicContainer HasMoreContent(this IDynamicContainer container) + { + (container as DynamicContainer).HasMoreContent = true; + return container; + } + } + + public interface IDynamicElement : IElement + { + Size Size { get; } + } + + internal class DynamicElement : ContainerElement, IDynamicElement + { + public Size Size { get; internal set; } + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/DynamicComponentExtensions.cs b/QuestPDF/Fluent/DynamicComponentExtensions.cs new file mode 100644 index 0000000..c5335ca --- /dev/null +++ b/QuestPDF/Fluent/DynamicComponentExtensions.cs @@ -0,0 +1,18 @@ +using QuestPDF.Elements; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Fluent +{ + public static class DynamicComponentExtensions + { + public static void Dynamic(this IContainer element) where TDynamic : IDynamicComponent, new() + { + element.Dynamic(new TDynamic()); + } + + public static void Dynamic(this IContainer element, IDynamicComponent dynamicElement) + { + element.Element(new DynamicHost(dynamicElement)); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Infrastructure/IDynamicComponent.cs b/QuestPDF/Infrastructure/IDynamicComponent.cs new file mode 100644 index 0000000..d31cb9f --- /dev/null +++ b/QuestPDF/Infrastructure/IDynamicComponent.cs @@ -0,0 +1,9 @@ +using QuestPDF.Elements; + +namespace QuestPDF.Infrastructure +{ + public interface IDynamicComponent + { + void Compose(DynamicContext context, IDynamicContainer container); + } +} \ No newline at end of file From 152304b467de1115f305c207a0f29c25bc28b9b3 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Tue, 26 Apr 2022 01:11:40 +0200 Subject: [PATCH 07/16] Dynamic: improved TextStyle handling --- QuestPDF/Drawing/DocumentGenerator.cs | 2 +- QuestPDF/Elements/Dynamic.cs | 2 +- QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/QuestPDF/Drawing/DocumentGenerator.cs b/QuestPDF/Drawing/DocumentGenerator.cs index 6974470..cd9b339 100644 --- a/QuestPDF/Drawing/DocumentGenerator.cs +++ b/QuestPDF/Drawing/DocumentGenerator.cs @@ -187,7 +187,7 @@ namespace QuestPDF.Drawing } if (content is DynamicHost dynamicHost) - dynamicHost.TextStyle = documentDefaultTextStyle; + dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle); var targetTextStyle = documentDefaultTextStyle; diff --git a/QuestPDF/Elements/Dynamic.cs b/QuestPDF/Elements/Dynamic.cs index a62c8cb..c744430 100644 --- a/QuestPDF/Elements/Dynamic.cs +++ b/QuestPDF/Elements/Dynamic.cs @@ -9,7 +9,7 @@ namespace QuestPDF.Elements internal class DynamicHost : Element, IStateResettable { private IDynamicComponent Child { get; } - internal TextStyle TextStyle { get; set; } + internal TextStyle TextStyle { get; } = new(); public DynamicHost(IDynamicComponent child) { diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 39d493e..8e5af10 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -12,7 +12,7 @@ namespace QuestPDF.Elements.Text.Items private const char Space = ' '; 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 (); From e1101d870d85440a04fb4a9eb2ed26da7f25ce0b Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Wed, 27 Apr 2022 06:56:13 +0200 Subject: [PATCH 08/16] Dynamic: improve state management --- QuestPDF.Examples/DynamicExamples.cs | 51 +++++++++++-------- QuestPDF/Elements/Dynamic.cs | 38 +++++++------- QuestPDF/Fluent/DynamicComponentExtensions.cs | 10 ++-- QuestPDF/Infrastructure/IDynamicComponent.cs | 26 ++++++++-- 4 files changed, 74 insertions(+), 51 deletions(-) diff --git a/QuestPDF.Examples/DynamicExamples.cs b/QuestPDF.Examples/DynamicExamples.cs index 7353198..7e1a6f3 100644 --- a/QuestPDF.Examples/DynamicExamples.cs +++ b/QuestPDF.Examples/DynamicExamples.cs @@ -15,25 +15,29 @@ namespace QuestPDF.Examples 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 + public class OrdersTable : IDynamicComponent { private ICollection Items { get; } - private ICollection ItemsLeft { get; set; } - + public OrdersTableState State { get; set; } + public OrdersTable(ICollection items) { Items = items; + + State = new OrdersTableState + { + ShownItemsCount = 0 + }; } public void Compose(DynamicContext context, IDynamicContainer container) { - if (context.Operation == DynamicLayoutOperation.Reset) - { - ItemsLeft = new List(Items); - return; - } - var header = ComposeHeader(context); var sampleFooter = ComposeFooter(context, Enumerable.Empty()); var decorationHeight = header.Size.Height + sampleFooter.Size.Height; @@ -41,12 +45,9 @@ namespace QuestPDF.Examples var rows = GetItemsForPage(context, decorationHeight).ToList(); var footer = ComposeFooter(context, rows.Select(x => x.Item)); - if (ItemsLeft.Count > rows.Count) + if (State.ShownItemsCount + rows.Count < Items.Count) container.HasMoreContent(); - if (context.Operation == DynamicLayoutOperation.Draw) - ItemsLeft = ItemsLeft.Skip(rows.Count).ToList(); - container.MinimalBox().Decoration(decoration => { decoration.Header().Element(header); @@ -59,6 +60,11 @@ namespace QuestPDF.Examples decoration.Footer().Element(footer); }); + + State = new OrdersTableState + { + ShownItemsCount = State.ShownItemsCount + rows.Count + }; } private IDynamicElement ComposeHeader(DynamicContext context) @@ -98,10 +104,11 @@ namespace QuestPDF.Examples private IEnumerable<(OrderItem Item, IDynamicElement Element)> GetItemsForPage(DynamicContext context, float decorationHeight) { var totalHeight = decorationHeight; - var counter = Items.Count - ItemsLeft.Count + 1; - - foreach (var orderItem in ItemsLeft) + + foreach (var index in Enumerable.Range(State.ShownItemsCount, Items.Count - State.ShownItemsCount)) { + var item = Items.ElementAt(index); + var element = context.CreateElement(content => { content @@ -110,11 +117,11 @@ namespace QuestPDF.Examples .Padding(5) .Row(row => { - row.ConstantItem(30).Text(counter++); - row.RelativeItem().Text(orderItem.ItemName); - row.ConstantItem(50).AlignRight().Text(orderItem.Count); - row.ConstantItem(50).AlignRight().Text($"{orderItem.Price}$"); - row.ConstantItem(50).AlignRight().Text($"{orderItem.Count*orderItem.Price}$"); + 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}$"); }); }); @@ -124,7 +131,7 @@ namespace QuestPDF.Examples break; totalHeight += elementHeight; - yield return (orderItem, element); + yield return (item, element); } } } diff --git a/QuestPDF/Elements/Dynamic.cs b/QuestPDF/Elements/Dynamic.cs index c744430..dc526c1 100644 --- a/QuestPDF/Elements/Dynamic.cs +++ b/QuestPDF/Elements/Dynamic.cs @@ -8,22 +8,26 @@ namespace QuestPDF.Elements { internal class DynamicHost : Element, IStateResettable { - private IDynamicComponent Child { get; } + private DynamicComponentProxy Child { get; } + private object InitialComponentState { get; set; } + internal TextStyle TextStyle { get; } = new(); - public DynamicHost(IDynamicComponent child) + public DynamicHost(DynamicComponentProxy child) { Child = child; + + InitialComponentState = Child.GetState(); } public void ResetState() { - GetContent(Size.Zero, DynamicLayoutOperation.Reset); + Child.SetState(InitialComponentState); } internal override SpacePlan Measure(Size availableSpace) { - var content = GetContent(availableSpace, DynamicLayoutOperation.Measure); + var content = GetContent(availableSpace, acceptNewState: false); var measurement = content.Measure(availableSpace); if (measurement.Type != SpacePlanType.FullRender) @@ -36,11 +40,13 @@ namespace QuestPDF.Elements internal override void Draw(Size availableSpace) { - GetContent(availableSpace, DynamicLayoutOperation.Draw).Draw(availableSpace); + GetContent(availableSpace, acceptNewState: true).Draw(availableSpace); } - DynamicContainer GetContent(Size availableSize, DynamicLayoutOperation operation) + private DynamicContainer GetContent(Size availableSize, bool acceptNewState) { + var componentState = Child.GetState(); + var context = new DynamicContext { PageNumber = PageContext.CurrentPage, @@ -48,13 +54,15 @@ namespace QuestPDF.Elements Canvas = Canvas, TextStyle = TextStyle, - AvailableSize = availableSize, - Operation = operation + AvailableSize = availableSize }; var container = new DynamicContainer(); Child.Compose(context, container); - + + if (!acceptNewState) + Child.SetState(componentState); + container.VisitChildren(x => x?.Initialize(PageContext, Canvas)); container.VisitChildren(x => (x as IStateResettable)?.ResetState()); @@ -62,23 +70,15 @@ namespace QuestPDF.Elements } } - public enum DynamicLayoutOperation - { - Reset, - Measure, - Draw - } - 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 DynamicLayoutOperation Operation { get; internal set; } - + public IDynamicElement CreateElement(Action content) { var container = new DynamicElement(); diff --git a/QuestPDF/Fluent/DynamicComponentExtensions.cs b/QuestPDF/Fluent/DynamicComponentExtensions.cs index c5335ca..5fe013b 100644 --- a/QuestPDF/Fluent/DynamicComponentExtensions.cs +++ b/QuestPDF/Fluent/DynamicComponentExtensions.cs @@ -5,14 +5,10 @@ namespace QuestPDF.Fluent { public static class DynamicComponentExtensions { - public static void Dynamic(this IContainer element) where TDynamic : IDynamicComponent, new() + public static void Dynamic(this IContainer element, IDynamicComponent dynamicElement) where TState : struct { - element.Dynamic(new TDynamic()); - } - - public static void Dynamic(this IContainer element, IDynamicComponent dynamicElement) - { - element.Element(new DynamicHost(dynamicElement)); + var componentProxy = DynamicComponentProxy.CreateFrom(dynamicElement); + element.Element(new DynamicHost(componentProxy)); } } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/IDynamicComponent.cs b/QuestPDF/Infrastructure/IDynamicComponent.cs index d31cb9f..e401362 100644 --- a/QuestPDF/Infrastructure/IDynamicComponent.cs +++ b/QuestPDF/Infrastructure/IDynamicComponent.cs @@ -1,9 +1,29 @@ -using QuestPDF.Elements; +using System; +using QuestPDF.Elements; +using QuestPDF.Helpers; namespace QuestPDF.Infrastructure { - public interface IDynamicComponent + internal class DynamicComponentProxy { - void Compose(DynamicContext context, IDynamicContainer container); + internal Action SetState { get; private set; } + internal Func GetState { get; private set; } + internal Action Compose { get; private set; } + + internal static DynamicComponentProxy CreateFrom(IDynamicComponent component) where TState : struct + { + return new DynamicComponentProxy + { + GetState = () => component.State, + SetState = x => component.State = (TState)x, + Compose = component.Compose + }; + } + } + + public interface IDynamicComponent where TState : struct + { + TState State { get; set; } + void Compose(DynamicContext context, IDynamicContainer dynamicContainer); } } \ No newline at end of file From c8824021cc693cf5e80a292db970b344a0bbd3bd Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Wed, 27 Apr 2022 09:24:44 +0200 Subject: [PATCH 09/16] Dynamic: generalized dynamic element size measurement --- QuestPDF.Examples/DynamicExamples.cs | 3 +++ QuestPDF/Elements/Dynamic.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/QuestPDF.Examples/DynamicExamples.cs b/QuestPDF.Examples/DynamicExamples.cs index 7e1a6f3..42a38c4 100644 --- a/QuestPDF.Examples/DynamicExamples.cs +++ b/QuestPDF.Examples/DynamicExamples.cs @@ -72,6 +72,7 @@ namespace QuestPDF.Examples return context.CreateElement(element => { element + .Width(context.AvailableSize.Width) .BorderBottom(1) .BorderColor(Colors.Grey.Darken2) .Padding(5) @@ -95,6 +96,7 @@ namespace QuestPDF.Examples return context.CreateElement(element => { element + .Width(context.AvailableSize.Width) .Padding(5) .AlignRight() .Text($"Subtotal: {total}$", TextStyle.Default.Size(14).SemiBold()); @@ -112,6 +114,7 @@ namespace QuestPDF.Examples var element = context.CreateElement(content => { content + .Width(context.AvailableSize.Width) .BorderBottom(1) .BorderColor(Colors.Grey.Lighten2) .Padding(5) diff --git a/QuestPDF/Elements/Dynamic.cs b/QuestPDF/Elements/Dynamic.cs index dc526c1..746a786 100644 --- a/QuestPDF/Elements/Dynamic.cs +++ b/QuestPDF/Elements/Dynamic.cs @@ -88,7 +88,7 @@ namespace QuestPDF.Elements container.VisitChildren(x => x?.Initialize(PageContext, Canvas)); container.VisitChildren(x => (x as IStateResettable)?.ResetState()); - container.Size = container.Measure(AvailableSize); + container.Size = container.Measure(Size.Max); return container; } From a2629575e8ff5c303a0c6726580436cb387e60ce Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Wed, 27 Apr 2022 09:50:17 +0200 Subject: [PATCH 10/16] Dynamic: simplified container composition process --- QuestPDF.Examples/DynamicExamples.cs | 30 +++++++++------- QuestPDF/Elements/Dynamic.cs | 37 +++++--------------- QuestPDF/Infrastructure/IDynamicComponent.cs | 10 ++++-- 3 files changed, 34 insertions(+), 43 deletions(-) diff --git a/QuestPDF.Examples/DynamicExamples.cs b/QuestPDF.Examples/DynamicExamples.cs index 42a38c4..8e94e33 100644 --- a/QuestPDF.Examples/DynamicExamples.cs +++ b/QuestPDF.Examples/DynamicExamples.cs @@ -36,7 +36,7 @@ namespace QuestPDF.Examples }; } - public void Compose(DynamicContext context, IDynamicContainer container) + public DynamicComponentComposeResult Compose(DynamicContext context) { var header = ComposeHeader(context); var sampleFooter = ComposeFooter(context, Enumerable.Empty()); @@ -45,26 +45,32 @@ namespace QuestPDF.Examples var rows = GetItemsForPage(context, decorationHeight).ToList(); var footer = ComposeFooter(context, rows.Select(x => x.Item)); - if (State.ShownItemsCount + rows.Count < Items.Count) - container.HasMoreContent(); - - container.MinimalBox().Decoration(decoration => + var content = context.CreateElement(container => { - decoration.Header().Element(header); - - decoration.Content().Box().Stack(stack => + container.MinimalBox().Decoration(decoration => { - foreach (var row in rows) - stack.Item().Element(row.Element); - }); + decoration.Header().Element(header); - decoration.Footer().Element(footer); + 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) diff --git a/QuestPDF/Elements/Dynamic.cs b/QuestPDF/Elements/Dynamic.cs index 746a786..4c6d21f 100644 --- a/QuestPDF/Elements/Dynamic.cs +++ b/QuestPDF/Elements/Dynamic.cs @@ -27,23 +27,25 @@ namespace QuestPDF.Elements internal override SpacePlan Measure(Size availableSpace) { - var content = GetContent(availableSpace, acceptNewState: false); + 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 content.HasMoreContent + return result.HasMoreContent ? SpacePlan.PartialRender(measurement) : SpacePlan.FullRender(measurement); } internal override void Draw(Size availableSpace) { - GetContent(availableSpace, acceptNewState: true).Draw(availableSpace); + var content = GetContent(availableSpace, acceptNewState: true).Content as Element; + content?.Draw(availableSpace); } - private DynamicContainer GetContent(Size availableSize, bool acceptNewState) + private DynamicComponentComposeResult GetContent(Size availableSize, bool acceptNewState) { var componentState = Child.GetState(); @@ -57,16 +59,12 @@ namespace QuestPDF.Elements AvailableSize = availableSize }; - var container = new DynamicContainer(); - Child.Compose(context, container); + var result = Child.Compose(context); if (!acceptNewState) Child.SetState(componentState); - container.VisitChildren(x => x?.Initialize(PageContext, Canvas)); - container.VisitChildren(x => (x as IStateResettable)?.ResetState()); - - return container; + return result; } } @@ -94,25 +92,6 @@ namespace QuestPDF.Elements } } - public interface IDynamicContainer : IContainer - { - - } - - internal class DynamicContainer : Container, IDynamicContainer - { - internal bool HasMoreContent { get; set; } - } - - public static class DynamicContainerExtensions - { - public static IDynamicContainer HasMoreContent(this IDynamicContainer container) - { - (container as DynamicContainer).HasMoreContent = true; - return container; - } - } - public interface IDynamicElement : IElement { Size Size { get; } diff --git a/QuestPDF/Infrastructure/IDynamicComponent.cs b/QuestPDF/Infrastructure/IDynamicComponent.cs index e401362..c7fb26c 100644 --- a/QuestPDF/Infrastructure/IDynamicComponent.cs +++ b/QuestPDF/Infrastructure/IDynamicComponent.cs @@ -8,7 +8,7 @@ namespace QuestPDF.Infrastructure { internal Action SetState { get; private set; } internal Func GetState { get; private set; } - internal Action Compose { get; private set; } + internal Func Compose { get; private set; } internal static DynamicComponentProxy CreateFrom(IDynamicComponent component) where TState : struct { @@ -20,10 +20,16 @@ namespace QuestPDF.Infrastructure }; } } + + public class DynamicComponentComposeResult + { + public IElement Content { get; set; } + public bool HasMoreContent { get; set; } + } public interface IDynamicComponent where TState : struct { TState State { get; set; } - void Compose(DynamicContext context, IDynamicContainer dynamicContainer); + DynamicComponentComposeResult Compose(DynamicContext context); } } \ No newline at end of file From f3d4151273064bb7b86b90fce761689d25796745 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Thu, 5 May 2022 23:20:23 +0200 Subject: [PATCH 11/16] Improved performance of the table rendering algorithm (square to linear complexity) --- QuestPDF.Examples/TableBenchmark.cs | 46 +++++++++++++++++++++++++++++ QuestPDF/Elements/Table/Table.cs | 39 ++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 QuestPDF.Examples/TableBenchmark.cs diff --git a/QuestPDF.Examples/TableBenchmark.cs b/QuestPDF.Examples/TableBenchmark.cs new file mode 100644 index 0000000..2d61355 --- /dev/null +++ b/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(20_000) + .EnableCaching(true) + .EnableDebugging(false) + .Render(container => + { + container + .Padding(10) + .MinimalBox() + .Border(1) + .Table(table => + { + const int numberOfRows = 250_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}"); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Table/Table.cs b/QuestPDF/Elements/Table/Table.cs index f508576..4a33c25 100644 --- a/QuestPDF/Elements/Table/Table.cs +++ b/QuestPDF/Elements/Table/Table.cs @@ -19,15 +19,22 @@ namespace QuestPDF.Elements.Table private int RowsCount { 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) { StartingRowsCount = Cells.Select(x => x.Row).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(); + BuildCache(); base.Initialize(pageContext, canvas); } - + internal override IEnumerable GetChildren() { return Cells; @@ -38,6 +45,31 @@ namespace QuestPDF.Elements.Table Cells.ForEach(x => x.IsRendered = false); CurrentRow = 1; } + + private void BuildCache() + { + if (CellsCache != null) + return; + + if (Cells.Count == 0) + { + MaxRow = 0; + CellsCache = Array.Empty(); + + 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()) + .ToArray(); + } internal override SpacePlan Measure(Size availableSpace) { @@ -143,7 +175,10 @@ namespace QuestPDF.Elements.Table var rowBottomOffsets = new DynamicDictionary(); var commands = new List(); - 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 maxRenderingRow = RowsCount; From 4da3ef5dfa8dd6279ca994560ed8ca5bac080ea9 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Thu, 5 May 2022 23:52:17 +0200 Subject: [PATCH 12/16] Fixed breaking long words --- QuestPDF.Examples/TableBenchmark.cs | 4 ++-- QuestPDF.Examples/TextExamples.cs | 2 +- QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/QuestPDF.Examples/TableBenchmark.cs b/QuestPDF.Examples/TableBenchmark.cs index 2d61355..9cba9f3 100644 --- a/QuestPDF.Examples/TableBenchmark.cs +++ b/QuestPDF.Examples/TableBenchmark.cs @@ -16,7 +16,7 @@ namespace QuestPDF.Examples .ProducePdf() .PageSize(PageSizes.A4) .ShowResults() - .MaxPages(20_000) + .MaxPages(10_000) .EnableCaching(true) .EnableDebugging(false) .Render(container => @@ -27,7 +27,7 @@ namespace QuestPDF.Examples .Border(1) .Table(table => { - const int numberOfRows = 250_000; + const int numberOfRows = 100_000; const int numberOfColumns = 10; table.ColumnsDefinition(columns => diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index 1b26a24..52f1aa6 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -397,7 +397,7 @@ namespace QuestPDF.Examples text.DefaultTextStyle(x => x.BackgroundColor(Colors.Red.Lighten3).FontSize(24)); text.Span(" " + Placeholders.LoremIpsum()); - text.Span(" 0123456789012345678901234567890123456789012345678901234567890123456789 ").WrapAnywhere(); + text.Span(" 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 ").WrapAnywhere(); }); }); }); diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 8e5af10..543c29c 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -119,7 +119,7 @@ namespace QuestPDF.Elements.Text.Items // there is no available space to wrap text // if the item is first within the line, perform safe mode and chop the word // otherwise, move the item into the next line - return isFirstElementInLine ? (textLength, textLength + 1) : null; + return isFirstElementInLine ? (textLength, textLength) : null; } public virtual void Draw(TextDrawingRequest request) From 1af7c0a14477269bde388f84f57b8de2b68fbd5c Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Fri, 6 May 2022 20:55:18 +0200 Subject: [PATCH 13/16] Adjusted code related to subscript/superscript --- QuestPDF.Examples/TextExamples.cs | 84 +++++++++++++++---- QuestPDF/Drawing/FontManager.cs | 30 ++++--- QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 42 ++++++---- QuestPDF/Infrastructure/FontPosition.cs | 2 +- 4 files changed, 113 insertions(+), 45 deletions(-) diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index 0db0b11..d0d05ba 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -38,12 +38,11 @@ namespace QuestPDF.Examples } [Test] - public void SuperscriptSubscript() + public void SuperscriptSubscript_General() { RenderingTest .Create() - .PageSize(500, 300) - + .PageSize(500, 500) .ProduceImages() .ShowResults() .Render(container => @@ -55,28 +54,77 @@ namespace QuestPDF.Examples .Padding(10) .Text(text => { - text.Span("E=mc"); + 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"); - - text.EmptyLine(); - - text.ParagraphSpacing(2); - - var style = TextStyle.Default.Underline(); - - text.Span("Subscript").Style(style).Subscript().BackgroundColor(Colors.Green.Medium); - text.Span("Normal").Style(style).NormalPosition().BackgroundColor(Colors.Blue.Medium); - text.Line("Superscript").Style(style).Superscript().BackgroundColor(Colors.Red.Medium); - - text.Line("Superscript").Style(style).Superscript().BackgroundColor(Colors.Green.Medium); - text.Line("Normal").Style(style).NormalPosition().BackgroundColor(Colors.Blue.Medium); - text.Line("Subscript").Style(style).Subscript().BackgroundColor(Colors.Red.Medium); + }); + }); + } + + [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."); + }); }); }); } diff --git a/QuestPDF/Drawing/FontManager.cs b/QuestPDF/Drawing/FontManager.cs index 281028f..3c699a9 100644 --- a/QuestPDF/Drawing/FontManager.cs +++ b/QuestPDF/Drawing/FontManager.cs @@ -78,9 +78,14 @@ namespace QuestPDF.Drawing { var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal); - //Extra weight for superscript and subscript - if (style.FontPosition == FontPosition.Superscript || style.FontPosition == FontPosition.Subscript) - weight = (SKFontStyleWeight)((int)weight + 100); + // 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; @@ -100,19 +105,22 @@ namespace QuestPDF.Drawing $"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."); } + + 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) { return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics); } - - private static float GetTextScale(TextStyle style) - { - if (style.FontPosition == FontPosition.Superscript || style.FontPosition == FontPosition.Subscript) - return 0.65f; - - return 1; - } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 147efcc..26807d3 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -126,35 +126,47 @@ namespace QuestPDF.Elements.Text.Items { var fontMetrics = Style.ToFontMetrics(); - var glyphOffsetY = GetVerticalGlyphOffsetForStyle(Style); - + var glyphOffset = GetGlyphOffset(); 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, new Position(0, glyphOffsetY), Style); + request.Canvas.DrawText(text, new Position(0, glyphOffset), Style); // draw underline if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue) - DrawLine(glyphOffsetY + fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness ?? 1); + { + var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffset; + DrawLine(fontMetrics.UnderlinePosition.Value + underlineOffset, fontMetrics.UnderlineThickness ?? 1); + } // draw stroke if ((Style.HasStrikethrough ?? false) && fontMetrics.StrikeoutPosition.HasValue) - DrawLine(glyphOffsetY + 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) { - 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); } - } - private static float GetVerticalGlyphOffsetForStyle(TextStyle style) - { - if (style.FontPosition == FontPosition.Superscript) - return (style.Size ?? 12f) * -0.35f; - if (style.FontPosition == FontPosition.Subscript) - return (style.Size ?? 12f) * 0.1f; + float GetGlyphOffset() + { + var fontSize = Style.Size ?? 12f; - return 0; + var offsetFactor = Style.FontPosition switch + { + FontPosition.Normal => 0, + FontPosition.Subscript => 0.1f, + FontPosition.Superscript => -0.35f, + _ => throw new ArgumentOutOfRangeException() + }; + + return fontSize * offsetFactor; + } } } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/FontPosition.cs b/QuestPDF/Infrastructure/FontPosition.cs index d332740..8ac8131 100644 --- a/QuestPDF/Infrastructure/FontPosition.cs +++ b/QuestPDF/Infrastructure/FontPosition.cs @@ -2,7 +2,7 @@ { internal enum FontPosition { - Normal = 0, + Normal, Subscript, Superscript, } From 2beee1d22e15ad0b90cfe54b3db538dd31b0d172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Zi=C4=85bek?= Date: Fri, 6 May 2022 20:59:06 +0200 Subject: [PATCH 14/16] Update PreviewerService.cs --- QuestPDF/Previewer/PreviewerService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuestPDF/Previewer/PreviewerService.cs b/QuestPDF/Previewer/PreviewerService.cs index 1cc0f6b..69a83a6 100644 --- a/QuestPDF/Previewer/PreviewerService.cs +++ b/QuestPDF/Previewer/PreviewerService.cs @@ -111,7 +111,7 @@ namespace QuestPDF.Previewer while (true) { - await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken); + await Task.Delay(TimeSpan.FromMilliseconds(250)); if (cancellationToken.IsCancellationRequested) throw new Exception($"Cannot connect to the QuestPDF Previewer tool. Please make sure that your Operating System does not block HTTP connections on port {Port}."); From 25e6d9fbcaabcc22d2e0af3967b3d4e20a9d1415 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Fri, 6 May 2022 21:09:16 +0200 Subject: [PATCH 15/16] Adjusted library versions --- QuestPDF.Previewer/QuestPDF.Previewer.csproj | 2 +- QuestPDF/Previewer/PreviewerService.cs | 6 +++--- QuestPDF/QuestPDF.csproj | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/QuestPDF.Previewer/QuestPDF.Previewer.csproj b/QuestPDF.Previewer/QuestPDF.Previewer.csproj index eb8f0e9..5aeb9fa 100644 --- a/QuestPDF.Previewer/QuestPDF.Previewer.csproj +++ b/QuestPDF.Previewer/QuestPDF.Previewer.csproj @@ -4,7 +4,7 @@ MarcinZiabek CodeFlint QuestPDF.Previewer - 2022.4.1 + 2022.5.0 true questpdf-previewer 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. diff --git a/QuestPDF/Previewer/PreviewerService.cs b/QuestPDF/Previewer/PreviewerService.cs index 69a83a6..bce0826 100644 --- a/QuestPDF/Previewer/PreviewerService.cs +++ b/QuestPDF/Previewer/PreviewerService.cs @@ -24,7 +24,7 @@ namespace QuestPDF.Previewer HttpClient = new() { BaseAddress = new Uri($"http://localhost:{port}/"), - Timeout = TimeSpan.FromMilliseconds(250) + Timeout = TimeSpan.FromSeconds(1) }; } @@ -94,12 +94,12 @@ namespace QuestPDF.Previewer private void CheckVersionCompatibility(Version version) { - if (version.Major == 2022 && version.Minor == 4) + if (version.Major == 2022 && version.Minor == 5) return; throw new Exception($"Previewer version is not compatible. Possible solutions: " + $"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() diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index 61bd3b6..a4a3d93 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -4,7 +4,7 @@ MarcinZiabek CodeFlint QuestPDF - 2022.4.1 + 2022.5.0 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. $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt")) 9 From 0ebf2833af3f0f6394754120bdae7f6aa5f20bb2 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Fri, 6 May 2022 21:17:16 +0200 Subject: [PATCH 16/16] Updated release notes --- QuestPDF/Resources/ReleaseNotes.txt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/QuestPDF/Resources/ReleaseNotes.txt b/QuestPDF/Resources/ReleaseNotes.txt index a181c0b..8931772 100644 --- a/QuestPDF/Resources/ReleaseNotes.txt +++ b/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. \ No newline at end of file