Merge pull request #106 from SvizelPritula/main

FontManager: Add support for multiple variants of font families registered at runtime
This commit is contained in:
Marcin Ziąbek 2022-02-21 22:26:51 +01:00 коммит произвёл GitHub
Родитель 68df390d0e 78eef45ce2
Коммит 8a64abaac2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 265 добавлений и 12 удалений

Просмотреть файл

@ -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;
}
}
}