Merge branch 'text-shaping-new'

This commit is contained in:
MarcinZiabek
2022-06-08 02:46:54 +02:00
28 changed files with 449 additions and 140 deletions
+1 -1
View File
@@ -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}");
+27 -12
View File
@@ -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());
});
});
}
@@ -388,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);
@@ -407,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();
+79
View File
@@ -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);
}
}
}
@@ -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);
+1 -1
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.5.0</Version>
<Version>2022.6.0</Version>
<PackAsTool>true</PackAsTool>
<ToolCommandName>questpdf-previewer</ToolCommandName>
<PackageDescription>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.</PackageDescription>
+2 -2
View File
@@ -1,4 +1,5 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
using SkiaSharp;
@@ -10,7 +11,6 @@ namespace QuestPDF.UnitTests.TestEngine
public Action<float> RotateFunc { get; set; }
public Action<float, float> ScaleFunc { get; set; }
public Action<SKImage, Position, Size> DrawImageFunc { get; set; }
public Action<string, Position, TextStyle> DrawTextFunc { get; set; }
public Action<Position, Size, string> 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();
@@ -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();
-18
View File
@@ -82,19 +82,6 @@ namespace QuestPDF.UnitTests.TestEngine
Assert.AreEqual(expected.Color, color, "Draw rectangle: color");
},
DrawTextFunc = (text, position, style) =>
{
var expected = GetExpected<CanvasDrawTextOperation>();
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<CanvasDrawImageOperation>();
@@ -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));
+4 -5
View File
@@ -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);
}
}
@@ -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)
{
}
@@ -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)
{
}
@@ -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;
}
}
}
@@ -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.";
}
}
}
+42 -4
View File
@@ -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,11 @@ namespace QuestPDF.Drawing
{
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
private static ConcurrentDictionary<object, SKPaint> Paints = new();
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new();
private static ConcurrentDictionary<object, SKPaint> FontPaints = new();
private static ConcurrentDictionary<string, SKPaint> ColorPaints = new();
private static ConcurrentDictionary<object, Font> ShaperFonts = new();
private static ConcurrentDictionary<object, SKFont> Fonts = new();
private static ConcurrentDictionary<object, TextShaper> TextShapers = new();
private static void RegisterFontType(SKData fontData, string? customName = null)
{
@@ -47,7 +52,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 +66,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 +127,38 @@ 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));
}
internal static SKFont FoFont(this TextStyle style)
{
return Fonts.GetOrAdd(style.PaintKey, _ => style.ToPaint().ToFont());
}
}
}
+1 -1
View File
@@ -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)
{
}
+17 -3
View File
@@ -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
+3 -2
View File
@@ -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)
+155
View File
@@ -0,0 +1,155 @@
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(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
{
public ShapedGlyph[] Glyphs { get; }
public TextShapingResult(ShapedGlyph[] glyphs)
{
Glyphs = glyphs;
}
public int BreakText(int startIndex, float maxWidth)
{
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;
}
public float MeasureWidth(int startIndex, int endIndex)
{
if (Glyphs.Length == 0)
return 0;
var start = Glyphs[startIndex];
var end = Glyphs[endIndex];
return end.Position.X - start.Position.X + end.Width;
}
public DrawTextCommand? PositionText(int startIndex, int endIndex, TextStyle textStyle)
{
if (Glyphs.Length == 0)
return null;
using var skTextBlobBuilder = new SKTextBlobBuilder();
var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(textStyle.FoFont(), 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
};
}
}
}
+16 -2
View File
@@ -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);
}
}
}
}
@@ -1,4 +1,5 @@
using QuestPDF.Infrastructure;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Calculation
{
@@ -1,4 +1,5 @@
using System;
using QuestPDF.Drawing;
namespace QuestPDF.Elements.Text.Calculation
{
@@ -8,6 +8,7 @@ namespace QuestPDF.Elements.Text.Items
{
public const string PageNumberPlaceholder = "123";
public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
protected override bool EnableTextCache => false;
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
+57 -41
View File
@@ -1,20 +1,24 @@
using System;
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;
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 +29,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 +47,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 +64,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);
if (textLength <= 0)
if (endIndex < 0)
return null;
// break text only on spaces
var wrappedTextLength = WrapText(text, textLength, request.IsFirstElementInLine);
var wrappedText = WrapText(startIndex, 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, wrappedText.Value.endIndex);
return new TextMeasurementResult
{
@@ -86,56 +88,70 @@ 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<char> text, int textLength, 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 (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].Codepoint != spaceCodepoint && TextShapingResult.Glyphs[endIndex + 1].Codepoint == spaceCodepoint)
return (endIndex, endIndex + 2);
// find last space within the available text to wrap
var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(Space);
var lastSpaceIndex = endIndex;
while (lastSpaceIndex >= startIndex)
{
if (TextShapingResult.Glyphs[lastSpaceIndex].Codepoint == spaceCodepoint)
break;
lastSpaceIndex--;
}
// 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 ? (textLength, textLength) : null;
return isFirstElementInLine ? (endIndex, endIndex + 1) : 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();
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);
var textDrawingCommand = TextShapingResult.PositionText(request.StartIndex, request.EndIndex, Style);
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);
// 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 +161,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)
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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);
+1 -1
View File
@@ -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: " +
+3 -4
View File
@@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.5.0</Version>
<Version>2022.6.0-prerelease</Version>
<PackageDescription>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.</PackageDescription>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
<LangVersion>9</LangVersion>
@@ -24,7 +23,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.80.*" />
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.3" />
</ItemGroup>
<ItemGroup>
@@ -45,5 +45,4 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>
+4 -4
View File
@@ -1,4 +1,4 @@
- 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.
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.