From edc595d1992bbddfd3b29a7d64c5b1a92393b298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Zi=C4=85bek?= Date: Mon, 9 Aug 2021 22:35:39 +0200 Subject: [PATCH] Initial text rendering improvements --- QuestPDF.Examples/ElementExamples.cs | 50 +++++-- QuestPDF.Examples/Engine/RenderingTest.cs | 36 ++++- QuestPDF.Examples/TextExamples.cs | 33 +++++ QuestPDF/Drawing/SpacePlan/TextRender.cs | 15 +++ QuestPDF/Elements/Placeholder.cs | 7 +- QuestPDF/Elements/TextBlock.cs | 155 ++++++++++++++++++++++ QuestPDF/Elements/TextItem.cs | 58 ++++++++ QuestPDF/Fluent/ElementExtensions.cs | 18 --- QuestPDF/Fluent/TextExtensions.cs | 57 ++++++++ QuestPDF/Fluent/TextStyleExtensions.cs | 17 ++- QuestPDF/Infrastructure/TextStyle.cs | 5 +- 11 files changed, 410 insertions(+), 41 deletions(-) create mode 100644 QuestPDF.Examples/TextExamples.cs create mode 100644 QuestPDF/Drawing/SpacePlan/TextRender.cs create mode 100644 QuestPDF/Elements/TextBlock.cs create mode 100644 QuestPDF/Elements/TextItem.cs create mode 100644 QuestPDF/Fluent/TextExtensions.cs diff --git a/QuestPDF.Examples/ElementExamples.cs b/QuestPDF.Examples/ElementExamples.cs index 44c046d..108ee0f 100644 --- a/QuestPDF.Examples/ElementExamples.cs +++ b/QuestPDF.Examples/ElementExamples.cs @@ -502,25 +502,49 @@ namespace QuestPDF.Examples { RenderingTest .Create() - .PageSize(400, 250) + .PageSize(300, 175) .FileName() .Render(container => { container - .Padding(25) - .Stack(stack => + .Background(Colors.White) + .Padding(10) + .Decoration(decoration => { - var scales = new[] { 0.75f, 1f, 1.25f, 1.5f }; + var headerFontStyle = TextStyle + .Default + .Size(20) + .Color(Colors.Blue.Darken2) + .SemiBold(); + + decoration + .Header() + .PaddingBottom(10) + .Text("Example: scale component", headerFontStyle); + + decoration + .Content() + .Stack(stack => + { + var scales = new[] { 0.8f, 0.9f, 1.1f, 1.2f }; - foreach (var scale in scales) - { - stack - .Item() - .Border(1) - .Scale(scale) - .Padding(10) - .Text($"Content with {scale} scale.", TextStyle.Default.Size(20)); - } + foreach (var scale in scales) + { + var fontColor = scale <= 1f + ? Colors.Red.Lighten4 + : Colors.Green.Lighten4; + + var fontStyle = TextStyle.Default.Size(16); + + stack + .Item() + .Border(1) + .Background(fontColor) + .Scale(scale) + .Padding(5) + .Text($"Content with {scale} scale.", fontStyle); + } + }); }); }); } diff --git a/QuestPDF.Examples/Engine/RenderingTest.cs b/QuestPDF.Examples/Engine/RenderingTest.cs index 98b3254..04e1b3e 100644 --- a/QuestPDF.Examples/Engine/RenderingTest.cs +++ b/QuestPDF.Examples/Engine/RenderingTest.cs @@ -9,10 +9,17 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Examples.Engine { + public enum RenderingTestResult + { + Pdf, + Images + } + public class RenderingTest { private string FileNamePrefix = "test"; private Size Size { get; set; } + private RenderingTestResult ResultType { get; set; } = RenderingTestResult.Images; private RenderingTest() { @@ -35,18 +42,39 @@ namespace QuestPDF.Examples.Engine Size = new Size(width, height); return this; } + + public RenderingTest ProducePdf() + { + ResultType = RenderingTestResult.Pdf; + return this; + } + + public RenderingTest ProduceImages() + { + ResultType = RenderingTestResult.Images; + return this; + } public void Render(Action content) { var container = new Container(); content(container); - Func fileNameSchema = i => $"{FileNamePrefix}-${i}.png"; - var document = new SimpleDocument(container, Size); - document.GenerateImages(fileNameSchema); - Process.Start("explorer", fileNameSchema(0)); + if (ResultType == RenderingTestResult.Images) + { + Func fileNameSchema = i => $"{FileNamePrefix}-${i}.png"; + document.GenerateImages(fileNameSchema); + Process.Start("explorer", fileNameSchema(0)); + } + + if (ResultType == RenderingTestResult.Pdf) + { + var fileName = $"{FileNamePrefix}.pdf"; + document.GeneratePdf(fileName); + Process.Start("explorer", fileName); + } } } } \ No newline at end of file diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs new file mode 100644 index 0000000..e6943cc --- /dev/null +++ b/QuestPDF.Examples/TextExamples.cs @@ -0,0 +1,33 @@ +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class TextExamples + { + [Test] + public void TextElements() + { + RenderingTest + .Create() + .PageSize(600, 400) + .FileName() + .ProducePdf() + .Render(container => + { + container.Padding(20).Text(text => + { + text.Span("Let's start with something bold...", TextStyle.Default.SemiBold().Size(18)); + text.Span("And BIG...", TextStyle.Default.Size(28).Color(Colors.DeepOrange.Darken2).BackgroundColor(Colors.Yellow.Lighten3).Underlined()); + text.Span(Placeholders.LoremIpsum(), TextStyle.Default.Size(16)); + //text.Element().ExternalLink("https://www.questpdf.com/").Width(200).Height(50).Text("Visit questpdf.com", TextStyle.Default.Underlined().Color(Colors.Blue.Darken2)); + text.Span(Placeholders.LoremIpsum(), TextStyle.Default.Size(16).Stroked()); + text.Span("And now it's time for some colors 12345 678 90293 03490 83290.", TextStyle.Default.Size(20).Color(Colors.Green.Medium)); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Drawing/SpacePlan/TextRender.cs b/QuestPDF/Drawing/SpacePlan/TextRender.cs new file mode 100644 index 0000000..0288b2e --- /dev/null +++ b/QuestPDF/Drawing/SpacePlan/TextRender.cs @@ -0,0 +1,15 @@ +using QuestPDF.Infrastructure; + +namespace QuestPDF.Drawing.SpacePlan +{ + internal class TextRender : FullRender + { + public float Ascent { get; set; } + public float Descent { get; set; } + + public TextRender(float width, float height) : base(width, height) + { + + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Placeholder.cs b/QuestPDF/Elements/Placeholder.cs index ff4a5d6..cf33087 100644 --- a/QuestPDF/Elements/Placeholder.cs +++ b/QuestPDF/Elements/Placeholder.cs @@ -18,16 +18,15 @@ namespace QuestPDF.Elements { container .Background(Colors.Grey.Lighten2) + .Padding(5) .AlignMiddle() .AlignCenter() - .Padding(5) - .MaxHeight(32) .Element(x => { if (string.IsNullOrWhiteSpace(Text)) - x.Image(ImageData, ImageScaling.FitArea); + x.MaxHeight(32).Image(ImageData, ImageScaling.FitArea); else - x.Text(Text, TextStyle.Default.Size(14).SemiBold()); + x.Text(Text, TextStyle.Default.Size(14)); }); } } diff --git a/QuestPDF/Elements/TextBlock.cs b/QuestPDF/Elements/TextBlock.cs new file mode 100644 index 0000000..999fd08 --- /dev/null +++ b/QuestPDF/Elements/TextBlock.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class TextBlock : Element, IStateResettable + { + public List Children { get; set; } = new List(); + public Queue ChildrenQueue { get; set; } = new Queue(); + + public void ResetState() + { + ChildrenQueue = new Queue(Children); + } + + internal override void HandleVisitor(Action visit) + { + Children.ForEach(x => x?.HandleVisitor(visit)); + base.HandleVisitor(visit); + } + + internal override ISpacePlan Measure(Size availableSpace) + { + return new FullRender(availableSpace); + + if (!ChildrenQueue.Any()) + return new FullRender(Size.Zero); + + if (Children.Count < 50) + return new FullRender(Size.Zero); + + var items = SelectItemsForCurrentLine(availableSpace); + + if (items == null) + return new Wrap(); + + var totalWidth = items.Sum(x => x.Measurement.Width); + var totalHeight = items.Max(x => x.Measurement.Height); + + return new PartialRender(totalWidth, totalHeight); + + return new FullRender(Size.Zero); + return CreateParent(availableSpace).Measure(availableSpace); + } + + internal override void Draw(Size availableSpace) + { + while (true) + { + if (!ChildrenQueue.Any()) + return; + + var items = SelectItemsForCurrentLine(availableSpace); + + if (items == null) + return; + + var totalWidth = items.Sum(x => x.Measurement.Width); + var totalHeight = items.Max(x => x.Measurement.Height); + + var spaceBetween = (availableSpace.Width - totalWidth) / (items.Count - 1); + + var offset = items + .Select(x => x.Measurement) + .Cast() + .Where(x => x != null) + .Select(x => x.Ascent) + .Select(Math.Abs) + .Max(); + + Canvas.Translate(new Position(0, offset)); + + foreach (var item in items) + { + item.Element.Draw(availableSpace); + Canvas.Translate(new Position(item.Measurement.Width + spaceBetween, 0)); + } + + Canvas.Translate(new Position(-availableSpace.Width - spaceBetween, totalHeight - offset)); + + items.ForEach(x => ChildrenQueue.Dequeue()); + } + } + + Container CreateParent(Size availableSpace) + { + var children = Children + .Select(x => new + { + Element = x, + Measurement = x.Measure(Size.Max) as Size + }) + .Select(x => new GridElement() + { + Child = x.Element, + Columns = (int)x.Measurement.Width + 1 + }) + .ToList(); + + var grid = new Grid() + { + Alignment = HorizontalAlignment.Left, + ColumnsCount = (int)availableSpace.Width, + Children = children + }; + + var container = new Container(); + grid.Compose(container); + container.HandleVisitor(x => x.Initialize(PageContext, Canvas)); + + return container; + } + + private List? SelectItemsForCurrentLine(Size availableSpace) + { + var totalWidth = 0f; + + var items = ChildrenQueue + .Select(x => new MeasuredElement + { + Element = x, + Measurement = x.Measure(Size.Max) as FullRender + }) + .TakeWhile(x => + { + if (x.Measurement == null) + return false; + + if (totalWidth + x.Measurement.Width > availableSpace.Width) + return false; + + totalWidth += x.Measurement.Width; + return true; + }) + .ToList(); + + if (items.Any(x => x.Measurement == null)) + return null; + + if (items.Max(x => x.Measurement.Height) > availableSpace.Height) + return null; + + return items; + } + + private class MeasuredElement + { + public Element Element { get; set; } + public FullRender? Measurement { get; set; } + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/TextItem.cs b/QuestPDF/Elements/TextItem.cs new file mode 100644 index 0000000..e75b45c --- /dev/null +++ b/QuestPDF/Elements/TextItem.cs @@ -0,0 +1,58 @@ +using System; +using QuestPDF.Drawing; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class TextItem : Element + { + public string Value { get; set; } + public TextStyle Style { get; set; } = new TextStyle(); + + internal override ISpacePlan Measure(Size availableSpace) + { + var paint = Style.ToPaint(); + var metrics = paint.FontMetrics; + + var width = paint.MeasureText(Value); + var height = Math.Abs(metrics.Descent) + Math.Abs(metrics.Ascent); + + if (availableSpace.Width < width || availableSpace.Height < height) + return new Wrap(); + + return new TextRender(width, height) + { + Descent = metrics.Descent, + Ascent = metrics.Ascent + }; + } + + internal override void Draw(Size availableSpace) + { + var paint = Style.ToPaint(); + var metrics = paint.FontMetrics; + + var size = Measure(availableSpace) as Size; + + if (size == null) + return; + + Canvas.DrawRectangle(new Position(0, metrics.Ascent), new Size(size.Width, size.Height), Style.BackgroundColor); + Canvas.DrawText(Value, Position.Zero, Style); + + // draw underline + if (Style.IsUnderlined && metrics.UnderlinePosition.HasValue) + DrawLine(metrics.UnderlinePosition.Value, metrics.UnderlineThickness.Value); + + // draw stroke + if (Style.IsStroked && metrics.StrikeoutPosition.HasValue) + DrawLine(metrics.StrikeoutPosition.Value, metrics.StrikeoutThickness.Value); + + void DrawLine(float offset, float thickness) + { + Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(size.Width, thickness), Style.Color); + } + } + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/ElementExtensions.cs b/QuestPDF/Fluent/ElementExtensions.cs index 591afd4..7982e61 100644 --- a/QuestPDF/Fluent/ElementExtensions.cs +++ b/QuestPDF/Fluent/ElementExtensions.cs @@ -92,25 +92,7 @@ namespace QuestPDF.Fluent MinHeight = minHeight }); } - - public static void Text(this IContainer element, object text, TextStyle? style = null) - { - text ??= string.Empty; - style ??= TextStyle.Default; - if (element is Alignment alignment) - { - style = style.Clone(); - style.Alignment = alignment.Horizontal; - } - - element.Element(new Text - { - Value = text.ToString(), - Style = style - }); - } - public static void PageBreak(this IContainer element) { element.Element(new PageBreak()); diff --git a/QuestPDF/Fluent/TextExtensions.cs b/QuestPDF/Fluent/TextExtensions.cs new file mode 100644 index 0000000..5cf09f5 --- /dev/null +++ b/QuestPDF/Fluent/TextExtensions.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using QuestPDF.Elements; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Fluent +{ + public class TextDescriptor + { + internal ICollection Elements = new List(); + + internal TextDescriptor() + { + + } + + public void Span(string text, TextStyle? style = null) + { + text.Split(' ') + .Select(x => $"{x} ") + .Select(x => new TextItem + { + Value = x, + Style = style ?? TextStyle.Default + }) + .ToList() + .ForEach(Elements.Add); + } + + public IContainer Element() + { + var container = new Container(); + Elements.Add(container); + return container; + } + } + + public static class TextExtensions + { + public static void Text(this IContainer element, Action content) + { + var descriptor = new TextDescriptor(); + content?.Invoke(descriptor); + + element.Element(new TextBlock() + { + Children = descriptor.Elements.ToList() + }); + } + + public static void Text(this IContainer element, object text, TextStyle? style = null) + { + element.Text(x => x.Span(text.ToString(), style)); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/TextStyleExtensions.cs b/QuestPDF/Fluent/TextStyleExtensions.cs index 4a74abc..2644513 100644 --- a/QuestPDF/Fluent/TextStyleExtensions.cs +++ b/QuestPDF/Fluent/TextStyleExtensions.cs @@ -18,6 +18,11 @@ namespace QuestPDF.Fluent return style.Mutate(x => x.Color = value); } + public static TextStyle BackgroundColor(this TextStyle style, string value) + { + return style.Mutate(x => x.BackgroundColor = value); + } + public static TextStyle FontType(this TextStyle style, string value) { return style.Mutate(x => x.FontType = value); @@ -37,7 +42,17 @@ namespace QuestPDF.Fluent { return style.Mutate(x => x.IsItalic = value); } - + + public static TextStyle Stroked(this TextStyle style, bool value = true) + { + return style.Mutate(x => x.IsStroked = value); + } + + public static TextStyle Underlined(this TextStyle style, bool value = true) + { + return style.Mutate(x => x.IsUnderlined = value); + } + #region Alignmnet public static TextStyle Alignment(this TextStyle style, HorizontalAlignment value) diff --git a/QuestPDF/Infrastructure/TextStyle.cs b/QuestPDF/Infrastructure/TextStyle.cs index d12e82b..a6fe417 100644 --- a/QuestPDF/Infrastructure/TextStyle.cs +++ b/QuestPDF/Infrastructure/TextStyle.cs @@ -5,18 +5,21 @@ namespace QuestPDF.Infrastructure public class TextStyle { internal string Color { get; set; } = Colors.Black; + internal string BackgroundColor { get; set; } = Colors.Transparent; internal string FontType { get; set; } = "Calibri"; internal float Size { get; set; } = 12; internal float LineHeight { get; set; } = 1.2f; internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; internal FontWeight FontWeight { get; set; } = FontWeight.Normal; internal bool IsItalic { get; set; } = false; + internal bool IsStroked { get; set; } = false; + internal bool IsUnderlined { get; set; } = false; public static TextStyle Default => new TextStyle(); public override string ToString() { - return $"{Color}|{FontType}|{Size}|{LineHeight}|{Alignment}|{FontWeight}|{IsItalic}"; + return $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{Alignment}|{FontWeight}|{IsItalic}|{IsStroked}|{IsUnderlined}"; } internal TextStyle Clone() => (TextStyle)MemberwiseClone();