diff --git a/QuestPDF.UnitTests/FontStyleSetTests.cs b/QuestPDF.UnitTests/FontStyleSetTests.cs new file mode 100644 index 0000000..5f5a47d --- /dev/null +++ b/QuestPDF.UnitTests/FontStyleSetTests.cs @@ -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) + ); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Drawing/FontManager.cs b/QuestPDF/Drawing/FontManager.cs index cd600e4..131c1bc 100644 --- a/QuestPDF/Drawing/FontManager.cs +++ b/QuestPDF/Drawing/FontManager.cs @@ -8,16 +8,29 @@ namespace QuestPDF.Drawing { public static class FontManager { - private static ConcurrentDictionary Typefaces = new ConcurrentDictionary(); + private static ConcurrentDictionary StyleSets = new ConcurrentDictionary(); private static ConcurrentDictionary FontMetrics = new ConcurrentDictionary(); private static ConcurrentDictionary Paints = new ConcurrentDictionary(); private static ConcurrentDictionary ColorPaint = new ConcurrentDictionary(); + 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."); + } } } diff --git a/QuestPDF/Drawing/FontStyleSet.cs b/QuestPDF/Drawing/FontStyleSet.cs new file mode 100644 index 0000000..a96b633 --- /dev/null +++ b/QuestPDF/Drawing/FontStyleSet.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using SkiaSharp; + +namespace QuestPDF.Drawing +{ + internal class FontStyleSet + { + private ConcurrentDictionary Styles = new ConcurrentDictionary(); + + 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> 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 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; + } + } +} \ No newline at end of file