Implemented font-fallback as required configuration

This commit is contained in:
MarcinZiabek
2022-09-10 23:27:27 +02:00
parent 0468dd5f02
commit 5390fc3f1b
5 changed files with 129 additions and 24 deletions
+6 -3
View File
@@ -625,7 +625,7 @@ namespace QuestPDF.Examples
{ {
RenderingTest RenderingTest
.Create() .Create()
.ProducePdf() .ProduceImages()
.ShowResults() .ShowResults()
.RenderDocument(container => .RenderDocument(container =>
{ {
@@ -633,6 +633,9 @@ namespace QuestPDF.Examples
{ {
page.Margin(50); page.Margin(50);
page.PageColor(Colors.White); page.PageColor(Colors.White);
page.DefaultTextStyle(x => x
.Fallback(y => y.FontFamily("Segoe UI Emoji")
.Fallback(y => y.FontFamily("Microsoft YaHei"))));
page.Size(PageSizes.A4); page.Size(PageSizes.A4);
@@ -640,11 +643,11 @@ namespace QuestPDF.Examples
{ {
t.Line("This is normal text."); t.Line("This is normal text.");
t.EmptyLine(); t.EmptyLine();
t.Line("Following line should use font fallback:"); t.Line("Following line should use font fallback:");
t.Line("中文文本"); t.Line("中文文本");
t.EmptyLine(); t.EmptyLine();
t.Line("The following line contains a mix of known and unknown characters."); t.Line("The following line contains a mix of known and unknown characters.");
t.Line("Mixed line: This 中文 is 文文 a mixed 本 本 line 本 中文文本!"); t.Line("Mixed line: This 中文 is 文文 a mixed 本 本 line 本 中文文本!");
t.EmptyLine(); t.EmptyLine();
+82 -19
View File
@@ -1,4 +1,6 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing; using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions; using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements.Text.Items; using QuestPDF.Elements.Text.Items;
@@ -16,45 +18,99 @@ namespace QuestPDF.Elements.Text
public TextStyle Style { get; set; } public TextStyle Style { get; set; }
} }
public class FallbackOption
{
public TextStyle Style { get; set; }
public SKFont Font { get; set; }
public SKTypeface Typeface { get; set; }
}
private static SKFontManager FontManager => SKFontManager.Default; private static SKFontManager FontManager => SKFontManager.Default;
public static IEnumerable<TextRun> SplitWithFontFallback(this string text, TextStyle textStyle) public static IEnumerable<TextRun> SplitWithFontFallback(this string text, TextStyle textStyle)
{ {
var partStartIndex = 0; var fallbackOptions = GetFallbackOptions(textStyle).ToArray();
var partTextStyle = textStyle;
var spanStartIndex = 0;
var spanFallbackOption = fallbackOptions[0];
for (var i = 0; i < text.Length; i += char.IsSurrogatePair(text, i) ? 2 : 1) for (var i = 0; i < text.Length; i += char.IsSurrogatePair(text, i) ? 2 : 1)
{ {
var codepoint = char.ConvertToUtf32(text, i); var codepoint = char.ConvertToUtf32(text, i);
var font = partTextStyle.ToFont(); var newFallbackOption = MatchFallbackOption(fallbackOptions, codepoint);
var typeface = font.Typeface;
if (font.ContainsGlyph(codepoint)) if (newFallbackOption == spanFallbackOption)
continue; continue;
var fallbackTypeface = FontManager.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codepoint);
if (fallbackTypeface == null)
throw new DocumentDrawingException($"Could not find an appropriate font fallback for text: '{text}'");
yield return new TextRun yield return new TextRun
{ {
Content = text.Substring(partStartIndex, i - partStartIndex), Content = text.Substring(spanStartIndex, i - spanStartIndex),
Style = partTextStyle Style = spanFallbackOption.Style
}; };
partStartIndex = i; spanStartIndex = i;
partTextStyle = textStyle.FontFamily(fallbackTypeface.FamilyName).Weight((FontWeight)fallbackTypeface.FontWeight); spanFallbackOption = newFallbackOption;
} }
if (partStartIndex > text.Length) if (spanStartIndex > text.Length)
yield break; yield break;
yield return new TextRun yield return new TextRun
{ {
Content = text.Substring(partStartIndex, text.Length - partStartIndex), Content = text.Substring(spanStartIndex, text.Length - spanStartIndex),
Style = partTextStyle Style = spanFallbackOption.Style
}; };
static IEnumerable<FallbackOption> GetFallbackOptions(TextStyle? textStyle)
{
while (textStyle != null)
{
var font = textStyle.ToFont();
yield return new FallbackOption
{
Style = textStyle,
Font = font,
Typeface = font.Typeface
};
textStyle = textStyle.Fallback;
}
}
static FallbackOption MatchFallbackOption(ICollection<FallbackOption> fallbackOptions, int codepoint)
{
foreach (var fallbackOption in fallbackOptions)
{
if (fallbackOption.Font.ContainsGlyph(codepoint))
return fallbackOption;
}
var character = char.ConvertFromUtf32(codepoint);
var unicode = $"U-{codepoint:X4}";
var proposedFonts = FindFontsContainingGlyph(codepoint);
var proposedFontsFormatted = proposedFonts.Any() ? string.Join(", ", proposedFonts) : "no fonts available";
throw new DocumentDrawingException(
$"Could not find an appropriate font fallback for glyph: {unicode} '{character}'. " +
$"Font families available on current environment that contain this glyph: {proposedFontsFormatted}. " +
$"Possible solutions: " +
$"1) Use one of the listed fonts as the primary font in your document. " +
$"2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. ");
}
static IEnumerable<string> FindFontsContainingGlyph(int codepoint)
{
var fontManager = SKFontManager.Default;
return fontManager
.GetFontFamilies()
.Select(fontManager.MatchFamily)
.Where(x => x.ContainsGlyph(codepoint))
.Select(x => x.FamilyName);
}
} }
public static IEnumerable<ITextBlockItem> ApplyFontFallback(this ICollection<ITextBlockItem> textBlockItems) public static IEnumerable<ITextBlockItem> ApplyFontFallback(this ICollection<ITextBlockItem> textBlockItems)
@@ -63,6 +119,13 @@ namespace QuestPDF.Elements.Text
{ {
if (textBlockItem is TextBlockSpan textBlockSpan) if (textBlockItem is TextBlockSpan textBlockSpan)
{ {
// perform font-fallback operation only when any fallback is available
if (textBlockSpan.Style.Fallback == null)
{
yield return textBlockSpan;
continue;
}
var textRuns = textBlockSpan.Text.SplitWithFontFallback(textBlockSpan.Style); var textRuns = textBlockSpan.Text.SplitWithFontFallback(textBlockSpan.Style);
foreach (var textRun in textRuns) foreach (var textRun in textRuns)
@@ -15,6 +15,17 @@ namespace QuestPDF.Fluent
return descriptor; return descriptor;
} }
public static T Fallback<T>(this T descriptor, TextStyle? value = null) where T : TextSpanDescriptor
{
descriptor.TextStyle.Fallback = value;
return descriptor;
}
public static T Fallback<T>(this T descriptor, Func<TextStyle, TextStyle> handler) where T : TextSpanDescriptor
{
return descriptor.Fallback(handler(TextStyle.Default));
}
public static T FontColor<T>(this T descriptor, string value) where T : TextSpanDescriptor public static T FontColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
{ {
descriptor.TextStyle.Color = value; descriptor.TextStyle.Color = value;
+10
View File
@@ -14,6 +14,16 @@ namespace QuestPDF.Fluent
return style; return style;
} }
public static TextStyle Fallback(this TextStyle style, TextStyle? value = null)
{
return style.Mutate(x => x.Fallback = value);
}
public static TextStyle Fallback(this TextStyle style, Func<TextStyle, TextStyle> handler)
{
return style.Fallback(handler(TextStyle.Default));
}
[Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")] [Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")]
public static TextStyle Color(this TextStyle style, string value) public static TextStyle Color(this TextStyle style, string value)
{ {
+20 -2
View File
@@ -1,4 +1,5 @@
using System; using System;
using HarfBuzzSharp;
using QuestPDF.Helpers; using QuestPDF.Helpers;
namespace QuestPDF.Infrastructure namespace QuestPDF.Infrastructure
@@ -19,6 +20,8 @@ namespace QuestPDF.Infrastructure
internal bool? HasUnderline { get; set; } internal bool? HasUnderline { get; set; }
internal bool? WrapAnywhere { get; set; } internal bool? WrapAnywhere { get; set; }
internal TextStyle? Fallback { get; set; }
// TODO: without cache, this may be an expensive operation // TODO: without cache, this may be an expensive operation
internal object PaintKey => (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color); internal object PaintKey => (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color);
internal object FontMetricsKey => (FontFamily, Size, FontWeight, IsItalic); internal object FontMetricsKey => (FontFamily, Size, FontWeight, IsItalic);
@@ -35,7 +38,8 @@ namespace QuestPDF.Infrastructure
IsItalic = false, IsItalic = false,
HasStrikethrough = false, HasStrikethrough = false,
HasUnderline = false, HasUnderline = false,
WrapAnywhere = false WrapAnywhere = false,
Fallback = null
}; };
// it is important to create new instances for the DefaultTextStyle element to work correctly // it is important to create new instances for the DefaultTextStyle element to work correctly
@@ -48,9 +52,18 @@ namespace QuestPDF.Infrastructure
HasGlobalStyleApplied = true; HasGlobalStyleApplied = true;
ApplyParentStyle(globalStyle); ApplyParentStyle(globalStyle);
if (Fallback != null)
ApplyFallbackStyle(this);
}
internal void ApplyFallbackStyle(TextStyle parentStyle)
{
ApplyParentStyle(parentStyle, false);
Fallback?.ApplyFallbackStyle(this);
} }
internal void ApplyParentStyle(TextStyle parentStyle) internal void ApplyParentStyle(TextStyle parentStyle, bool mapFallback = true)
{ {
Color ??= parentStyle.Color; Color ??= parentStyle.Color;
BackgroundColor ??= parentStyle.BackgroundColor; BackgroundColor ??= parentStyle.BackgroundColor;
@@ -63,6 +76,9 @@ namespace QuestPDF.Infrastructure
HasStrikethrough ??= parentStyle.HasStrikethrough; HasStrikethrough ??= parentStyle.HasStrikethrough;
HasUnderline ??= parentStyle.HasUnderline; HasUnderline ??= parentStyle.HasUnderline;
WrapAnywhere ??= parentStyle.WrapAnywhere; WrapAnywhere ??= parentStyle.WrapAnywhere;
if (mapFallback)
Fallback ??= parentStyle.Fallback?.Clone();
} }
internal void OverrideStyle(TextStyle parentStyle) internal void OverrideStyle(TextStyle parentStyle)
@@ -78,12 +94,14 @@ namespace QuestPDF.Infrastructure
HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough; HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
HasUnderline = parentStyle.HasUnderline ?? HasUnderline; HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
WrapAnywhere = parentStyle.WrapAnywhere ?? WrapAnywhere; WrapAnywhere = parentStyle.WrapAnywhere ?? WrapAnywhere;
Fallback = parentStyle.Fallback?.Clone() ?? Fallback;
} }
internal TextStyle Clone() internal TextStyle Clone()
{ {
var clone = (TextStyle)MemberwiseClone(); var clone = (TextStyle)MemberwiseClone();
clone.HasGlobalStyleApplied = false; clone.HasGlobalStyleApplied = false;
clone.Fallback = Fallback?.Clone();
return clone; return clone;
} }
} }