From 5402e5fedc6c005190facd69e09c45d3e267d67f Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Sat, 12 Mar 2022 12:17:00 +0100 Subject: [PATCH] Fixed text rendering issues --- QuestPDF.Examples/TextBenchmark.cs | 45 ++++++------- QuestPDF.Examples/TextExamples.cs | 59 +++++++++++++++-- .../Calculation/TextMeasurementRequest.cs | 2 +- .../Text/Calculation/TextMeasurementResult.cs | 2 +- QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 64 ++++++++++++------- QuestPDF/Elements/Text/TextBlock.cs | 2 +- 6 files changed, 117 insertions(+), 57 deletions(-) diff --git a/QuestPDF.Examples/TextBenchmark.cs b/QuestPDF.Examples/TextBenchmark.cs index 47645a3..607fab5 100644 --- a/QuestPDF.Examples/TextBenchmark.cs +++ b/QuestPDF.Examples/TextBenchmark.cs @@ -23,7 +23,7 @@ namespace QuestPDF.Examples .PageSize(PageSizes.A4) .ProducePdf() .ShowResults() - .Render(x => ComposeBook(x, chapters)); + .RenderDocument(x => ComposeBook(x, chapters)); } [Test] @@ -42,8 +42,8 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(PageSizes.A4) - .ProducePdf() - .Render(x => ComposeBook(x, chapters)); + .ProducePdf() + .RenderDocument(x => ComposeBook(x, chapters)); } IEnumerable PerformTest(int attempts) @@ -102,37 +102,30 @@ namespace QuestPDF.Examples } } - private void ComposeBook(IContainer container, ICollection chapters) + private void ComposeBook(IDocumentContainer container, ICollection chapters) { var subtitleStyle = TextStyle.Default.Size(24).SemiBold().Color(Colors.Blue.Medium); var normalStyle = TextStyle.Default.Size(14); - - ComposePage(container); - void ComposePage(IContainer container) + container.Page(page => { - container - .Padding(50) - .Decoration(decoration => - { - decoration - .Content() - .Column(column => - { - column.Item().Element(Title); - column.Item().PageBreak(); - column.Item().Element(TableOfContents); - column.Item().PageBreak(); + page.Margin(50); + + page.Content().Column(column => + { + column.Item().Element(Title); + column.Item().PageBreak(); + column.Item().Element(TableOfContents); + column.Item().PageBreak(); - Chapters(column); + Chapters(column); - column.Item().Element(Acknowledgements); - }); + column.Item().Element(Acknowledgements); + }); + + page.Footer().Element(Footer); + }); - decoration.After().Element(Footer); - }); - } - void Title(IContainer container) { container diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index a375392..027e320 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -31,7 +31,8 @@ namespace QuestPDF.Examples { text.DefaultTextStyle(TextStyle.Default.Size(20)); text.Span("This is a normal text, followed by an "); - text.Span("underlined red text.", TextStyle.Default.Size(20).Color(Colors.Red.Medium).Underline()); + text.Span("underlined red text", TextStyle.Default.Color(Colors.Red.Medium).Underline()); + text.Span("."); }); }); } @@ -189,7 +190,7 @@ namespace QuestPDF.Examples text.EmptyLine(); text.Span("The new text element also supports injecting custom content between words: "); - text.Element().PaddingBottom(-10).Height(16).Width(32).Image(Placeholders.Image); + text.Element().PaddingBottom(-4).Height(16).Width(32).Image(Placeholders.Image); text.Span("."); text.EmptyLine(); @@ -246,7 +247,7 @@ namespace QuestPDF.Examples text.Span("and this is slightly bigger text.", TextStyle.Default.Size(16)); text.Span("The new text element also supports injecting custom content between words: "); - text.Element().PaddingBottom(-10).Height(16).Width(32).Image(Placeholders.Image); + text.Element().PaddingBottom(-4).Height(16).Width(32).Image(Placeholders.Image); text.Span("."); text.EmptyLine(); @@ -288,7 +289,57 @@ namespace QuestPDF.Examples page.Content().Text( "This is a specially crafted sentence with a specially chosen length for demonstration of the bug that occurs ;;;;;. ", - TextStyle.Default.Size(11)); + TextStyle.Default.Size(11).BackgroundColor(Colors.Red.Lighten3)); + }); + }); + } + + [Test] + public void EmptyText() + { + // issue 135 + + RenderingTest + .Create() + .ProduceImages() + .ShowResults() + .RenderDocument(container => + { + container.Page(page => + { + page.Margin(50); + page.Background(Colors.White); + + page.Size(PageSizes.A4); + + page.Content().Text( + " ", + TextStyle.Default.Size(11).BackgroundColor(Colors.Red.Lighten3)); + }); + }); + } + + [Test] + public void Whitespaces() + { + // issue 135 + + RenderingTest + .Create() + .ProduceImages() + .ShowResults() + .RenderDocument(container => + { + container.Page(page => + { + page.Margin(50); + page.Background(Colors.White); + + page.Size(PageSizes.A4); + + page.Content().Text( + " x ", + TextStyle.Default.Size(11).BackgroundColor(Colors.Red.Lighten3)); }); }); } diff --git a/QuestPDF/Elements/Text/Calculation/TextMeasurementRequest.cs b/QuestPDF/Elements/Text/Calculation/TextMeasurementRequest.cs index 1b358d3..cd578ee 100644 --- a/QuestPDF/Elements/Text/Calculation/TextMeasurementRequest.cs +++ b/QuestPDF/Elements/Text/Calculation/TextMeasurementRequest.cs @@ -9,6 +9,6 @@ namespace QuestPDF.Elements.Text.Calculation public int StartIndex { get; set; } public float AvailableWidth { get; set; } - public bool IsFirstLineElement { get; set; } + public bool IsFirstElementInLine { get; set; } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs b/QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs index 6cded5b..c501131 100644 --- a/QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs +++ b/QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs @@ -17,6 +17,6 @@ namespace QuestPDF.Elements.Text.Calculation public int NextIndex { get; set; } public int TotalIndex { get; set; } - public bool IsLast => EndIndex == TotalIndex; + public bool IsLast => NextIndex == EndIndex; } } \ No newline at end of file diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 706a806..904d56a 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -33,13 +33,15 @@ namespace QuestPDF.Elements.Text.Items var startIndex = request.StartIndex; - if (request.IsFirstLineElement) + // if the element is the first one within the line, + // ignore leading spaces + if (request.IsFirstElementInLine) { - while (startIndex + 1 < Text.Length && Text[startIndex] == space) + while (startIndex < Text.Length && Text[startIndex] == space) startIndex++; } - - if (Text.Length == 0) + + if (Text.Length == 0 || startIndex == Text.Length) { return new TextMeasurementResult { @@ -58,37 +60,26 @@ namespace QuestPDF.Elements.Text.Items if (textLength <= 0) return null; - - if (textLength < text.Length && text[textLength] == space) - textLength++; - + // break text only on spaces - if (textLength < text.Length) - { - var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(space) - 1; + var wrappedTextLength = WrapText(text, textLength, request.IsFirstElementInLine); - if (lastSpaceIndex <= 0) - { - if (!request.IsFirstLineElement) - return null; - } - else - { - textLength = lastSpaceIndex + 1; - } - } + if (wrappedTextLength == null) + return null; + + textLength = wrappedTextLength.Value; text = text.Slice(0, textLength); var endIndex = startIndex + textLength; var nextIndex = endIndex; - while (nextIndex + 1 < Text.Length && Text[nextIndex] == space) + // when breaking text, omit spaces at the end of the line + while (nextIndex < Text.Length && Text[nextIndex] == space) nextIndex++; // measure final text - var finalText = text.TrimEnd(); - var width = paint.MeasureText(finalText); + var width = paint.MeasureText(text); return new TextMeasurementResult { @@ -104,6 +95,31 @@ namespace QuestPDF.Elements.Text.Items NextIndex = nextIndex, TotalIndex = Text.Length }; + + static int? WrapText(ReadOnlySpan text, int textLength, bool isFirstElementInLine) + { + // textLength - length of the part of the text that fits in available width (creating a line) + + // entire text fits, no need to wrap + if (textLength == text.Length) + return textLength; + + // current line ends at word, next character is space, perfect place to wrap + if (text[textLength - 1] != space && text[textLength] == space) + return textLength; + + // find last space within the available text to wrap + var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(space); + + // text contains space that can be used to wrap + if (lastSpaceIndex > 0) + return lastSpaceIndex; + + // 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 : null; + } } public virtual void Draw(TextDrawingRequest request) diff --git a/QuestPDF/Elements/Text/TextBlock.cs b/QuestPDF/Elements/Text/TextBlock.cs index 0b19928..4c0b184 100644 --- a/QuestPDF/Elements/Text/TextBlock.cs +++ b/QuestPDF/Elements/Text/TextBlock.cs @@ -170,7 +170,7 @@ namespace QuestPDF.Elements.Text StartIndex = currentItemIndex, AvailableWidth = availableWidth - currentWidth, - IsFirstLineElement = !currentLineElements.Any() + IsFirstElementInLine = !currentLineElements.Any() }; var measurementResponse = currentElement.Measure(measurementRequest);