From 6d122c784df7a3ec39940fde28ef164828dc028f Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Sun, 22 May 2022 20:25:11 +0200 Subject: [PATCH 1/8] Implemented working text shaping --- QuestPDF.Examples/TextBenchmark.cs | 2 +- QuestPDF.Examples/TextExamples.cs | 35 ++-- QuestPDF.Examples/TextShapingTests.cs | 79 +++++++++ QuestPDF.UnitTests/TestEngine/MockCanvas.cs | 4 +- .../TestEngine/OperationRecordingCanvas.cs | 3 +- QuestPDF.UnitTests/TestEngine/TestPlan.cs | 18 -- QuestPDF/Drawing/FontManager.cs | 40 ++++- QuestPDF/Drawing/FreeCanvas.cs | 2 +- QuestPDF/Drawing/SkiaCanvasBase.cs | 5 +- QuestPDF/Drawing/TextShaper.cs | 154 ++++++++++++++++++ .../Text/Calculation/TextDrawingRequest.cs | 3 +- .../Text/Calculation/TextMeasurementResult.cs | 1 + .../Text/Items/TextBlockPageNumber.cs | 1 + QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 88 +++++----- QuestPDF/Helpers/Placeholders.cs | 2 +- QuestPDF/Infrastructure/ICanvas.cs | 3 +- QuestPDF/QuestPDF.csproj | 3 +- 17 files changed, 363 insertions(+), 80 deletions(-) create mode 100644 QuestPDF.Examples/TextShapingTests.cs create mode 100644 QuestPDF/Drawing/TextShaper.cs diff --git a/QuestPDF.Examples/TextBenchmark.cs b/QuestPDF.Examples/TextBenchmark.cs index fb4c105..a5d1fb9 100644 --- a/QuestPDF.Examples/TextBenchmark.cs +++ b/QuestPDF.Examples/TextBenchmark.cs @@ -31,7 +31,7 @@ namespace QuestPDF.Examples { var chapters = GetBookChapters().ToList(); - var results = PerformTest(16).ToList(); + var results = PerformTest(128).ToList(); Console.WriteLine($"Min: {results.Min():F}"); Console.WriteLine($"Max: {results.Max():F}"); diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index 5f4ee30..10c8b71 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -12,11 +12,11 @@ namespace QuestPDF.Examples public class TextExamples { [Test] - public void SimpleTextBlock() + public void SimpleText() { RenderingTest .Create() - .PageSize(500, 300) + .PageSize(500, 100) .ProduceImages() .ShowResults() @@ -27,6 +27,27 @@ namespace QuestPDF.Examples .MinimalBox() .Border(1) .Padding(10) + .Text(Placeholders.Paragraph()); + }); + } + + [Test] + public void SimpleTextBlock() + { + RenderingTest + .Create() + .PageSize(600, 300) + + .ProduceImages() + .ShowResults() + .Render(container => + { + container + .Padding(5) + .MinimalBox() + .Border(1) + .MaxWidth(300) + .Padding(10) .Text(text => { text.DefaultTextStyle(TextStyle.Default.FontSize(20)); @@ -191,7 +212,7 @@ namespace QuestPDF.Examples { RenderingTest .Create() - .PageSize(500, 300) + .PageSize(500, 500) .ProduceImages() .ShowResults() .Render(container => @@ -203,13 +224,7 @@ namespace QuestPDF.Examples .Padding(10) .Text(text => { - text.ParagraphSpacing(10); - - foreach (var i in Enumerable.Range(1, 3)) - { - text.Span($"Paragraph {i}: ").SemiBold(); - text.Line(Placeholders.Paragraph()); - } + text.Line(Placeholders.Paragraph()); }); }); } diff --git a/QuestPDF.Examples/TextShapingTests.cs b/QuestPDF.Examples/TextShapingTests.cs new file mode 100644 index 0000000..90fa8a8 --- /dev/null +++ b/QuestPDF.Examples/TextShapingTests.cs @@ -0,0 +1,79 @@ +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using SkiaSharp; +using SkiaSharp.HarfBuzz; + +namespace QuestPDF.Examples +{ + public class TextShapingTests + { + // [Test] + // public void ShapeText() + // { + // using var textPaint = new SKPaint + // { + // Color = SKColors.Black, + // Typeface = SKTypeface.CreateDefault(), + // IsAntialias = true, + // TextSize = 20 + // }; + // + // using var backgroundPaint = new SKPaint + // { + // Color = SKColors.LightGray + // }; + // + // RenderingTest + // .Create() + // .PageSize(550, 250) + // .ProduceImages() + // .ShowResults() + // .Render(container => + // { + // //var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio ipsum, aliquam a neque a, lacinia vehicula lectus."; + // //var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا الواجب والعمل سنتنازل غالباً ونرفض الشعور"; + // + // var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + // var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا"; + // + // var text = arabic; + // var metrics = textPaint.FontMetrics; + // + // container + // .Padding(25) + // .Canvas((canvas, space) => + // { + // canvas.Translate(0, 20); + // + // var width = MeasureText(text, textPaint); + // var widthReal = textPaint.MeasureText(text); + // canvas.DrawRect(0, metrics.Descent, width, metrics.Ascent - metrics.Descent, backgroundPaint); + // + // canvas.DrawShapedText(text, 0, 0, textPaint); + // + // canvas.Translate(0, 40); + // canvas.DrawText(text, 0, 0, textPaint); + // }); + // }); + // } + + [Test] + public void MeasureTest() + { + using var textPaint = new SKPaint + { + Color = SKColors.Black, + Typeface = SKTypeface.CreateDefault(), + IsAntialias = true, + TextSize = 20 + }; + + var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio ipsum, aliquam a neque a, lacinia vehicula lectus."; + var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا"; + // 012345678901234567890123456789012345678901234567890123456 + var shaper = new SKShaper(textPaint.Typeface); + var result = shaper.Shape(lorem, textPaint); + } + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/MockCanvas.cs b/QuestPDF.UnitTests/TestEngine/MockCanvas.cs index 6e4926e..68bbc07 100644 --- a/QuestPDF.UnitTests/TestEngine/MockCanvas.cs +++ b/QuestPDF.UnitTests/TestEngine/MockCanvas.cs @@ -1,4 +1,5 @@ using System; +using QuestPDF.Drawing; using QuestPDF.Infrastructure; using SkiaSharp; @@ -10,7 +11,6 @@ namespace QuestPDF.UnitTests.TestEngine public Action RotateFunc { get; set; } public Action ScaleFunc { get; set; } public Action DrawImageFunc { get; set; } - public Action DrawTextFunc { get; set; } public Action DrawRectFunc { get; set; } public void Translate(Position vector) => TranslateFunc(vector); @@ -18,7 +18,7 @@ namespace QuestPDF.UnitTests.TestEngine public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY); public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color); - public void DrawText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style); + public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException(); public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size); public void DrawHyperlink(string url, Size size) => throw new NotImplementedException(); diff --git a/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs b/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs index 4fbedfe..5ca55aa 100644 --- a/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs +++ b/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using QuestPDF.Drawing; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine.Operations; using SkiaSharp; @@ -15,7 +16,7 @@ namespace QuestPDF.UnitTests.TestEngine public void Scale(float scaleX, float scaleY) => Operations.Add(new CanvasScaleOperation(scaleX, scaleY)); public void DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color)); - public void DrawText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style)); + public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException(); public void DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size)); public void DrawHyperlink(string url, Size size) => throw new NotImplementedException(); diff --git a/QuestPDF.UnitTests/TestEngine/TestPlan.cs b/QuestPDF.UnitTests/TestEngine/TestPlan.cs index 1ec5fe9..bd803e5 100644 --- a/QuestPDF.UnitTests/TestEngine/TestPlan.cs +++ b/QuestPDF.UnitTests/TestEngine/TestPlan.cs @@ -82,19 +82,6 @@ namespace QuestPDF.UnitTests.TestEngine Assert.AreEqual(expected.Color, color, "Draw rectangle: color"); }, - DrawTextFunc = (text, position, style) => - { - var expected = GetExpected(); - - Assert.AreEqual(expected.Text, text); - - Assert.AreEqual(expected.Position.X, position.X, "Draw text: X"); - Assert.AreEqual(expected.Position.Y, position.Y, "Draw text: Y"); - - Assert.AreEqual(expected.Style.Color, style.Color, "Draw text: color"); - Assert.AreEqual(expected.Style.FontFamily, style.FontFamily, "Draw text: font"); - Assert.AreEqual(expected.Style.Size, style.Size, "Draw text: size"); - }, DrawImageFunc = (image, position, size) => { var expected = GetExpected(); @@ -201,11 +188,6 @@ namespace QuestPDF.UnitTests.TestEngine return AddOperation(new CanvasDrawRectangleOperation(position, size, color)); } - public TestPlan ExpectCanvasDrawText(string text, Position position, TextStyle style) - { - return AddOperation(new CanvasDrawTextOperation(text, position, style)); - } - public TestPlan ExpectCanvasDrawImage(Position position, Size size) { return AddOperation(new CanvasDrawImageOperation(position, size)); diff --git a/QuestPDF/Drawing/FontManager.cs b/QuestPDF/Drawing/FontManager.cs index 3c699a9..2efd877 100644 --- a/QuestPDF/Drawing/FontManager.cs +++ b/QuestPDF/Drawing/FontManager.cs @@ -2,9 +2,11 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; +using HarfBuzzSharp; using QuestPDF.Fluent; using QuestPDF.Infrastructure; using SkiaSharp; +using SkiaSharp.HarfBuzz; namespace QuestPDF.Drawing { @@ -12,8 +14,10 @@ namespace QuestPDF.Drawing { private static ConcurrentDictionary StyleSets = new(); private static ConcurrentDictionary FontMetrics = new(); - private static ConcurrentDictionary Paints = new(); - private static ConcurrentDictionary ColorPaint = new(); + private static ConcurrentDictionary FontPaints = new(); + private static ConcurrentDictionary ColorPaints = new(); + private static ConcurrentDictionary ShaperFonts = new(); + private static ConcurrentDictionary TextShapers = new(); private static void RegisterFontType(SKData fontData, string? customName = null) { @@ -47,7 +51,7 @@ namespace QuestPDF.Drawing internal static SKPaint ColorToPaint(this string color) { - return ColorPaint.GetOrAdd(color, Convert); + return ColorPaints.GetOrAdd(color, Convert); static SKPaint Convert(string color) { @@ -61,7 +65,7 @@ namespace QuestPDF.Drawing internal static SKPaint ToPaint(this TextStyle style) { - return Paints.GetOrAdd(style.PaintKey, key => Convert(style)); + return FontPaints.GetOrAdd(style.PaintKey, key => Convert(style)); static SKPaint Convert(TextStyle style) { @@ -122,5 +126,33 @@ namespace QuestPDF.Drawing { return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics); } + + internal static Font ToShaperFont(this TextStyle style) + { + return ShaperFonts.GetOrAdd(style.PaintKey, _ => + { + var typeface = style.ToPaint().Typeface; + + using var harfBuzzBlob = typeface.OpenStream(out var ttcIndex).ToHarfBuzzBlob(); + + using var face = new Face(harfBuzzBlob, ttcIndex) + { + Index = ttcIndex, + UnitsPerEm = typeface.UnitsPerEm, + GlyphCount = typeface.GlyphCount + }; + + var font = new Font(face); + font.SetScale(TextShaper.FontShapingScale, TextShaper.FontShapingScale); + font.SetFunctionsOpenType(); + + return font; + }); + } + + internal static TextShaper ToTextShaper(this TextStyle style) + { + return TextShapers.GetOrAdd(style.PaintKey, _ => new TextShaper(style)); + } } } \ No newline at end of file diff --git a/QuestPDF/Drawing/FreeCanvas.cs b/QuestPDF/Drawing/FreeCanvas.cs index 251c9d1..bc2c68e 100644 --- a/QuestPDF/Drawing/FreeCanvas.cs +++ b/QuestPDF/Drawing/FreeCanvas.cs @@ -41,7 +41,7 @@ namespace QuestPDF.Drawing } - public void DrawText(string text, Position position, TextStyle style) + public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) { } diff --git a/QuestPDF/Drawing/SkiaCanvasBase.cs b/QuestPDF/Drawing/SkiaCanvasBase.cs index aaf8e25..9d1851a 100644 --- a/QuestPDF/Drawing/SkiaCanvasBase.cs +++ b/QuestPDF/Drawing/SkiaCanvasBase.cs @@ -1,5 +1,6 @@ using QuestPDF.Infrastructure; using SkiaSharp; +using SkiaSharp.HarfBuzz; namespace QuestPDF.Drawing { @@ -27,9 +28,9 @@ namespace QuestPDF.Drawing Canvas.DrawRect(vector.X, vector.Y, size.Width, size.Height, paint); } - public void DrawText(string text, Position vector, TextStyle style) + public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) { - Canvas.DrawText(text, vector.X, vector.Y, style.ToPaint()); + Canvas.DrawText(skTextBlob, position.X, position.Y, style.ToPaint()); } public void DrawImage(SKImage image, Position vector, Size size) diff --git a/QuestPDF/Drawing/TextShaper.cs b/QuestPDF/Drawing/TextShaper.cs new file mode 100644 index 0000000..3a76cb0 --- /dev/null +++ b/QuestPDF/Drawing/TextShaper.cs @@ -0,0 +1,154 @@ +using System; +using HarfBuzzSharp; +using QuestPDF.Infrastructure; +using SkiaSharp; +using Buffer = HarfBuzzSharp.Buffer; + +namespace QuestPDF.Drawing +{ + internal class TextShaper + { + public const int FontShapingScale = 512; + + private Font Font { get; } + private SKPaint Paint { get; } + + public TextShaper(TextStyle style) + { + Font = style.ToShaperFont(); + Paint = style.ToPaint(); + } + + public TextShapingResult Shape(string text) + { + var buffer = new Buffer(); + + PopulateBufferWithText(buffer, text); + buffer.GuessSegmentProperties(); + + Font.Shape(buffer); + + var length = buffer.Length; + var glyphInfos = buffer.GlyphInfos; + var glyphPositions = buffer.GlyphPositions; + + var scaleY = Paint.TextSize / FontShapingScale; + var scaleX = scaleY * Paint.TextScaleX; + + var xOffset = 0f; + var yOffset = 0f; + + var glyphs = new ShapedGlyph[length]; + + for (var i = 0; i < length; i++) + { + glyphs[i] = new ShapedGlyph + { + Codepoint = (ushort)glyphInfos[i].Codepoint, + Position = new SKPoint(xOffset + glyphPositions[i].XOffset * scaleX, yOffset - glyphPositions[i].YOffset * scaleY), + Width = glyphPositions[i].XAdvance * scaleX + }; + + xOffset += glyphPositions[i].XAdvance * scaleX; + yOffset += glyphPositions[i].YAdvance * scaleY; + } + + return new TextShapingResult(Paint, glyphs); + } + + void PopulateBufferWithText(Buffer buffer, string text) + { + var encoding = Paint.TextEncoding; + + if (encoding == SKTextEncoding.Utf8) + buffer.AddUtf8(text); + + else if (encoding == SKTextEncoding.Utf16) + buffer.AddUtf16(text); + + else if (encoding == SKTextEncoding.Utf32) + buffer.AddUtf32(text); + + else + throw new NotSupportedException("TextEncoding of type GlyphId is not supported."); + } + } + + internal struct ShapedGlyph + { + public ushort Codepoint; + public SKPoint Position; + public float Width; + } + + internal struct DrawTextCommand + { + public SKTextBlob SkTextBlob; + public float TextOffsetX; + } + + internal class TextShapingResult + { + private SKPaint Paint { get; } + public ShapedGlyph[] Glyphs { get; } + + public TextShapingResult(SKPaint paint, ShapedGlyph[] glyphs) + { + Paint = paint; + Glyphs = glyphs; + } + + public int BreakText(int startIndex, float maxWidth) + { + var index = startIndex; + var currentWidth = 0f; + + while (index < Glyphs.Length) + { + currentWidth += Glyphs[index].Width; + + if (currentWidth > maxWidth) + break; + + index++; + } + + return index - 1; + } + + public float MeasureWidth(int startGlyphIndex, int endIndex) + { + var start = Glyphs[startGlyphIndex]; + var end = Glyphs[endIndex]; + + return end.Position.X - start.Position.X + end.Width; + } + + public DrawTextCommand? PositionText(int startIndex, int endIndex) + { + if (Glyphs.Length == 0) + return null; + + using var skTextBlobBuilder = new SKTextBlobBuilder(); + + var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(Paint.ToFont(), endIndex - startIndex + 1); + var glyphSpan = positionedRunBuffer.GetGlyphSpan(); + var positionSpan = positionedRunBuffer.GetPositionSpan(); + + for (var sourceIndex = startIndex; sourceIndex <= endIndex; sourceIndex++) + { + var runIndex = sourceIndex - startIndex; + + glyphSpan[runIndex] = Glyphs[sourceIndex].Codepoint; + positionSpan[runIndex] = Glyphs[sourceIndex].Position; + } + + return new DrawTextCommand + { + SkTextBlob = skTextBlobBuilder.Build(), + TextOffsetX = -Glyphs[startIndex].Position.X + }; + } + } + +} \ No newline at end of file diff --git a/QuestPDF/Elements/Text/Calculation/TextDrawingRequest.cs b/QuestPDF/Elements/Text/Calculation/TextDrawingRequest.cs index ef16075..5b08b6f 100644 --- a/QuestPDF/Elements/Text/Calculation/TextDrawingRequest.cs +++ b/QuestPDF/Elements/Text/Calculation/TextDrawingRequest.cs @@ -1,4 +1,5 @@ -using QuestPDF.Infrastructure; +using QuestPDF.Drawing; +using QuestPDF.Infrastructure; namespace QuestPDF.Elements.Text.Calculation { diff --git a/QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs b/QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs index 6cded5b..4643763 100644 --- a/QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs +++ b/QuestPDF/Elements/Text/Calculation/TextMeasurementResult.cs @@ -1,4 +1,5 @@ using System; +using QuestPDF.Drawing; namespace QuestPDF.Elements.Text.Calculation { diff --git a/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs b/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs index a4ab822..1b12b3c 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs @@ -8,6 +8,7 @@ namespace QuestPDF.Elements.Text.Items { public const string PageNumberPlaceholder = "123"; public Func Source { get; set; } = _ => PageNumberPlaceholder; + protected override bool EnableTextCache => false; public override TextMeasurementResult? Measure(TextMeasurementRequest request) { diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 26807d3..d730b10 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -1,20 +1,23 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using QuestPDF.Drawing; using QuestPDF.Elements.Text.Calculation; using QuestPDF.Infrastructure; +using SkiaSharp; +using SkiaSharp.HarfBuzz; using Size = QuestPDF.Infrastructure.Size; namespace QuestPDF.Elements.Text.Items { internal class TextBlockSpan : ITextBlockItem { - private const char Space = ' '; - public string Text { get; set; } public TextStyle Style { get; set; } = new(); + public TextShapingResult? TextShapingResult { get; set; } private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new (); + protected virtual bool EnableTextCache => true; public virtual TextMeasurementResult? Measure(TextMeasurementRequest request) { @@ -25,11 +28,17 @@ namespace QuestPDF.Elements.Text.Items return MeasureCache[cacheKey]; } - + internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request) { + if (!EnableTextCache) + TextShapingResult = null; + + TextShapingResult ??= Style.ToTextShaper().Shape(Text); + var paint = Style.ToPaint(); var fontMetrics = Style.ToFontMetrics(); + var spaceCodepoint = paint.ToFont().Typeface.GetGlyphs(" ")[0]; var startIndex = request.StartIndex; @@ -37,11 +46,11 @@ namespace QuestPDF.Elements.Text.Items // ignore leading spaces if (!request.IsFirstElementInBlock && request.IsFirstElementInLine) { - while (startIndex < Text.Length && Text[startIndex] == Space) + while (startIndex < TextShapingResult.Glyphs.Length && Text[startIndex] == spaceCodepoint) startIndex++; } - - if (Text.Length == 0 || startIndex == Text.Length) + + if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length) { return new TextMeasurementResult { @@ -54,27 +63,19 @@ namespace QuestPDF.Elements.Text.Items } // start breaking text from requested position - var text = Text.AsSpan().Slice(startIndex); - - var textLength = (int)paint.BreakText(text, request.AvailableWidth + Size.Epsilon); + var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth + Size.Epsilon); - if (textLength <= 0) + if (endIndex < 0) return null; // break text only on spaces - var wrappedTextLength = WrapText(text, textLength, request.IsFirstElementInLine); + var wrappedText = WrapText(endIndex, request.IsFirstElementInLine); - if (wrappedTextLength == null) + if (wrappedText == null) return null; - - textLength = wrappedTextLength.Value.fragmentLength; - - text = text.Slice(0, textLength); - - var endIndex = startIndex + textLength; - + // measure final text - var width = paint.MeasureText(text); + var width = TextShapingResult.MeasureWidth(startIndex, endIndex); return new TextMeasurementResult { @@ -86,31 +87,41 @@ namespace QuestPDF.Elements.Text.Items LineHeight = Style.LineHeight ?? 1, StartIndex = startIndex, - EndIndex = endIndex, - NextIndex = startIndex + wrappedTextLength.Value.nextIndex, - TotalIndex = Text.Length + EndIndex = wrappedText.Value.endIndex, + NextIndex = wrappedText.Value.nextIndex, + TotalIndex = TextShapingResult.Glyphs.Length - 1 }; } // TODO: consider introduce text wrapping abstraction (basic, english-like, asian-like) - private (int fragmentLength, int nextIndex)? WrapText(ReadOnlySpan text, int textLength, bool isFirstElementInLine) + private (int endIndex, int nextIndex)? WrapText(int endIndex, bool isFirstElementInLine) { + var spaceCodepoint = Style.ToPaint().ToFont().Typeface.GetGlyphs(" ")[0]; + // 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, textLength + 1); + if (endIndex == TextShapingResult.Glyphs.Length - 1) + return (endIndex, endIndex); // breaking anywhere if (Style.WrapAnywhere ?? false) - return (textLength, textLength); + return (endIndex, endIndex + 1); // current line ends at word, next character is space, perfect place to wrap - if (text[textLength - 1] != Space && text[textLength] == Space) - return (textLength, textLength + 1); + if (TextShapingResult.Glyphs[endIndex - 1].Codepoint != spaceCodepoint && TextShapingResult.Glyphs[endIndex].Codepoint == spaceCodepoint) + return (endIndex, endIndex + 1); // find last space within the available text to wrap - var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(Space); + var lastSpaceIndex = endIndex; + + while (lastSpaceIndex > 0) + { + if (TextShapingResult.Glyphs[lastSpaceIndex].Codepoint == spaceCodepoint) + break; + + lastSpaceIndex--; + } // text contains space that can be used to wrap if (lastSpaceIndex > 0) @@ -119,23 +130,26 @@ 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) : null; + return isFirstElementInLine ? (endIndex, endIndex) : null; } public virtual void Draw(TextDrawingRequest request) { var fontMetrics = Style.ToFontMetrics(); - var glyphOffset = GetGlyphOffset(); - var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex); + var glyphOffsetY = GetGlyphOffset(); + + var textDrawingCommand = TextShapingResult.PositionText(request.StartIndex, request.EndIndex); 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, glyphOffset), Style); + + if (textDrawingCommand.HasValue) + request.Canvas.DrawText(textDrawingCommand.Value.SkTextBlob, new Position(textDrawingCommand.Value.TextOffsetX, glyphOffsetY), Style); // draw underline if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue) { - var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffset; + var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffsetY; DrawLine(fontMetrics.UnderlinePosition.Value + underlineOffset, fontMetrics.UnderlineThickness ?? 1); } @@ -145,7 +159,7 @@ namespace QuestPDF.Elements.Text.Items var strikeoutThickness = fontMetrics.StrikeoutThickness ?? 1; strikeoutThickness *= Style.FontPosition == FontPosition.Normal ? 1f : 0.625f; - DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffset, strikeoutThickness); + DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffsetY, strikeoutThickness); } void DrawLine(float offset, float thickness) diff --git a/QuestPDF/Helpers/Placeholders.cs b/QuestPDF/Helpers/Placeholders.cs index 5fa1e64..3383784 100644 --- a/QuestPDF/Helpers/Placeholders.cs +++ b/QuestPDF/Helpers/Placeholders.cs @@ -7,7 +7,7 @@ namespace QuestPDF.Helpers { public static class Placeholders { - public static readonly Random Random = new(); + public static readonly Random Random = new Random(0); #region Word Cache diff --git a/QuestPDF/Infrastructure/ICanvas.cs b/QuestPDF/Infrastructure/ICanvas.cs index 65b470b..8a65ef1 100644 --- a/QuestPDF/Infrastructure/ICanvas.cs +++ b/QuestPDF/Infrastructure/ICanvas.cs @@ -1,3 +1,4 @@ +using QuestPDF.Drawing; using SkiaSharp; namespace QuestPDF.Infrastructure @@ -7,7 +8,7 @@ namespace QuestPDF.Infrastructure void Translate(Position vector); void DrawRectangle(Position vector, Size size, string color); - void DrawText(string text, Position position, TextStyle style); + void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style); void DrawImage(SKImage image, Position position, Size size); void DrawHyperlink(string url, Size size); diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index a4a3d93..91a88ef 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -24,7 +24,8 @@ - + + From d97fa2cef1a176650723a6ee65715a7ac4411bd3 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Mon, 23 May 2022 01:18:21 +0200 Subject: [PATCH 2/8] Minor optimization of SkFont creation --- QuestPDF/Drawing/FontManager.cs | 6 ++++++ QuestPDF/Drawing/TextShaper.cs | 10 ++++------ QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/QuestPDF/Drawing/FontManager.cs b/QuestPDF/Drawing/FontManager.cs index 2efd877..fe37924 100644 --- a/QuestPDF/Drawing/FontManager.cs +++ b/QuestPDF/Drawing/FontManager.cs @@ -17,6 +17,7 @@ namespace QuestPDF.Drawing private static ConcurrentDictionary FontPaints = new(); private static ConcurrentDictionary ColorPaints = new(); private static ConcurrentDictionary ShaperFonts = new(); + private static ConcurrentDictionary Fonts = new(); private static ConcurrentDictionary TextShapers = new(); private static void RegisterFontType(SKData fontData, string? customName = null) @@ -154,5 +155,10 @@ namespace QuestPDF.Drawing { return TextShapers.GetOrAdd(style.PaintKey, _ => new TextShaper(style)); } + + internal static SKFont FoFont(this TextStyle style) + { + return Fonts.GetOrAdd(style.PaintKey, _ => style.ToPaint().ToFont()); + } } } \ No newline at end of file diff --git a/QuestPDF/Drawing/TextShaper.cs b/QuestPDF/Drawing/TextShaper.cs index 3a76cb0..c0d18c8 100644 --- a/QuestPDF/Drawing/TextShaper.cs +++ b/QuestPDF/Drawing/TextShaper.cs @@ -53,7 +53,7 @@ namespace QuestPDF.Drawing yOffset += glyphPositions[i].YAdvance * scaleY; } - return new TextShapingResult(Paint, glyphs); + return new TextShapingResult(glyphs); } void PopulateBufferWithText(Buffer buffer, string text) @@ -89,12 +89,10 @@ namespace QuestPDF.Drawing internal class TextShapingResult { - private SKPaint Paint { get; } public ShapedGlyph[] Glyphs { get; } - public TextShapingResult(SKPaint paint, ShapedGlyph[] glyphs) + public TextShapingResult(ShapedGlyph[] glyphs) { - Paint = paint; Glyphs = glyphs; } @@ -124,14 +122,14 @@ namespace QuestPDF.Drawing return end.Position.X - start.Position.X + end.Width; } - public DrawTextCommand? PositionText(int startIndex, int endIndex) + public DrawTextCommand? PositionText(int startIndex, int endIndex, TextStyle textStyle) { if (Glyphs.Length == 0) return null; using var skTextBlobBuilder = new SKTextBlobBuilder(); - var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(Paint.ToFont(), endIndex - startIndex + 1); + var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(textStyle.FoFont(), endIndex - startIndex + 1); var glyphSpan = positionedRunBuffer.GetGlyphSpan(); var positionSpan = positionedRunBuffer.GetPositionSpan(); diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index d730b10..3ea667f 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -139,7 +139,7 @@ namespace QuestPDF.Elements.Text.Items var glyphOffsetY = GetGlyphOffset(); - var textDrawingCommand = TextShapingResult.PositionText(request.StartIndex, request.EndIndex); + var textDrawingCommand = TextShapingResult.PositionText(request.StartIndex, request.EndIndex, Style); request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor); From a0b32e5ecf793dfd0922c5517665ed763be7acae Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Tue, 24 May 2022 23:50:53 +0200 Subject: [PATCH 3/8] Fixed Row.AutoItem width calculation --- QuestPDF.Previewer.Examples/Program.cs | 3 ++- .../Layouts/TableOfContentsTemplate.cs | 20 ++++++++++++++++--- QuestPDF/Elements/Row.cs | 18 +++++------------ 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/QuestPDF.Previewer.Examples/Program.cs b/QuestPDF.Previewer.Examples/Program.cs index 3e72075..ab17dbe 100644 --- a/QuestPDF.Previewer.Examples/Program.cs +++ b/QuestPDF.Previewer.Examples/Program.cs @@ -2,13 +2,14 @@ using QuestPDF.Helpers; using QuestPDF.Infrastructure; using QuestPDF.Previewer; +using QuestPDF.ReportSample; using QuestPDF.ReportSample.Layouts; //ImagePlaceholder.Solid = true; // var model = DataSource.GetReport(); // var report = new StandardReport(model); -// report.ShowInPreviewer().Wait(); +// report.ShowInPreviewer(); // // return; diff --git a/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs b/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs index 0eba5d0..dd59f7a 100644 --- a/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs +++ b/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs @@ -3,6 +3,7 @@ using System.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; +using SkiaSharp; namespace QuestPDF.ReportSample.Layouts { @@ -44,9 +45,22 @@ namespace QuestPDF.ReportSample.Layouts .SectionLink(locationName) .Row(row => { - row.ConstantItem(25).Text($"{number}."); - row.RelativeItem().Text(locationName); - row.ConstantItem(150).AlignRight().Text(text => + row.ConstantItem(20).Text($"{number}."); + row.AutoItem().Text(locationName); + + row.RelativeItem().PaddingHorizontal(2).AlignBottom().TranslateY(-3).Height(1).Canvas((canvas, space) => + { + // best to statically cache + using var paint = new SKPaint + { + StrokeWidth = space.Height, + PathEffect = SKPathEffect.CreateDash(new float[] { 1, 3 }, 0) + }; + + canvas.DrawLine(0, 0, space.Width, 0, paint); + }); + + row.AutoItem().Text(text => { text.BeginPageNumberOfSection(locationName); text.Span(" - "); diff --git a/QuestPDF/Elements/Row.cs b/QuestPDF/Elements/Row.cs index 713ccf2..cdf6a63 100644 --- a/QuestPDF/Elements/Row.cs +++ b/QuestPDF/Elements/Row.cs @@ -102,13 +102,14 @@ namespace QuestPDF.Elements private void UpdateItemsWidth(float availableWidth) { - HandleItemsWithAutoWidth(); + foreach (var rowItem in Items.Where(x => x.Type == RowItemType.Auto)) + rowItem.Size = rowItem.Measure(Size.Max).Width; - var constantWidth = Items.Where(x => x.Type == RowItemType.Constant).Sum(x => x.Size); + var constantWidth = Items.Where(x => x.Type != RowItemType.Relative).Sum(x => x.Size); var relativeWidth = Items.Where(x => x.Type == RowItemType.Relative).Sum(x => x.Size); var spacingWidth = (Items.Count - 1) * Spacing; - foreach (var item in Items.Where(x => x.Type == RowItemType.Constant)) + foreach (var item in Items.Where(x => x.Type != RowItemType.Relative)) item.Width = item.Size; if (relativeWidth <= 0) @@ -119,16 +120,7 @@ namespace QuestPDF.Elements foreach (var item in Items.Where(x => x.Type == RowItemType.Relative)) item.Width = item.Size * widthPerRelativeUnit; } - - private void HandleItemsWithAutoWidth() - { - foreach (var rowItem in Items.Where(x => x.Type == RowItemType.Auto)) - { - rowItem.Size = rowItem.Measure(Size.Max).Width; - rowItem.Type = RowItemType.Constant; - } - } - + private ICollection PlanLayout(Size availableSpace) { var leftOffset = 0f; From acdf1ed1aae42a7fbe2ccc9c482abec87afb2f11 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Wed, 25 May 2022 00:21:14 +0200 Subject: [PATCH 4/8] Stability improvement --- QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 3ea667f..1091bdd 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -101,7 +101,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 == 0 || endIndex == TextShapingResult.Glyphs.Length - 1) return (endIndex, endIndex); // breaking anywhere From 5fa32a06cc609b95d61137737a263a293e2b8e87 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Thu, 26 May 2022 00:42:50 +0200 Subject: [PATCH 5/8] Text: fixed corner case --- QuestPDF.Examples/TextExamples.cs | 4 +-- QuestPDF/Drawing/TextShaper.cs | 15 ++++++---- QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 30 ++++++++++--------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index 10c8b71..c562b10 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -403,7 +403,7 @@ namespace QuestPDF.Examples .Padding(10) .Text(text => { - text.DefaultTextStyle(TextStyle.Default.FontSize(20)); + text.DefaultTextStyle(TextStyle.Default.FontSize(20).BackgroundColor(Colors.Red.Lighten4)); text.AlignLeft(); text.ParagraphSpacing(10); @@ -422,7 +422,7 @@ namespace QuestPDF.Examples { text.Line($"{i}: {Placeholders.Paragraph()}"); - text.Hyperlink("Please visit QuestPDF website", "https://www.questpdf.com"); + text.Hyperlink("Please visit QuestPDF website. ", "https://www.questpdf.com"); text.Span("This is page number "); text.CurrentPageNumber(); diff --git a/QuestPDF/Drawing/TextShaper.cs b/QuestPDF/Drawing/TextShaper.cs index c0d18c8..8867f80 100644 --- a/QuestPDF/Drawing/TextShaper.cs +++ b/QuestPDF/Drawing/TextShaper.cs @@ -99,13 +99,13 @@ namespace QuestPDF.Drawing public int BreakText(int startIndex, float maxWidth) { var index = startIndex; - var currentWidth = 0f; + maxWidth += Glyphs[startIndex].Position.X; while (index < Glyphs.Length) { - currentWidth += Glyphs[index].Width; - - if (currentWidth > maxWidth) + var glyph = Glyphs[index]; + + if (glyph.Position.X + glyph.Width > maxWidth + Size.Epsilon) break; index++; @@ -114,9 +114,12 @@ namespace QuestPDF.Drawing return index - 1; } - public float MeasureWidth(int startGlyphIndex, int endIndex) + public float MeasureWidth(int startIndex, int endIndex) { - var start = Glyphs[startGlyphIndex]; + if (Glyphs.Length == 0) + return 0; + + var start = Glyphs[startIndex]; var end = Glyphs[endIndex]; return end.Position.X - start.Position.X + end.Width; diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 1091bdd..c9702b3 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using QuestPDF.Drawing; using QuestPDF.Elements.Text.Calculation; +using QuestPDF.Helpers; using QuestPDF.Infrastructure; using SkiaSharp; using SkiaSharp.HarfBuzz; @@ -63,19 +64,19 @@ namespace QuestPDF.Elements.Text.Items } // start breaking text from requested position - var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth + Size.Epsilon); + var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth); if (endIndex < 0) return null; // break text only on spaces - var wrappedText = WrapText(endIndex, request.IsFirstElementInLine); + var wrappedText = WrapText(startIndex, endIndex, request.IsFirstElementInLine); if (wrappedText == null) return null; // measure final text - var width = TextShapingResult.MeasureWidth(startIndex, endIndex); + var width = TextShapingResult.MeasureWidth(startIndex, wrappedText.Value.endIndex); return new TextMeasurementResult { @@ -93,15 +94,15 @@ namespace QuestPDF.Elements.Text.Items }; } - // TODO: consider introduce text wrapping abstraction (basic, english-like, asian-like) - private (int endIndex, int nextIndex)? WrapText(int endIndex, bool isFirstElementInLine) + // TODO: consider introducing text wrapping abstraction (basic, english-like, asian-like) + private (int endIndex, int nextIndex)? WrapText(int startIndex, int endIndex, bool isFirstElementInLine) { var spaceCodepoint = Style.ToPaint().ToFont().Typeface.GetGlyphs(" ")[0]; // 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 == 0 || endIndex == TextShapingResult.Glyphs.Length - 1) + if (endIndex == TextShapingResult.Glyphs.Length - 1) return (endIndex, endIndex); // breaking anywhere @@ -109,13 +110,13 @@ 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 - 1].Codepoint != spaceCodepoint && TextShapingResult.Glyphs[endIndex].Codepoint == spaceCodepoint) - return (endIndex, endIndex + 1); + if (TextShapingResult.Glyphs[endIndex].Codepoint != spaceCodepoint && TextShapingResult.Glyphs[endIndex + 1].Codepoint == spaceCodepoint) + return (endIndex, endIndex + 2); // find last space within the available text to wrap var lastSpaceIndex = endIndex; - while (lastSpaceIndex > 0) + while (lastSpaceIndex >= startIndex) { if (TextShapingResult.Glyphs[lastSpaceIndex].Codepoint == spaceCodepoint) break; @@ -124,13 +125,13 @@ namespace QuestPDF.Elements.Text.Items } // text contains space that can be used to wrap - if (lastSpaceIndex > 0) - return (lastSpaceIndex, lastSpaceIndex + 1); + if (lastSpaceIndex >= startIndex) + return (lastSpaceIndex - 1, lastSpaceIndex + 1); // 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 ? (endIndex, endIndex) : null; + return isFirstElementInLine ? (endIndex, endIndex + 1) : null; } public virtual void Draw(TextDrawingRequest request) @@ -140,8 +141,9 @@ namespace QuestPDF.Elements.Text.Items var glyphOffsetY = GetGlyphOffset(); var textDrawingCommand = TextShapingResult.PositionText(request.StartIndex, request.EndIndex, Style); - - request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor); + + if (Style.BackgroundColor != Colors.Transparent) + request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor); if (textDrawingCommand.HasValue) request.Canvas.DrawText(textDrawingCommand.Value.SkTextBlob, new Position(textDrawingCommand.Value.TextOffsetX, glyphOffsetY), Style); From 90d1ce7af79879eb417d5d040ce2defe50759ea1 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Tue, 31 May 2022 00:52:49 +0200 Subject: [PATCH 6/8] 2022.6.0-prerelease --- QuestPDF/QuestPDF.csproj | 4 +--- QuestPDF/Resources/ReleaseNotes.txt | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index 91a88ef..9f3c0cf 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -1,10 +1,9 @@  - MarcinZiabek CodeFlint QuestPDF - 2022.5.0 + 2022.6.0-prerelease 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 @@ -46,5 +45,4 @@ \ - diff --git a/QuestPDF/Resources/ReleaseNotes.txt b/QuestPDF/Resources/ReleaseNotes.txt index d295164..dbb73d9 100644 --- a/QuestPDF/Resources/ReleaseNotes.txt +++ b/QuestPDF/Resources/ReleaseNotes.txt @@ -1,4 +1,2 @@ -- Implemented the DynamicComponent element (useful when you want to generate dynamic and conditional content that is page aware, e.g. per-page totals), -- Extended text rendering capabilities by adding subscript and superscript effects, -- Improved table rendering performance, -- Previewer tool stability fixes. +Implemented support for the text shaping algorithm that fixes rendering more advanced languages such as Arabic. +Fixed: a rare case when the Row.AutoItem does not calculate properly the width of its content. From d894914e7e9900a01289ae048d757de09f1b6886 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Wed, 8 Jun 2022 00:09:59 +0200 Subject: [PATCH 7/8] Improved Previewer stability --- QuestPDF.Previewer/CommunicationService.cs | 2 ++ QuestPDF.Previewer/QuestPDF.Previewer.csproj | 2 +- QuestPDF/Previewer/PreviewerService.cs | 2 +- QuestPDF/Resources/ReleaseNotes.txt | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/QuestPDF.Previewer/CommunicationService.cs b/QuestPDF.Previewer/CommunicationService.cs index 0a78ef5..d988fa1 100644 --- a/QuestPDF.Previewer/CommunicationService.cs +++ b/QuestPDF.Previewer/CommunicationService.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -29,6 +30,7 @@ class CommunicationService { var builder = WebApplication.CreateBuilder(); builder.Services.AddLogging(x => x.ClearProviders()); + builder.WebHost.UseKestrel(options => options.Limits.MaxRequestBodySize = null); Application = builder.Build(); Application.MapGet("ping", HandlePing); diff --git a/QuestPDF.Previewer/QuestPDF.Previewer.csproj b/QuestPDF.Previewer/QuestPDF.Previewer.csproj index 5aeb9fa..9ada0f5 100644 --- a/QuestPDF.Previewer/QuestPDF.Previewer.csproj +++ b/QuestPDF.Previewer/QuestPDF.Previewer.csproj @@ -4,7 +4,7 @@ MarcinZiabek CodeFlint QuestPDF.Previewer - 2022.5.0 + 2022.6.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 bce0826..14a94c1 100644 --- a/QuestPDF/Previewer/PreviewerService.cs +++ b/QuestPDF/Previewer/PreviewerService.cs @@ -94,7 +94,7 @@ namespace QuestPDF.Previewer private void CheckVersionCompatibility(Version version) { - if (version.Major == 2022 && version.Minor == 5) + if (version.Major == 2022 && version.Minor == 6) return; throw new Exception($"Previewer version is not compatible. Possible solutions: " + diff --git a/QuestPDF/Resources/ReleaseNotes.txt b/QuestPDF/Resources/ReleaseNotes.txt index dbb73d9..aa0bf2b 100644 --- a/QuestPDF/Resources/ReleaseNotes.txt +++ b/QuestPDF/Resources/ReleaseNotes.txt @@ -1,2 +1,3 @@ Implemented support for the text shaping algorithm that fixes rendering more advanced languages such as Arabic. Fixed: a rare case when the Row.AutoItem does not calculate properly the width of its content. +Fixed: the QuestPDF Previewer does not work with content-rich documents. From d4103797aab70952b65e1fbf7effbf02374e3829 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Wed, 8 Jun 2022 02:01:58 +0200 Subject: [PATCH 8/8] Improved exception message when SkiaSharp throws the TypeInitializationException --- QuestPDF/Drawing/DocumentGenerator.cs | 9 ++++----- .../Exceptions/DocumentComposeException.cs | 12 +---------- .../Exceptions/DocumentDrawingException.cs | 12 +---------- .../Exceptions/DocumentLayoutException.cs | 16 +++------------ .../Exceptions/InitializationException.cs | 20 +++++++++++++++++++ QuestPDF/Drawing/PdfCanvas.cs | 20 ++++++++++++++++--- QuestPDF/Drawing/XpsCanvas.cs | 18 +++++++++++++++-- QuestPDF/Resources/ReleaseNotes.txt | 1 + 8 files changed, 63 insertions(+), 45 deletions(-) create mode 100644 QuestPDF/Drawing/Exceptions/InitializationException.cs diff --git a/QuestPDF/Drawing/DocumentGenerator.cs b/QuestPDF/Drawing/DocumentGenerator.cs index cd9b339..3fe373c 100644 --- a/QuestPDF/Drawing/DocumentGenerator.cs +++ b/QuestPDF/Drawing/DocumentGenerator.cs @@ -135,11 +135,10 @@ namespace QuestPDF.Drawing $"In this case, please increase the value {nameof(DocumentMetadata)}.{nameof(DocumentMetadata.DocumentLayoutExceptionThreshold)} property configured in the {nameof(IDocument.GetMetadata)} method. " + $"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." + $"Please analyze your documents structure to detect this element and fix its size constraints."; - - throw new DocumentLayoutException(message) - { - ElementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode." - }; + + var elementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode."; + + throw new DocumentLayoutException(message, elementTrace); } } diff --git a/QuestPDF/Drawing/Exceptions/DocumentComposeException.cs b/QuestPDF/Drawing/Exceptions/DocumentComposeException.cs index 972dbe8..eeefb4e 100644 --- a/QuestPDF/Drawing/Exceptions/DocumentComposeException.cs +++ b/QuestPDF/Drawing/Exceptions/DocumentComposeException.cs @@ -4,17 +4,7 @@ namespace QuestPDF.Drawing.Exceptions { public class DocumentComposeException : Exception { - public DocumentComposeException() - { - - } - - public DocumentComposeException(string message) : base(message) - { - - } - - public DocumentComposeException(string message, Exception inner) : base(message, inner) + internal DocumentComposeException(string message) : base(message) { } diff --git a/QuestPDF/Drawing/Exceptions/DocumentDrawingException.cs b/QuestPDF/Drawing/Exceptions/DocumentDrawingException.cs index cebb76c..a650d90 100644 --- a/QuestPDF/Drawing/Exceptions/DocumentDrawingException.cs +++ b/QuestPDF/Drawing/Exceptions/DocumentDrawingException.cs @@ -4,17 +4,7 @@ namespace QuestPDF.Drawing.Exceptions { public class DocumentDrawingException : Exception { - public DocumentDrawingException() - { - - } - - public DocumentDrawingException(string message) : base(message) - { - - } - - public DocumentDrawingException(string message, Exception inner) : base(message, inner) + internal DocumentDrawingException(string message, Exception inner) : base(message, inner) { } diff --git a/QuestPDF/Drawing/Exceptions/DocumentLayoutException.cs b/QuestPDF/Drawing/Exceptions/DocumentLayoutException.cs index ff83755..acdc1f0 100644 --- a/QuestPDF/Drawing/Exceptions/DocumentLayoutException.cs +++ b/QuestPDF/Drawing/Exceptions/DocumentLayoutException.cs @@ -4,21 +4,11 @@ namespace QuestPDF.Drawing.Exceptions { public class DocumentLayoutException : Exception { - public string ElementTrace { get; set; } - - public DocumentLayoutException() - { - - } + public string? ElementTrace { get; } - public DocumentLayoutException(string message) : base(message) + internal DocumentLayoutException(string message, string? elementTrace = null) : base(message) { - - } - - public DocumentLayoutException(string message, Exception inner) : base(message, inner) - { - + ElementTrace = elementTrace; } } } \ No newline at end of file diff --git a/QuestPDF/Drawing/Exceptions/InitializationException.cs b/QuestPDF/Drawing/Exceptions/InitializationException.cs new file mode 100644 index 0000000..5be5dd2 --- /dev/null +++ b/QuestPDF/Drawing/Exceptions/InitializationException.cs @@ -0,0 +1,20 @@ +using System; + +namespace QuestPDF.Drawing.Exceptions +{ + public class InitializationException : Exception + { + internal InitializationException(string documentType, Exception innerException) : base(CreateMessage(documentType), innerException) + { + + } + + private static string CreateMessage(string documentType) + { + return $"Cannot create the {documentType} document using the SkiaSharp library. " + + $"This exception usually means that, on your operating system where you run the application, SkiaSharp requires installing additional dependencies. " + + $"Such dependencies are available as additional nuget packages, for example SkiaSharp.NativeAssets.Linux. " + + $"Please refer to the SkiaSharp documentation for more details."; + } + } +} \ No newline at end of file diff --git a/QuestPDF/Drawing/PdfCanvas.cs b/QuestPDF/Drawing/PdfCanvas.cs index ac703eb..cdc6a2d 100644 --- a/QuestPDF/Drawing/PdfCanvas.cs +++ b/QuestPDF/Drawing/PdfCanvas.cs @@ -1,4 +1,6 @@ -using System.IO; +using System; +using System.IO; +using QuestPDF.Drawing.Exceptions; using QuestPDF.Helpers; using SkiaSharp; @@ -7,11 +9,23 @@ namespace QuestPDF.Drawing internal class PdfCanvas : SkiaDocumentCanvasBase { public PdfCanvas(Stream stream, DocumentMetadata documentMetadata) - : base(SKDocument.CreatePdf(stream, MapMetadata(documentMetadata))) + : base(CreatePdf(stream, documentMetadata)) { } - + + private static SKDocument CreatePdf(Stream stream, DocumentMetadata documentMetadata) + { + try + { + return SKDocument.CreatePdf(stream, MapMetadata(documentMetadata)); + } + catch (TypeInitializationException exception) + { + throw new InitializationException("PDF", exception); + } + } + private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata) { return new SKDocumentPdfMetadata diff --git a/QuestPDF/Drawing/XpsCanvas.cs b/QuestPDF/Drawing/XpsCanvas.cs index 2c3f65e..2d08f48 100644 --- a/QuestPDF/Drawing/XpsCanvas.cs +++ b/QuestPDF/Drawing/XpsCanvas.cs @@ -1,4 +1,6 @@ -using System.IO; +using System; +using System.IO; +using QuestPDF.Drawing.Exceptions; using QuestPDF.Helpers; using SkiaSharp; @@ -7,9 +9,21 @@ namespace QuestPDF.Drawing internal class XpsCanvas : SkiaDocumentCanvasBase { public XpsCanvas(Stream stream, DocumentMetadata documentMetadata) - : base(SKDocument.CreateXps(stream, documentMetadata.RasterDpi)) + : base(CreateXps(stream, documentMetadata)) { } + + private static SKDocument CreateXps(Stream stream, DocumentMetadata documentMetadata) + { + try + { + return SKDocument.CreateXps(stream, documentMetadata.RasterDpi); + } + catch (TypeInitializationException exception) + { + throw new InitializationException("XPS", exception); + } + } } } \ No newline at end of file diff --git a/QuestPDF/Resources/ReleaseNotes.txt b/QuestPDF/Resources/ReleaseNotes.txt index aa0bf2b..0679b9f 100644 --- a/QuestPDF/Resources/ReleaseNotes.txt +++ b/QuestPDF/Resources/ReleaseNotes.txt @@ -1,3 +1,4 @@ Implemented support for the text shaping algorithm that fixes rendering more advanced languages such as Arabic. +Improved exception message when SkiaSharp throws the TypeInitializationException (when additional dependencies are needed). Fixed: a rare case when the Row.AutoItem does not calculate properly the width of its content. Fixed: the QuestPDF Previewer does not work with content-rich documents.