From 350abc2838460334d717f75311f10ccd3853a849 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Sun, 2 Oct 2022 19:50:50 +0200 Subject: [PATCH] Improved RTL, fixed line breaking algorithm --- QuestPDF.Examples/TextExamples.cs | 16 ++-- QuestPDF/Drawing/TextShaper.cs | 87 ++++++++++++++----- QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 12 +-- QuestPDF/QuestPDF.csproj | 2 +- 4 files changed, 80 insertions(+), 37 deletions(-) diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index 861079c..1566c3e 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -604,7 +604,7 @@ namespace QuestPDF.Examples { RenderingTest .Create() - .PageSize(500, 100) + .PageSize(250, 100) .ProduceImages() .ShowResults() @@ -614,9 +614,9 @@ namespace QuestPDF.Examples .Padding(25) .MinimalBox() .Background(Colors.Grey.Lighten2) - .Text("ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا") + .Text("خوارزمية ترتيب") .FontFamily(Fonts.Calibri) - .FontSize(20); + .FontSize(30); }); } @@ -663,14 +663,12 @@ namespace QuestPDF.Examples { RenderingTest .Create() - .PageSize(PageSizes.A4) + .PageSize(new PageSize(1000, 500)) .ProducePdf() .ShowResults() .Render(container => { - var text = "بسم الله الرحمن الرحيم" + - "\n" + - "الحمد لله رب العالمين (1) مالك يوم الدين"; + var text = "في المعلوماتية أو الرياضيات، خوارزمية الترتيب هي خوارزمية تمكن من تنظيم مجموعة عناصر حسب ترتيب محدد."; container .Padding(25) @@ -679,7 +677,7 @@ namespace QuestPDF.Examples { column.Spacing(20); - foreach (var size in new[] { 14, 12, 11, 10, 8, 7 }) + foreach (var size in new[] { 36, 34, 32, 30, 15 }) { column .Item() @@ -690,7 +688,7 @@ namespace QuestPDF.Examples .Background(Colors.Grey.Lighten2) .Text(text) .FontSize(20) - .FontFamily("Segoe UI").DirectionFromRightToLeft(); + .FontFamily("Segoe UI"); } }); }); diff --git a/QuestPDF/Drawing/TextShaper.cs b/QuestPDF/Drawing/TextShaper.cs index 46a1e91..a5e6f9e 100644 --- a/QuestPDF/Drawing/TextShaper.cs +++ b/QuestPDF/Drawing/TextShaper.cs @@ -62,7 +62,7 @@ namespace QuestPDF.Drawing yOffset += glyphPositions[i].YAdvance * scaleY; } - return new TextShapingResult(glyphs); + return new TextShapingResult(buffer.Direction, glyphs); } void PopulateBufferWithText(Buffer buffer, string text) @@ -98,29 +98,65 @@ namespace QuestPDF.Drawing internal class TextShapingResult { - public ShapedGlyph[] Glyphs { get; } + private Direction Direction { get; } + private ShapedGlyph[] Glyphs { get; } - public TextShapingResult(ShapedGlyph[] glyphs) + public int Length => Glyphs.Length; + + public ShapedGlyph this[int index] => + Direction == Direction.LeftToRight + ? Glyphs[index] + : Glyphs[Glyphs.Length - 1 - index]; + + public TextShapingResult(Direction direction, ShapedGlyph[] glyphs) { + Direction = direction; Glyphs = glyphs; } public int BreakText(int startIndex, float maxWidth) { - var index = startIndex; - maxWidth += Glyphs[startIndex].Position.X; - - while (index < Glyphs.Length) + return Direction switch { - var glyph = Glyphs[index]; - - if (glyph.Position.X + glyph.Width > maxWidth + Size.Epsilon) - break; - - index++; - } + Direction.LeftToRight => BreakTextLeftToRight(), + Direction.RightToLeft => BreakTextRightToLeft(), + _ => throw new ArgumentOutOfRangeException() + }; - return index - 1; + int BreakTextLeftToRight() + { + var index = startIndex; + maxWidth += Glyphs[startIndex].Position.X; + + while (index < Glyphs.Length) + { + var glyph = Glyphs[index]; + + if (glyph.Position.X + glyph.Width > maxWidth + Size.Epsilon) + break; + + index++; + } + + return index - 1; + } + + int BreakTextRightToLeft() + { + var index = startIndex; + + var startOffset = this[startIndex].Position.X + this[startIndex].Width; + + while (index < Glyphs.Length) + { + if (startOffset - this[index].Position.X > maxWidth + Size.Epsilon) + break; + + index++; + } + + return index - 1; + } } public float MeasureWidth(int startIndex, int endIndex) @@ -128,10 +164,15 @@ namespace QuestPDF.Drawing if (Glyphs.Length == 0) return 0; - var start = Glyphs[startIndex]; - var end = Glyphs[endIndex]; + var start = this[startIndex]; + var end = this[endIndex]; - return end.Position.X - start.Position.X + end.Width; + return Direction switch + { + Direction.LeftToRight => end.Position.X - start.Position.X + end.Width, + Direction.RightToLeft => start.Position.X - end.Position.X + start.Width, + _ => throw new NotSupportedException() + }; } public DrawTextCommand? PositionText(int startIndex, int endIndex, TextStyle textStyle) @@ -152,14 +193,18 @@ namespace QuestPDF.Drawing { var runIndex = sourceIndex - startIndex; - glyphSpan[runIndex] = Glyphs[sourceIndex].Codepoint; - positionSpan[runIndex] = Glyphs[sourceIndex].Position; + glyphSpan[runIndex] = this[sourceIndex].Codepoint; + positionSpan[runIndex] = this[sourceIndex].Position; } + + var firstVisualCharacterIndex = Direction == Direction.LeftToRight + ? startIndex + : endIndex; return new DrawTextCommand { SkTextBlob = skTextBlobBuilder.Build(), - TextOffsetX = -Glyphs[startIndex].Position.X + TextOffsetX = -this[firstVisualCharacterIndex].Position.X }; } } diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 4dfad0f..677fcc0 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -47,11 +47,11 @@ namespace QuestPDF.Elements.Text.Items // ignore leading spaces if (!request.IsFirstElementInBlock && request.IsFirstElementInLine) { - while (startIndex < TextShapingResult.Glyphs.Length && Text[startIndex] == spaceCodepoint) + while (startIndex < TextShapingResult.Length && Text[startIndex] == spaceCodepoint) startIndex++; } - if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length) + if (TextShapingResult.Length == 0 || startIndex == TextShapingResult.Length) { return new TextMeasurementResult { @@ -90,7 +90,7 @@ namespace QuestPDF.Elements.Text.Items StartIndex = startIndex, EndIndex = wrappedText.Value.endIndex, NextIndex = wrappedText.Value.nextIndex, - TotalIndex = TextShapingResult.Glyphs.Length - 1 + TotalIndex = TextShapingResult.Length - 1 }; } @@ -102,7 +102,7 @@ namespace QuestPDF.Elements.Text.Items // textLength - length of the part of the text that fits in available width (creating a line) // entire text fits, no need to wrap - if (endIndex == TextShapingResult.Glyphs.Length - 1) + if (endIndex == TextShapingResult.Length - 1) return (endIndex, endIndex); // breaking anywhere @@ -110,7 +110,7 @@ namespace QuestPDF.Elements.Text.Items return (endIndex, endIndex + 1); // current line ends at word, next character is space, perfect place to wrap - if (TextShapingResult.Glyphs[endIndex].Codepoint != spaceCodepoint && TextShapingResult.Glyphs[endIndex + 1].Codepoint == spaceCodepoint) + if (TextShapingResult[endIndex].Codepoint != spaceCodepoint && TextShapingResult[endIndex + 1].Codepoint == spaceCodepoint) return (endIndex, endIndex + 2); // find last space within the available text to wrap @@ -118,7 +118,7 @@ namespace QuestPDF.Elements.Text.Items while (lastSpaceIndex >= startIndex) { - if (TextShapingResult.Glyphs[lastSpaceIndex].Codepoint == spaceCodepoint) + if (TextShapingResult[lastSpaceIndex].Codepoint == spaceCodepoint) break; lastSpaceIndex--; diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index c581f5e..259f636 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -7,7 +7,7 @@ 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 - true + false Logo.png https://www.questpdf.com/images/package-logo.png https://www.questpdf.com/