Implemented font-fallback as required configuration
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user