Compare commits

...

10 Commits

Author SHA1 Message Date
MarcinZiabek 662a55e21c Implemented text shaping prototype 2022-03-01 23:22:14 +01:00
MarcinZiabek 7d825d123b Created text measuring algorithm for shaped text via HarfBuzz 2022-02-28 23:31:51 +01:00
MarcinZiabek 054e7a5f20 Added minimal API example 2022-02-28 15:41:10 +01:00
MarcinZiabek a92cc25fb1 Improved font matching and exception message. Refactored tests. 2022-02-27 15:19:50 +01:00
MarcinZiabek 8db283d559 Code refactorization + added support for multi typeface font types 2022-02-25 23:02:42 +01:00
Marcin Ziąbek 8a64abaac2 Merge pull request #106 from SvizelPritula/main
FontManager: Add support for multiple variants of font families registered at runtime
2022-02-21 22:26:51 +01:00
MarcinZiabek 68df390d0e Element renaming + custom text format 2022-02-21 00:57:53 +01:00
MarcinZiabek d73c694452 Added minimal API 2022-02-20 16:17:43 +01:00
MarcinZiabek 4479a49ad0 Improved support for page sections 2022-02-16 15:34:54 +01:00
Benjamin Swart 78eef45ce2 FontManager: Add support for multiple variants of font families registered at runtime 2022-01-24 15:13:41 +01:00
42 changed files with 1020 additions and 381 deletions
+3 -3
View File
@@ -13,11 +13,11 @@ namespace QuestPDF.Examples
[Test]
public void Example()
{
FontManager.RegisterFontType("LibreBarcode39", File.OpenRead("LibreBarcode39-Regular.ttf"));
FontManager.RegisterFontType(File.OpenRead("LibreBarcode39-Regular.ttf"));
RenderingTest
.Create()
.PageSize(400, 100)
.PageSize(400, 200)
.ShowResults()
.Render(container =>
{
@@ -25,7 +25,7 @@ namespace QuestPDF.Examples
.Background(Colors.White)
.AlignCenter()
.AlignMiddle()
.Text("*QuestPDF*", TextStyle.Default.FontType("LibreBarcode39").Size(64));
.Text("*QuestPDF*", TextStyle.Default.FontType("Libre Barcode 39").Size(64));
});
}
}
+44
View File
@@ -0,0 +1,44 @@
using System.Diagnostics;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using ShimSkiaSharp;
using SKTypeface = SkiaSharp.SKTypeface;
namespace QuestPDF.Examples
{
public class MinimalApiExamples
{
[Test]
public void MinimalApi()
{
Document
.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A5);
page.Margin(1.5f, Unit.Centimetre);
page.Header()
.Text("Hello PDF!", TextStyle.Default.SemiBold().Size(20));
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(10);
x.Item().Text(Placeholders.LoremIpsum());
x.Item().Image(Placeholders.Image(200, 100));
});
});
})
.GeneratePdf("hello.pdf");
Process.Start("explorer.exe", "hello.pdf");
}
}
}
+2 -2
View File
@@ -236,7 +236,7 @@ namespace QuestPDF.Examples
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default);
text.DefaultTextStyle(TextStyle.Default.BackgroundColor(Colors.Grey.Lighten3));
text.AlignLeft();
text.ParagraphSpacing(10);
@@ -251,7 +251,7 @@ namespace QuestPDF.Examples
text.EmptyLine();
foreach (var i in Enumerable.Range(1, 100))
foreach (var i in Enumerable.Range(1, 1000))
{
text.Line($"{i}: {Placeholders.Paragraph()}");
+161
View File
@@ -0,0 +1,161 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
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;
}
[Test]
public void ShapeSingle()
{
using var textPaint = new SKPaint
{
Color = SKColors.Black,
Typeface = SKTypeface.CreateDefault(),
IsAntialias = true,
TextSize = 20
};
var text = string.Join(" ", Enumerable.Range(0, 10_000).Select(x => Placeholders.Sentence()));
using var shaper = new SKShaper(textPaint.Typeface);
var result = shaper.Shape(text + " ", textPaint);
}
[Test]
public void ShapeMany()
{
using var textPaint = new SKPaint
{
Color = SKColors.Black,
Typeface = SKTypeface.CreateDefault(),
IsAntialias = true,
TextSize = 20
};
var text = string.Join(" ", Enumerable.Range(0, 1_000).Select(x => Placeholders.Sentence()));
foreach (var part in Regex.Split(text, "( )|(\\w+)"))
{
using var shaper = new SKShaper(textPaint.Typeface);
var result = shaper.Shape(part + " ", textPaint);
}
}
[Test]
public void ShapeMany2()
{
using var textPaint = new SKPaint
{
Color = SKColors.Black,
Typeface = SKTypeface.CreateDefault(),
IsAntialias = true,
TextSize = 20
};
using var shaper = new SKShaper(textPaint.Typeface);
foreach (var i in Enumerable.Range(0, 10_000))
{
var text = Placeholders.Paragraph();
textPaint.MeasureText(text);
var result = shaper.Shape(text + " ", textPaint);
}
}
}
}
+28
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using SkiaSharp;
@@ -24,5 +25,32 @@ namespace QuestPDF.ReportSample
Latitude = Helpers.Random.NextDouble() * 180f - 90f
};
}
private static readonly ConcurrentDictionary<int, string> RomanNumeralCache = new ConcurrentDictionary<int, string>();
public static string FormatAsRomanNumeral(this int number)
{
if (number < 0 || number > 3999)
throw new ArgumentOutOfRangeException("Number should be in range from 1 to 3999");
return RomanNumeralCache.GetOrAdd(number, x =>
{
if (x >= 1000) return "M" + FormatAsRomanNumeral(x - 1000);
if (x >= 900) return "CM" + FormatAsRomanNumeral(x - 900);
if (x >= 500) return "D" + FormatAsRomanNumeral(x - 500);
if (x >= 400) return "CD" + FormatAsRomanNumeral(x - 400);
if (x >= 100) return "C" + FormatAsRomanNumeral(x - 100);
if (x >= 90) return "XC" + FormatAsRomanNumeral(x - 90);
if (x >= 50) return "L" + FormatAsRomanNumeral(x - 50);
if (x >= 40) return "XL" + FormatAsRomanNumeral(x - 40);
if (x >= 10) return "X" + FormatAsRomanNumeral(x - 10);
if (x >= 9) return "IX" + FormatAsRomanNumeral(x - 9);
if (x >= 5) return "V" + FormatAsRomanNumeral(x - 5);
if (x >= 4) return "IV" + FormatAsRomanNumeral(x - 4);
if (x >= 1) return "I" + FormatAsRomanNumeral(x - 1);
return string.Empty;
});
}
}
}
@@ -39,9 +39,9 @@ namespace QuestPDF.ReportSample.Layouts
page.Footer().AlignCenter().Text(text =>
{
text.CurrentPageNumber();
text.CurrentPageNumber(format: x => x?.FormatAsRomanNumeral() ?? "-----");
text.Span(" / ");
text.TotalPages();
text.TotalPages(format: x => x?.FormatAsRomanNumeral() ?? "-----");
});
});
}
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.ReportSample.Layouts
@@ -43,7 +45,18 @@ namespace QuestPDF.ReportSample.Layouts
{
row.ConstantItem(25).Text($"{number}.");
row.RelativeItem().Text(locationName);
row.ConstantItem(150).AlignRight().Text(text => text.PageNumberOfLocation(locationName));
row.ConstantItem(150).AlignRight().Text(text =>
{
text.BeginPageNumberOfSection(locationName);
text.Span(" - ");
text.EndPageNumberOfSection(locationName);
var lengthStyle = TextStyle.Default.Color(Colors.Grey.Medium);
text.Span(" (", lengthStyle);
text.TotalPagesWithinSection(locationName, lengthStyle, x => x == 1 ? "1 page long" : $"{x} pages long");
text.Span(")", lengthStyle);
});
});
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
public class ExternalLinkTests
{
[Test]
public void Measure() => SimpleContainerTests.Measure<ExternalLink>();
public void Measure() => SimpleContainerTests.Measure<Hyperlink>();
// TODO: consider tests for the Draw method
}
+146
View File
@@ -0,0 +1,146 @@
using FluentAssertions;
using NUnit.Framework;
using QuestPDF.Drawing;
using SkiaSharp;
using static SkiaSharp.SKFontStyleSlant;
namespace QuestPDF.UnitTests
{
[TestFixture]
public class FontStyleSetTests
{
private void ExpectComparisonOrder(SKFontStyle target, SKFontStyle[] styles)
{
for (var i = 0; i < styles.Length - 1; i++)
{
var currentStyle = styles[i];
var nextStyle = styles[i + 1];
FontStyleSet.IsBetterMatch(target, currentStyle, nextStyle).Should().BeTrue();
FontStyleSet.IsBetterMatch(target, nextStyle, currentStyle).Should().BeFalse();
}
}
[Test]
public void FontStyleSet_IsBetterMatch_CondensedWidth()
{
var styles = new[]
{
new SKFontStyle(500, 5, Upright),
new SKFontStyle(500, 4, Upright),
new SKFontStyle(500, 3, Upright),
new SKFontStyle(500, 6, Upright)
};
ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_ExpandedWidth()
{
var styles = new[]
{
new SKFontStyle(500, 6, Upright),
new SKFontStyle(500, 7, Upright),
new SKFontStyle(500, 8, Upright),
new SKFontStyle(500, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(500, 6, Upright), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_ItalicSlant()
{
var styles = new[]
{
new SKFontStyle(500, 5, Italic),
new SKFontStyle(500, 5, Oblique),
new SKFontStyle(500, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(500, 5, Italic), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_ObliqueSlant()
{
var styles = new[]
{
new SKFontStyle(500, 5, Oblique),
new SKFontStyle(500, 5, Italic),
new SKFontStyle(500, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(500, 5, Oblique), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_UprightSlant()
{
var styles = new[]
{
new SKFontStyle(500, 5, Upright),
new SKFontStyle(500, 5, Oblique),
new SKFontStyle(500, 5, Italic)
};
ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_ThinWeight()
{
var styles = new[]
{
new SKFontStyle(300, 5, Upright),
new SKFontStyle(200, 5, Upright),
new SKFontStyle(100, 5, Upright),
new SKFontStyle(400, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(300, 5, Upright), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_RegularWeight()
{
var styles = new[]
{
new SKFontStyle(500, 5, Upright),
new SKFontStyle(300, 5, Upright),
new SKFontStyle(100, 5, Upright),
new SKFontStyle(600, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(400, 5, Upright), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_BoldWeight()
{
var styles = new[]
{
new SKFontStyle(600, 5, Upright),
new SKFontStyle(700, 5, Upright),
new SKFontStyle(800, 5, Upright),
new SKFontStyle(500, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(600, 5, Upright), styles);
}
[Test]
public void FontStyleSet_RespectsPriority()
{
var styles = new[]
{
new SKFontStyle(600, 5, Italic),
new SKFontStyle(600, 6, Upright),
new SKFontStyle(500, 6, Italic)
};
ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
public class InternalLinkTests
{
[Test]
public void Measure() => SimpleContainerTests.Measure<InternalLink>();
public void Measure() => SimpleContainerTests.Measure<SectionLink>();
// TODO: consider tests for the Draw method
}
+1 -1
View File
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
public class InternalLocationTests
{
[Test]
public void Measure() => SimpleContainerTests.Measure<InternalLink>();
public void Measure() => SimpleContainerTests.Measure<SectionLink>();
// TODO: consider tests for the Draw method
}
+5 -4
View File
@@ -19,10 +19,11 @@ namespace QuestPDF.UnitTests.TestEngine
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 DrawShapedText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style);
public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
public void DrawExternalLink(string url, Size size) => throw new NotImplementedException();
public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException();
public void DrawLocation(string locationName) => throw new NotImplementedException();
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
public void DrawSection(string sectionName) => throw new NotImplementedException();
}
}
@@ -16,10 +16,11 @@ namespace QuestPDF.UnitTests.TestEngine
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 DrawShapedText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style));
public void DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
public void DrawExternalLink(string url, Size size) => throw new NotImplementedException();
public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException();
public void DrawLocation(string locationName) => throw new NotImplementedException();
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
public void DrawSection(string sectionName) => throw new NotImplementedException();
}
}
+66 -17
View File
@@ -1,23 +1,52 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Drawing
{
public static class FontManager
{
private static ConcurrentDictionary<string, SKTypeface> Typefaces = new ConcurrentDictionary<string, SKTypeface>();
private static ConcurrentDictionary<string, SKFontMetrics> FontMetrics = new ConcurrentDictionary<string, SKFontMetrics>();
private static ConcurrentDictionary<string, SKPaint> Paints = new ConcurrentDictionary<string, SKPaint>();
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new ConcurrentDictionary<string, SKPaint>();
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
private static ConcurrentDictionary<object, SKPaint> Paints = new();
private static ConcurrentDictionary<object, SKFont> Fonts = new();
private static ConcurrentDictionary<object, SKShaper> Shapers = new();
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new();
private static void RegisterFontType(SKData fontData, string? customName = null)
{
foreach (var index in Enumerable.Range(0, 256))
{
var typeface = SKTypeface.FromData(fontData, index);
if (typeface == null)
break;
var typefaceName = customName ?? typeface.FamilyName;
var fontStyleSet = StyleSets.GetOrAdd(typefaceName, _ => new FontStyleSet());
fontStyleSet.Add(typeface);
}
}
[Obsolete("Since version 2022.3, the FontManager class offers better font type matching support. Please use the RegisterFontType(Stream stream) overload.")]
public static void RegisterFontType(string fontName, Stream stream)
{
Typefaces.TryAdd(fontName, SKTypeface.FromStream(stream));
using var fontData = SKData.Create(stream);
RegisterFontType(fontData);
RegisterFontType(fontData, customName: fontName);
}
public static void RegisterFontType(Stream stream)
{
using var fontData = SKData.Create(stream);
RegisterFontType(fontData);
}
internal static SKPaint ColorToPaint(this string color)
{
return ColorPaint.GetOrAdd(color, Convert);
@@ -30,37 +59,57 @@ namespace QuestPDF.Drawing
};
}
}
internal static SKPaint ToPaint(this TextStyle style)
{
return Paints.GetOrAdd(style.Key, key => Convert(style));
return Paints.GetOrAdd(style.PaintKey, key => Convert(style));
static SKPaint Convert(TextStyle style)
{
return new SKPaint
{
Color = SKColor.Parse(style.Color),
Typeface = GetTypeface(style),
TextSize = (style.Size ?? 12),
TextEncoding = SKTextEncoding.Utf32
TextSize = style.Size ?? 12
};
}
static SKTypeface GetTypeface(TextStyle style)
{
if (Typefaces.TryGetValue(style.FontType, out var result))
return result;
var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal);
var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant);
if (StyleSets.TryGetValue(style.FontType, out var fontStyleSet))
return fontStyleSet.Match(fontStyle);
var fontFromDefaultSource = SKFontManager.Default.MatchFamily(style.FontType, fontStyle);
return SKTypeface.FromFamilyName(style.FontType, (int)(style.FontWeight ?? FontWeight.Normal), (int)SKFontStyleWidth.Normal, slant)
?? throw new ArgumentException($"The typeface {style.FontType} could not be found.");
if (fontFromDefaultSource != null)
return fontFromDefaultSource;
throw new ArgumentException(
$"The typeface '{style.FontType}' could not be found. " +
$"Please consider the following options: " +
$"1) install the font on your operating system or execution environment. " +
$"2) load a font file specifically for QuestPDF usage via the QuestPDF.Drawing.FontManager.RegisterFontType(Stream fileContentStream) static method.");
}
}
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.Key, key => style.ToPaint().FontMetrics);
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics);
}
internal static SKShaper ToShaper(this TextStyle style)
{
return Shapers.GetOrAdd(style.FontMetricsKey, _ => new SKShaper(style.ToFont().Typeface));
}
}
}
+132
View File
@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using SkiaSharp;
namespace QuestPDF.Drawing
{
internal class FontStyleSet
{
private ConcurrentDictionary<SKFontStyle, SKTypeface> Styles { get; } = new();
public void Add(SKTypeface typeface)
{
var style = typeface.FontStyle;
Styles.AddOrUpdate(style, _ => typeface, (_, _) => typeface);
}
public SKTypeface? Match(SKFontStyle target)
{
SKFontStyle? bestStyle = null;
SKTypeface? bestTypeface = null;
foreach (var entry in Styles)
{
if (IsBetterMatch(target, entry.Key, bestStyle))
{
bestStyle = entry.Key;
bestTypeface = entry.Value;
}
}
return bestTypeface;
}
private static Dictionary<SKFontStyleSlant, List<SKFontStyleSlant>> SlantFallbacks = new()
{
{ SKFontStyleSlant.Italic, new() { SKFontStyleSlant.Italic, SKFontStyleSlant.Oblique, SKFontStyleSlant.Upright } },
{ SKFontStyleSlant.Oblique, new() { SKFontStyleSlant.Oblique, SKFontStyleSlant.Italic, SKFontStyleSlant.Upright } },
{ SKFontStyleSlant.Upright, new() { SKFontStyleSlant.Upright, SKFontStyleSlant.Oblique, SKFontStyleSlant.Italic } },
};
// Checks whether style a is a better match for the target then style b. Uses the CSS font style matching algorithm
internal static bool IsBetterMatch(SKFontStyle? target, SKFontStyle? a, SKFontStyle? b)
{
// A font is better than no font
if (b == null)
return true;
if (a == null)
return false;
// First check font width
// For normal and condensed widths prefer smaller widths
// For expanded widths prefer larger widths
if (target.Width <= (int)SKFontStyleWidth.Normal)
{
if (a.Width <= target.Width && b.Width > target.Width)
return true;
if (a.Width > target.Width && b.Width <= target.Width)
return false;
}
else
{
if (a.Width >= target.Width && b.Width < target.Width)
return true;
if (a.Width < target.Width && b.Width >= target.Width)
return false;
}
// Prefer closest match
var widthDifferenceA = Math.Abs(a.Width - target.Width);
var widthDifferenceB = Math.Abs(b.Width - target.Width);
if (widthDifferenceA < widthDifferenceB)
return true;
if (widthDifferenceB < widthDifferenceA)
return false;
// Prefer closest slant based on provided fallback list
var slantFallback = SlantFallbacks[target.Slant];
var slantIndexA = slantFallback.IndexOf(a.Slant);
var slantIndexB = slantFallback.IndexOf(b.Slant);
if (slantIndexA < slantIndexB)
return true;
if (slantIndexB < slantIndexA)
return false;
// Check weight last
// For thin (<400) weights, prefer thinner weights
// For regular (400-500) weights, prefer other regular weights, then use rule for thin or bold
// For bold (>500) weights, prefer thicker weights
// Behavior for values other than multiples of 100 is not given in the specification
if (target.Weight >= 400 && target.Weight <= 500)
{
if ((a.Weight >= 400 && a.Weight <= 500) && !(b.Weight >= 400 && b.Weight <= 500))
return true;
if (!(a.Weight >= 400 && a.Weight <= 500) && (b.Weight >= 400 && b.Weight <= 500))
return false;
}
if (target.Weight < 450)
{
if (a.Weight <= target.Weight && b.Weight > target.Weight)
return true;
if (a.Weight > target.Weight && b.Weight <= target.Weight)
return false;
}
else
{
if (a.Weight >= target.Weight && b.Weight < target.Weight)
return true;
if (a.Weight < target.Weight && b.Weight >= target.Weight)
return false;
}
// Prefer closest weight
var weightDifferenceA = Math.Abs(a.Weight - target.Weight);
var weightDifferenceB = Math.Abs(b.Weight - target.Weight);
return weightDifferenceA < weightDifferenceB;
}
}
}
+8 -3
View File
@@ -44,6 +44,11 @@ namespace QuestPDF.Drawing
public void DrawText(string text, Position position, TextStyle style)
{
}
public void DrawShapedText(string text, Position position, TextStyle style)
{
}
public void DrawImage(SKImage image, Position position, Size size)
@@ -51,17 +56,17 @@ namespace QuestPDF.Drawing
}
public void DrawExternalLink(string url, Size size)
public void DrawHyperlink(string url, Size size)
{
}
public void DrawLocationLink(string locationName, Size size)
public void DrawSectionLink(string sectionName, Size size)
{
}
public void DrawLocation(string locationName)
public void DrawSection(string sectionName)
{
}
+11 -5
View File
@@ -1,5 +1,6 @@
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Drawing
{
@@ -32,24 +33,29 @@ namespace QuestPDF.Drawing
Canvas.DrawText(text, vector.X, vector.Y, style.ToPaint());
}
public void DrawShapedText(string text, Position vector, TextStyle style)
{
Canvas.DrawShapedText(text, vector.X, vector.Y, style.ToPaint());
}
public void DrawImage(SKImage image, Position vector, Size size)
{
Canvas.DrawImage(image, new SKRect(vector.X, vector.Y, size.Width, size.Height));
}
public void DrawExternalLink(string url, Size size)
public void DrawHyperlink(string url, Size size)
{
Canvas.DrawUrlAnnotation(new SKRect(0, 0, size.Width, size.Height), url);
}
public void DrawLocationLink(string locationName, Size size)
public void DrawSectionLink(string sectionName, Size size)
{
Canvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), locationName);
Canvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), sectionName);
}
public void DrawLocation(string locationName)
public void DrawSection(string sectionName)
{
Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), locationName);
Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), sectionName);
}
public void Rotate(float angle)
@@ -3,7 +3,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class ExternalLink : ContainerElement
internal class Hyperlink : ContainerElement
{
public string Url { get; set; } = "https://www.questpdf.com";
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements
if (targetSize.Type == SpacePlanType.Wrap)
return;
Canvas.DrawExternalLink(Url, targetSize);
Canvas.DrawHyperlink(Url, targetSize);
base.Draw(availableSpace);
}
}
@@ -2,7 +2,7 @@
namespace QuestPDF.Elements
{
internal class InternalLocation : ContainerElement, IStateResettable
internal class Section : ContainerElement, IStateResettable
{
public string LocationName { get; set; }
private bool IsRendered { get; set; }
@@ -16,12 +16,11 @@ namespace QuestPDF.Elements
{
if (!IsRendered)
{
PageContext.SetLocationPage(LocationName);
Canvas.DrawLocation(LocationName);
Canvas.DrawSection(LocationName);
IsRendered = true;
}
PageContext.SetSectionPage(LocationName);
base.Draw(availableSpace);
}
}
@@ -3,9 +3,9 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class InternalLink : ContainerElement
internal class SectionLink : ContainerElement
{
public string LocationName { get; set; }
public string SectionName { get; set; }
internal override void Draw(Size availableSpace)
{
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements
if (targetSize.Type == SpacePlanType.Wrap)
return;
Canvas.DrawLocationLink(LocationName, targetSize);
Canvas.DrawSectionLink(SectionName, targetSize);
base.Draw(availableSpace);
}
}
@@ -2,7 +2,7 @@
namespace QuestPDF.Elements.Text.Calculation
{
internal class TextMeasurementResult
internal class TextBlockSize
{
public float Width { get; set; }
public float Height => Math.Abs(Descent) + Math.Abs(Ascent);
@@ -11,12 +11,5 @@ namespace QuestPDF.Elements.Text.Calculation
public float Descent { get; set; }
public float LineHeight { get; set; }
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public int NextIndex { get; set; }
public int TotalIndex { get; set; }
public bool IsLast => EndIndex == TotalIndex;
}
}
@@ -2,14 +2,8 @@
namespace QuestPDF.Elements.Text.Calculation
{
internal class TextDrawingRequest
internal struct TextDrawingRequest
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public float TotalAscent { get; set; }
public Size TextSize { get; set; }
}
@@ -5,6 +5,6 @@ namespace QuestPDF.Elements.Text.Calculation
internal class TextLineElement
{
public ITextBlockItem Item { get; set; }
public TextMeasurementResult Measurement { get; set; }
public TextBlockSize Measurement { get; set; }
}
}
@@ -1,14 +0,0 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Calculation
{
internal class TextMeasurementRequest
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public int StartIndex { get; set; }
public float AvailableWidth { get; set; }
public bool IsFirstLineElement { get; set; }
}
}
@@ -5,7 +5,10 @@ namespace QuestPDF.Elements.Text.Items
{
internal interface ITextBlockItem
{
TextMeasurementResult? Measure(TextMeasurementRequest request);
ICanvas Canvas { get; set; }
IPageContext PageContext { get; set; }
TextBlockSize? Measure();
void Draw(TextDrawingRequest request);
}
}
@@ -7,41 +7,40 @@ namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockElement : ITextBlockItem
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public Element Element { get; set; } = Empty.Instance;
public TextMeasurementResult? Measure(TextMeasurementRequest request)
public TextBlockSize? Measure()
{
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
Element.VisitChildren(x => x.Initialize(PageContext, Canvas));
var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height));
var measurement = Element.Measure(Size.Max);
if (measurement.Type != SpacePlanType.FullRender)
return null;
return new TextMeasurementResult
return new TextBlockSize
{
Width = measurement.Width,
Ascent = -measurement.Height,
Descent = 0,
LineHeight = 1,
StartIndex = 0,
EndIndex = 0,
TotalIndex = 0
LineHeight = 1
};
}
public void Draw(TextDrawingRequest request)
{
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
Element.VisitChildren(x => x.Initialize(PageContext, Canvas));
request.Canvas.Translate(new Position(0, request.TotalAscent));
Canvas.Translate(new Position(0, request.TotalAscent));
Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
Canvas.Translate(new Position(0, -request.TotalAscent));
}
}
}
@@ -1,24 +0,0 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockExternalLink : TextBlockSpan
{
public string Url { get; set; }
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return MeasureWithoutCache(request);
}
public override void Draw(TextDrawingRequest request)
{
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawExternalLink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
base.Draw(request);
}
}
}
@@ -0,0 +1,19 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockHyperlink : TextBlockSpan
{
public string Url { get; set; }
public override void Draw(TextDrawingRequest request)
{
Canvas.Translate(new Position(0, request.TotalAscent));
Canvas.DrawHyperlink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
Canvas.Translate(new Position(0, -request.TotalAscent));
base.Draw(request);
}
}
}
@@ -1,24 +0,0 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockInternalLink : TextBlockSpan
{
public string LocationName { get; set; }
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return MeasureWithoutCache(request);
}
public override void Draw(TextDrawingRequest request)
{
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawLocationLink(LocationName, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
base.Draw(request);
}
}
}
@@ -1,33 +1,29 @@
using QuestPDF.Elements.Text.Calculation;
using System;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockPageNumber : TextBlockSpan
{
public string SlotName { get; set; }
public const string PageNumberPlaceholder = "123";
public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
public override TextBlockSize? Measure()
{
SetPageNumber(request.PageContext);
return MeasureWithoutCache(request);
SetPageNumber();
return base.Measure();
}
public override void Draw(TextDrawingRequest request)
{
SetPageNumber(request.PageContext);
SetPageNumber();
base.Draw(request);
}
private void SetPageNumber(IPageContext context)
private void SetPageNumber()
{
var pageNumberPlaceholder = 123;
var pageNumber = context.GetRegisteredLocations().Contains(SlotName)
? context.GetLocationPage(SlotName)
: pageNumberPlaceholder;
Text = pageNumber.ToString();
Text = Source(PageContext) ?? PageNumberPlaceholder;
}
}
}
@@ -0,0 +1,19 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSectionLink : TextBlockSpan
{
public string SectionName { get; set; }
public override void Draw(TextDrawingRequest request)
{
Canvas.Translate(new Position(0, request.TotalAscent));
Canvas.DrawSectionLink(SectionName, new Size(request.TextSize.Width, request.TextSize.Height));
Canvas.Translate(new Position(0, -request.TotalAscent));
base.Draw(request);
}
}
}
+56 -86
View File
@@ -1,118 +1,88 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
using SkiaSharp.HarfBuzz;
using Size = QuestPDF.Infrastructure.Size;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSpan : ITextBlockItem
{
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?>();
public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
{
var cacheKey = (request.StartIndex, request.AvailableWidth);
if (!MeasureCache.ContainsKey(cacheKey))
MeasureCache[cacheKey] = MeasureWithoutCache(request);
return MeasureCache[cacheKey];
}
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
public string Text { get; set; }
public TextStyle Style { get; set; }
protected TextBlockSize? Size { get; set; }
protected bool RequiresShaping { get; set; }
public virtual TextBlockSize? Measure()
{
Size ??= Text == " " ? GetSizeForSpace() : GetSizeForWord();
return Size;
}
private TextBlockSize GetSizeForWord()
{
const char space = ' ';
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
var startIndex = request.StartIndex;
var shaper = Style.ToShaper();
if (request.IsFirstLineElement)
{
while (startIndex + 1 < Text.Length && Text[startIndex] == space)
startIndex++;
}
if (Text.Length == 0)
{
return new TextMeasurementResult
{
Width = 0,
LineHeight = Style.LineHeight ?? 1,
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent
};
}
// shaper returns positions of all glyphs,
// by adding a space, it is possible to capture width of the last original character
var result = shaper.Shape(Text + " ", paint);
// start breaking text from requested position
var text = Text.Substring(startIndex);
// when text is left-to-right: last value corresponds to text width
// when text is right-to-left: glyphs are in the reverse order, first value represents text width
var width = Math.Max(result.Points.First().X, result.Points.Last().X);
RequiresShaping = result.Points.Length != Text.Length + 1;
var textLength = (int)paint.BreakText(text, request.AvailableWidth);
if (textLength <= 0)
return null;
if (textLength < text.Length && text[textLength] == space)
textLength++;
// break text only on spaces
if (textLength < text.Length)
{
var lastSpaceIndex = text.Substring(0, textLength).LastIndexOf(space) - 1;
if (lastSpaceIndex <= 0)
{
if (!request.IsFirstLineElement)
return null;
}
else
{
textLength = lastSpaceIndex + 1;
}
}
text = text.Substring(0, textLength);
var endIndex = startIndex + textLength;
var nextIndex = endIndex;
while (nextIndex + 1 < Text.Length && Text[nextIndex] == space)
nextIndex++;
// measure final text
var width = paint.MeasureText(text);
return new TextMeasurementResult
return new TextBlockSize
{
Width = width,
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent,
LineHeight = Style.LineHeight ?? 1,
StartIndex = startIndex,
EndIndex = endIndex,
NextIndex = nextIndex,
TotalIndex = Text.Length
LineHeight = Style.LineHeight ?? 1
};
}
private TextBlockSize GetSizeForSpace()
{
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
return new TextBlockSize
{
Width = paint.MeasureText(" "),
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent,
LineHeight = Style.LineHeight ?? 1
};
}
public virtual void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToFontMetrics();
var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
request.Canvas.DrawText(text, Position.Zero, Style);
Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
if (!string.IsNullOrWhiteSpace(Text))
{
if (RequiresShaping)
Canvas.DrawShapedText(Text, Position.Zero, Style);
else
Canvas.DrawText(Text, Position.Zero, Style);
}
// draw underline
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
@@ -124,7 +94,7 @@ namespace QuestPDF.Elements.Text.Items
void DrawLine(float offset, float thickness)
{
request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
}
}
}
+98 -75
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Elements.Text.Items;
@@ -11,108 +12,145 @@ namespace QuestPDF.Elements.Text
internal class TextBlock : Element, IStateResettable
{
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
public List<ITextBlockItem> Items { get; set; } = new();
public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
private Queue<ITextBlockItem> RenderingQueue { get; set; }
private int CurrentElementIndex { get; set; }
internal override void Initialize(IPageContext pageContext, ICanvas canvas)
{
Items = SplitElementsBySpace().ToList();
Items.ForEach(x =>
{
x.PageContext = pageContext;
x.Canvas = canvas;
});
base.Initialize(pageContext, canvas);
}
public void ResetState()
{
RenderingQueue = new Queue<ITextBlockItem>(Items);
CurrentElementIndex = 0;
}
private IEnumerable<ITextBlockItem> SplitElementsBySpace()
{
foreach (var item in Items)
{
if (item is TextBlockSpan span)
{
span.Text ??= "";
foreach (var spanItem in Regex.Split(span.Text, "( )|([^ ]+)"))
{
yield return new TextBlockSpan
{
Text = spanItem,
Style = span.Style
};
}
}
}
}
internal override SpacePlan Measure(Size availableSpace)
{
if (!RenderingQueue.Any())
if (!Items.Any())
return SpacePlan.FullRender(Size.Zero);
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
if (!lines.Any())
return SpacePlan.Wrap();
var width = lines.Max(x => x.Width);
var height = lines.Sum(x => x.LineHeight);
if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
return SpacePlan.Wrap();
var fullyRenderedItemsCount = lines
.SelectMany(x => x.Elements)
.GroupBy(x => x.Item)
.Count(x => x.Any(y => y.Measurement.IsLast));
if (fullyRenderedItemsCount == RenderingQueue.Count)
if (CurrentElementIndex + lines.Sum(x => x.Elements.Count) == Items.Count)
return SpacePlan.FullRender(width, height);
return SpacePlan.PartialRender(width, height);
}
internal override void Draw(Size availableSpace)
{
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
if (!lines.Any())
return;
var heightOffset = 0f;
var widthOffset = 0f;
foreach (var line in lines)
var isLastPart = lines.Sum(x => x.Elements.Count) + CurrentElementIndex == Items.Count;
foreach (var sourceLine in lines)
{
widthOffset = 0f;
var alignmentOffset = GetAlignmentOffset(line.Width);
var isLastLine = sourceLine == lines.Last();
var line = sourceLine;
if (!(isLastPart && isLastLine))
{
var spans = sourceLine.Elements
.Where(x => x.Item is TextBlockSpan)
.SkipWhile(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
.Reverse()
.SkipWhile(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
.Reverse()
.ToList();
var wordsWidth = spans
.Where(x => !string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
.Sum(x => x.Measurement.Width);
var spaceSpans = spans.Where(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text)).ToList();
var spaceWidth = (availableSpace.Width - wordsWidth) / spaceSpans.Count;
spaceSpans.ForEach(x => x.Measurement.Width = spaceWidth);
line = TextLine.From(spans);
}
var alignmentOffset = GetAlignmentOffset(line.Width);
Canvas.Translate(new Position(alignmentOffset, 0));
Canvas.Translate(new Position(0, -line.Ascent));
foreach (var item in line.Elements)
{
var textDrawingRequest = new TextDrawingRequest
{
Canvas = Canvas,
PageContext = PageContext,
StartIndex = item.Measurement.StartIndex,
EndIndex = item.Measurement.EndIndex,
TextSize = new Size(item.Measurement.Width, line.LineHeight),
TotalAscent = line.Ascent
};
item.Item.Draw(textDrawingRequest);
Canvas.Translate(new Position(item.Measurement.Width, 0));
widthOffset += item.Measurement.Width;
}
Canvas.Translate(new Position(-alignmentOffset, 0));
Canvas.Translate(new Position(-line.Width, line.Ascent));
Canvas.Translate(new Position(0, line.LineHeight));
heightOffset += line.LineHeight;
}
Canvas.Translate(new Position(0, -heightOffset));
lines
.SelectMany(x => x.Elements)
.GroupBy(x => x.Item)
.Where(x => x.Any(y => y.Measurement.IsLast))
.Select(x => x.Key)
.ToList()
.ForEach(x => RenderingQueue.Dequeue());
var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
Canvas.Translate(new Position(0, -heightOffset));
CurrentElementIndex += lines.Sum(x => x.Elements.Count);
if (!RenderingQueue.Any())
if (CurrentElementIndex == Items.Count)
ResetState();
float GetAlignmentOffset(float lineWidth)
{
if (Alignment == HorizontalAlignment.Left)
@@ -130,19 +168,18 @@ namespace QuestPDF.Elements.Text
}
}
public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
private IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
{
var queue = new Queue<ITextBlockItem>(RenderingQueue);
var currentItemIndex = CurrentElementIndex;
var currentHeight = 0f;
while (queue.Any())
while (true)
{
var line = GetNextLine();
if (!line.Elements.Any())
yield break;
if (currentHeight + line.LineHeight > availableHeight + Size.Epsilon)
yield break;
@@ -155,43 +192,29 @@ namespace QuestPDF.Elements.Text
var currentWidth = 0f;
var currentLineElements = new List<TextLineElement>();
while (true)
{
if (!queue.Any())
if (currentItemIndex == Items.Count)
break;
var currentElement = queue.Peek();
var measurementRequest = new TextMeasurementRequest
{
Canvas = Canvas,
PageContext = PageContext,
StartIndex = currentItemIndex,
AvailableWidth = availableWidth - currentWidth,
IsFirstLineElement = !currentLineElements.Any()
};
var measurementResponse = currentElement.Measure(measurementRequest);
if (measurementResponse == null)
var currentElement = Items[currentItemIndex];
var textBlockSize = currentElement.Measure();
if (textBlockSize == null)
break;
if (currentWidth + textBlockSize.Width > availableWidth + Size.Epsilon)
break;
currentLineElements.Add(new TextLineElement
{
Item = currentElement,
Measurement = measurementResponse
Measurement = textBlockSize
});
currentWidth += measurementResponse.Width;
currentItemIndex = measurementResponse.NextIndex;
if (!measurementResponse.IsLast)
break;
currentItemIndex = 0;
queue.Dequeue();
currentWidth += textBlockSize.Width;
currentItemIndex ++;
}
return TextLine.From(currentLineElements);
+24 -6
View File
@@ -60,7 +60,7 @@ namespace QuestPDF.Fluent
public static void Placeholder(this IContainer element, string? text = null)
{
element.Component(new Elements.Placeholder
element.Component(new Placeholder
{
Text = text ?? string.Empty
});
@@ -99,27 +99,45 @@ namespace QuestPDF.Fluent
return element.Element(new Container());
}
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
public static IContainer ExternalLink(this IContainer element, string url)
{
return element.Element(new ExternalLink
return element.Hyperlink(url);
}
public static IContainer Hyperlink(this IContainer element, string url)
{
return element.Element(new Hyperlink
{
Url = url
});
}
[Obsolete("This element has been renamed since version 2022.3. Please use the Section method.")]
public static IContainer Location(this IContainer element, string locationName)
{
return element.Element(new InternalLocation
return element.Section(locationName);
}
public static IContainer Section(this IContainer element, string sectionName)
{
return element.Element(new Section
{
LocationName = locationName
LocationName = sectionName
});
}
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
public static IContainer InternalLink(this IContainer element, string locationName)
{
return element.Element(new InternalLink
return element.SectionLink(locationName);
}
public static IContainer SectionLink(this IContainer element, string sectionName)
{
return element.Element(new SectionLink
{
LocationName = locationName
SectionName = sectionName
});
}
+35
View File
@@ -0,0 +1,35 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public class Document : IDocument
{
private Action<IDocumentContainer> ContentSource { get; }
private DocumentMetadata Metadata { get; set; } = DocumentMetadata.Default;
private Document(Action<IDocumentContainer> contentSource)
{
ContentSource = contentSource;
}
public static Document Create(Action<IDocumentContainer> handler)
{
return new Document(handler);
}
public Document WithMetadata(DocumentMetadata metadata)
{
Metadata = metadata ?? Metadata;
return this;
}
#region IDocument
public DocumentMetadata GetMetadata() => Metadata;
public void Compose(IDocumentContainer container) => ContentSource(container);
#endregion
}
}
+53 -23
View File
@@ -90,57 +90,81 @@ namespace QuestPDF.Fluent
Span(Environment.NewLine);
}
private void PageNumber(string slotName, TextStyle? style = null)
private static Func<int?, string> DefaultTextFormat = x => (x ?? 123).ToString();
private void PageNumber(Func<IPageContext, int?> pageNumber, TextStyle? style, Func<int?, string>? format)
{
if (IsNullOrEmpty(slotName))
throw new ArgumentException(nameof(slotName));
style ??= TextStyle.Default;
format ??= DefaultTextFormat;
AddItemToLastTextBlock(new TextBlockPageNumber()
{
Style = style,
SlotName = slotName
Source = context => format(pageNumber(context)),
Style = style
});
}
public void CurrentPageNumber(TextStyle? style = null)
public void CurrentPageNumber(TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(PageContext.CurrentPageSlot, style);
PageNumber(x => x.CurrentPage, style, format);
}
public void TotalPages(TextStyle? style = null)
public void TotalPages(TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(PageContext.TotalPagesSlot, style);
PageNumber(x => x.GetLocation(PageContext.DocumentLocation)?.Length, style, format);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the BeginPageNumberOfSection method.")]
public void PageNumberOfLocation(string locationName, TextStyle? style = null)
{
if (IsNullOrEmpty(locationName))
throw new ArgumentException(nameof(locationName));
PageNumber(locationName, style);
BeginPageNumberOfSection(locationName);
}
public void InternalLocation(string? text, string locationName, TextStyle? style = null)
public void BeginPageNumberOfSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(x => x.GetLocation(locationName)?.PageStart, style, format);
}
public void EndPageNumberOfSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(x => x.GetLocation(locationName)?.PageEnd, style, format);
}
public void PageNumberWithinSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageEnd, style, format);
}
public void TotalPagesWithinSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(x => x.GetLocation(locationName)?.Length, style, format);
}
public void SectionLink(string? text, string sectionName, TextStyle? style = null)
{
if (IsNullOrEmpty(text))
return;
if (IsNullOrEmpty(locationName))
throw new ArgumentException(nameof(locationName));
if (IsNullOrEmpty(sectionName))
throw new ArgumentException(nameof(sectionName));
style ??= TextStyle.Default;
AddItemToLastTextBlock(new TextBlockInternalLink
AddItemToLastTextBlock(new TextBlockSectionLink
{
Style = style,
Text = text,
LocationName = locationName
SectionName = sectionName
});
}
public void ExternalLocation(string? text, string url, TextStyle? style = null)
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
public void InternalLocation(string? text, string locationName, TextStyle? style = null)
{
SectionLink(text, locationName, style);
}
public void Hyperlink(string? text, string url, TextStyle? style = null)
{
if (IsNullOrEmpty(text))
return;
@@ -150,7 +174,7 @@ namespace QuestPDF.Fluent
style ??= TextStyle.Default;
AddItemToLastTextBlock(new TextBlockExternalLink
AddItemToLastTextBlock(new TextBlockHyperlink
{
Style = style,
Text = text,
@@ -158,6 +182,12 @@ namespace QuestPDF.Fluent
});
}
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
public void ExternalLocation(string? text, string url, TextStyle? style = null)
{
Hyperlink(text, url, style);
}
public IContainer Element()
{
var container = new Container();
+4 -3
View File
@@ -8,11 +8,12 @@ namespace QuestPDF.Infrastructure
void DrawRectangle(Position vector, Size size, string color);
void DrawText(string text, Position position, TextStyle style);
void DrawShapedText(string text, Position position, TextStyle style);
void DrawImage(SKImage image, Position position, Size size);
void DrawExternalLink(string url, Size size);
void DrawLocationLink(string locationName, Size size);
void DrawLocation(string locationName);
void DrawHyperlink(string url, Size size);
void DrawSectionLink(string sectionName, Size size);
void DrawSection(string sectionName);
void Rotate(float angle);
void Scale(float scaleX, float scaleY);
+12 -4
View File
@@ -2,10 +2,18 @@
namespace QuestPDF.Infrastructure
{
public interface IPageContext
internal class DocumentLocation
{
void SetLocationPage(string key);
int GetLocationPage(string key);
ICollection<string> GetRegisteredLocations();
public string Name { get; set; }
public int PageStart { get; set; }
public int PageEnd { get; set; }
public int Length => PageEnd - PageStart + 1;
}
internal interface IPageContext
{
int CurrentPage { get; }
void SetSectionPage(string name);
DocumentLocation? GetLocation(string name);
}
}
+26 -25
View File
@@ -1,44 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuestPDF.Infrastructure
{
public class PageContext : IPageContext
internal class PageContext : IPageContext
{
public const string CurrentPageSlot = "currentPage";
public const string TotalPagesSlot = "totalPages";
public const string DocumentLocation = "document";
private Dictionary<string, int> Locations { get; } = new Dictionary<string, int>();
private int PageNumber { get; set; }
private List<DocumentLocation> Locations { get; } = new();
public int CurrentPage { get; private set; }
internal void SetPageNumber(int number)
{
PageNumber = number;
Locations[CurrentPageSlot] = number;
if (!Locations.ContainsKey(TotalPagesSlot) || Locations[TotalPagesSlot] < number)
Locations[TotalPagesSlot] = number;
CurrentPage = number;
SetSectionPage(DocumentLocation);
}
public void SetLocationPage(string key)
public void SetSectionPage(string name)
{
if (Locations.ContainsKey(key))
return;
Locations[key] = PageNumber;
var location = GetLocation(name);
if (location == null)
{
location = new DocumentLocation
{
Name = name,
PageStart = CurrentPage,
PageEnd = CurrentPage
};
Locations.Add(location);
}
if (location.PageEnd < CurrentPage)
location.PageEnd = CurrentPage;
}
public int GetLocationPage(string key)
public DocumentLocation? GetLocation(string name)
{
if (!Locations.ContainsKey(key))
throw new ArgumentException($"The location '{key}' does not exists.");
return Locations[key];
}
public ICollection<string> GetRegisteredLocations()
{
return Locations.Keys;
return Locations.FirstOrDefault(x => x.Name == name);
}
}
}
+4 -2
View File
@@ -17,7 +17,8 @@ namespace QuestPDF.Infrastructure
internal bool? HasStrikethrough { get; set; }
internal bool? HasUnderline { get; set; }
internal string? Key { get; private set; }
internal object PaintKey { get; private set; }
internal object FontMetricsKey { get; private set; }
internal static TextStyle LibraryDefault => new TextStyle
{
@@ -42,7 +43,8 @@ namespace QuestPDF.Infrastructure
HasGlobalStyleApplied = true;
ApplyParentStyle(globalStyle);
Key ??= $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{FontWeight}|{IsItalic}|{HasStrikethrough}|{HasUnderline}";
PaintKey ??= (FontType, Size, FontWeight, IsItalic, Color);
FontMetricsKey ??= (FontType, Size, FontWeight, IsItalic);
}
internal void ApplyParentStyle(TextStyle parentStyle)
+4
View File
@@ -13,6 +13,10 @@ namespace QuestPDF.Infrastructure
Feet,
Inch,
/// <summary>
/// 1/1000th of inch
/// </summary>
Mill
}
+1
View File
@@ -25,6 +25,7 @@
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.3" />
</ItemGroup>
<ItemGroup>