diff --git a/QuestPDF.Examples/TextShapingTests.cs b/QuestPDF.Examples/TextShapingTests.cs new file mode 100644 index 0000000..df3e636 --- /dev/null +++ b/QuestPDF.Examples/TextShapingTests.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; +using NUnit.Framework; +using QuestPDF.Drawing; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +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); + }); + }); + } + + float MeasureText(string text, SKPaint paint) + { + var font = paint.ToFont(); + using var shaper = new SKShaper(paint.Typeface); + var result = shaper.Shape(text + " ", paint); + return Math.Max(result.Points.First().X, result.Points.Last().X); + + var glyphCount = result.Codepoints.Length; + var glyphIds = new ushort[glyphCount]; + var glyphWidths = new float[glyphCount]; + var glyphBounds = new SKRect[glyphCount]; + + font.GetGlyphs(result.Codepoints.Select(x => (int)x).ToArray(), glyphIds); + font.GetGlyphWidths(glyphIds, glyphWidths, glyphBounds); + + return glyphWidths.Sum(); + + return result.Points.Max(p => p.X); + + using var skTextBlobBuilder = new SKTextBlobBuilder(); + + var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(font, result.Codepoints.Length); + var glyphSpan = positionedRunBuffer.GetGlyphSpan(); + var positionSpan = positionedRunBuffer.GetPositionSpan(); + + for (int index = 0; index < result.Codepoints.Length; ++index) + { + glyphSpan[index] = (ushort) result.Codepoints[index]; + positionSpan[index] = result.Points[index]; + } + + using var text1 = skTextBlobBuilder.Build(); + return text1.Bounds.Width; + } + } +} \ No newline at end of file diff --git a/QuestPDF/Drawing/FontManager.cs b/QuestPDF/Drawing/FontManager.cs index 1a51bcf..f151909 100644 --- a/QuestPDF/Drawing/FontManager.cs +++ b/QuestPDF/Drawing/FontManager.cs @@ -12,6 +12,7 @@ namespace QuestPDF.Drawing private static ConcurrentDictionary StyleSets = new(); private static ConcurrentDictionary FontMetrics = new(); private static ConcurrentDictionary Paints = new(); + private static ConcurrentDictionary Fonts = new(); private static ConcurrentDictionary ColorPaint = new(); private static void RegisterFontType(SKData fontData, string? customName = null) @@ -94,6 +95,11 @@ namespace QuestPDF.Drawing } } + internal static SKFont ToFont(this TextStyle style) + { + return Fonts.GetOrAdd(style.FontMetricsKey, _ => style.ToPaint().Typeface.ToFont()); + } + internal static SKFontMetrics ToFontMetrics(this TextStyle style) { return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics); diff --git a/QuestPDF/Drawing/SkiaCanvasBase.cs b/QuestPDF/Drawing/SkiaCanvasBase.cs index aaf8e25..3d2d568 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 { @@ -29,7 +30,7 @@ namespace QuestPDF.Drawing public void DrawText(string text, Position vector, TextStyle style) { - Canvas.DrawText(text, vector.X, vector.Y, style.ToPaint()); + Canvas.DrawShapedText(text, vector.X, vector.Y, style.ToPaint()); } public void DrawImage(SKImage image, Position vector, Size size) diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index a8d0436..0011a16 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -11,8 +11,7 @@ namespace QuestPDF.Elements.Text.Items public string Text { get; set; } public TextStyle Style { get; set; } = new TextStyle(); - private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = - new Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?>(); + private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new(); public virtual TextMeasurementResult? Measure(TextMeasurementRequest request) { @@ -88,7 +87,7 @@ namespace QuestPDF.Elements.Text.Items // measure final text var width = paint.MeasureText(text); - + return new TextMeasurementResult { Width = width, @@ -104,7 +103,6 @@ namespace QuestPDF.Elements.Text.Items TotalIndex = Text.Length }; } - public virtual void Draw(TextDrawingRequest request) { var fontMetrics = Style.ToFontMetrics(); diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index 932b8b3..d2ca25a 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -25,6 +25,7 @@ +