Merge pull request #106 from SvizelPritula/main
FontManager: Add support for multiple variants of font families registered at runtime
This commit is contained in:
Коммит
8a64abaac2
|
@ -0,0 +1,123 @@
|
|||
using NUnit.Framework;
|
||||
using QuestPDF.Drawing;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class FontStyleSetTests
|
||||
{
|
||||
private void ExpectComparisonOrder(SKFontStyle target, params SKFontStyle[] styles)
|
||||
{
|
||||
for (int i = 0; i < styles.Length - 1; i++)
|
||||
{
|
||||
Assert.True(FontStyleSet.IsBetterMatch(target, styles[i], styles[i + 1]));
|
||||
Assert.False(FontStyleSet.IsBetterMatch(target, styles[i + 1], styles[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_CondensedWidth()
|
||||
{
|
||||
ExpectComparisonOrder(
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 4, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 3, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 6, SKFontStyleSlant.Upright)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_ExpandedWidth()
|
||||
{
|
||||
ExpectComparisonOrder(
|
||||
new SKFontStyle(500, 6, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 6, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 7, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 8, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Upright)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_ItalicSlant()
|
||||
{
|
||||
ExpectComparisonOrder(
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Italic),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Italic),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Oblique),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Upright)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_ObliqueSlant()
|
||||
{
|
||||
ExpectComparisonOrder(
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Oblique),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Oblique),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Italic),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Upright)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_UprightSlant()
|
||||
{
|
||||
ExpectComparisonOrder(
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Oblique),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Italic)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_ThinWeight()
|
||||
{
|
||||
ExpectComparisonOrder(
|
||||
new SKFontStyle(300, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(300, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(200, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(100, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(400, 5, SKFontStyleSlant.Upright)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_RegularWeight()
|
||||
{
|
||||
ExpectComparisonOrder(
|
||||
new SKFontStyle(400, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(300, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(100, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(600, 5, SKFontStyleSlant.Upright)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_BoldWeight()
|
||||
{
|
||||
ExpectComparisonOrder(
|
||||
new SKFontStyle(600, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(600, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(700, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(800, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Upright)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_RespectsPriority()
|
||||
{
|
||||
ExpectComparisonOrder(
|
||||
new SKFontStyle(500, 5, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(600, 5, SKFontStyleSlant.Italic),
|
||||
new SKFontStyle(600, 6, SKFontStyleSlant.Upright),
|
||||
new SKFontStyle(500, 6, SKFontStyleSlant.Italic)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,16 +8,29 @@ namespace QuestPDF.Drawing
|
|||
{
|
||||
public static class FontManager
|
||||
{
|
||||
private static ConcurrentDictionary<string, SKTypeface> Typefaces = new ConcurrentDictionary<string, SKTypeface>();
|
||||
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new ConcurrentDictionary<string, FontStyleSet>();
|
||||
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 void RegisterFontType(string fontName, SKTypeface typeface)
|
||||
{
|
||||
FontStyleSet set = StyleSets.GetOrAdd(fontName, _ => new FontStyleSet());
|
||||
set.Add(typeface);
|
||||
}
|
||||
|
||||
public static void RegisterFontType(string fontName, Stream stream)
|
||||
{
|
||||
Typefaces.TryAdd(fontName, SKTypeface.FromStream(stream));
|
||||
SKTypeface typeface = SKTypeface.FromStream(stream);
|
||||
RegisterFontType(fontName, typeface);
|
||||
}
|
||||
|
||||
|
||||
public static void RegisterFontType(Stream stream)
|
||||
{
|
||||
SKTypeface typeface = SKTypeface.FromStream(stream);
|
||||
RegisterFontType(typeface.FamilyName, typeface);
|
||||
}
|
||||
|
||||
internal static SKPaint ColorToPaint(this string color)
|
||||
{
|
||||
return ColorPaint.GetOrAdd(color, Convert);
|
||||
|
@ -30,11 +43,11 @@ namespace QuestPDF.Drawing
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal static SKPaint ToPaint(this TextStyle style)
|
||||
{
|
||||
return Paints.GetOrAdd(style.Key, key => Convert(style));
|
||||
|
||||
|
||||
static SKPaint Convert(TextStyle style)
|
||||
{
|
||||
return new SKPaint
|
||||
|
@ -48,13 +61,22 @@ namespace QuestPDF.Drawing
|
|||
|
||||
static SKTypeface GetTypeface(TextStyle style)
|
||||
{
|
||||
if (Typefaces.TryGetValue(style.FontType, out var result))
|
||||
return result;
|
||||
|
||||
var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
|
||||
|
||||
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.");
|
||||
SKFontStyleWeight weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal);
|
||||
SKFontStyleWidth width = SKFontStyleWidth.Normal;
|
||||
SKFontStyleSlant slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
|
||||
|
||||
SKFontStyle skFontStyle = new SKFontStyle(weight, width, slant);
|
||||
|
||||
FontStyleSet set;
|
||||
if (StyleSets.TryGetValue(style.FontType, out set))
|
||||
{
|
||||
return set.Match(skFontStyle);
|
||||
}
|
||||
else
|
||||
{
|
||||
return SKTypeface.FromFamilyName(style.FontType, skFontStyle)
|
||||
?? throw new ArgumentException($"The typeface {style.FontType} could not be found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
internal class FontStyleSet
|
||||
{
|
||||
private ConcurrentDictionary<SKFontStyle, SKTypeface> Styles = new ConcurrentDictionary<SKFontStyle, SKTypeface>();
|
||||
|
||||
public void Add(SKTypeface typeface)
|
||||
{
|
||||
SKFontStyle 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
|
||||
int widthDifferenceA = Math.Abs(a.Width - target.Width);
|
||||
int 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
|
||||
List<SKFontStyleSlant> slantFallback = SlantFallbacks[target.Slant];
|
||||
int slantIndexA = slantFallback.IndexOf(a.Slant);
|
||||
int 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
|
||||
int weightDifferenceA = Math.Abs(a.Weight - target.Weight);
|
||||
int weightDifferenceB = Math.Abs(b.Weight - target.Weight);
|
||||
|
||||
return weightDifferenceA < weightDifferenceB;
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче