Browse Source

FontManager: Add support for multiple variants of font families registered at runtime

Benjamin Swart 3 years ago
parent
commit
78eef45ce2

+ 123 - 0
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)
+            );
+        }
+    }
+}

+ 34 - 12
QuestPDF/Drawing/FontManager.cs

@@ -8,16 +8,29 @@ namespace QuestPDF.Drawing
 {
 {
     public static class FontManager
     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, SKFontMetrics> FontMetrics = new ConcurrentDictionary<string, SKFontMetrics>();
         private static ConcurrentDictionary<string, SKPaint> Paints = new ConcurrentDictionary<string, SKPaint>();
         private static ConcurrentDictionary<string, SKPaint> Paints = new ConcurrentDictionary<string, SKPaint>();
         private static ConcurrentDictionary<string, SKPaint> ColorPaint = 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)
         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)
         internal static SKPaint ColorToPaint(this string color)
         {
         {
             return ColorPaint.GetOrAdd(color, Convert);
             return ColorPaint.GetOrAdd(color, Convert);
@@ -30,11 +43,11 @@ namespace QuestPDF.Drawing
                 };
                 };
             }
             }
         }
         }
-        
+
         internal static SKPaint ToPaint(this TextStyle style)
         internal static SKPaint ToPaint(this TextStyle style)
         {
         {
             return Paints.GetOrAdd(style.Key, key => Convert(style));
             return Paints.GetOrAdd(style.Key, key => Convert(style));
-            
+
             static SKPaint Convert(TextStyle style)
             static SKPaint Convert(TextStyle style)
             {
             {
                 return new SKPaint
                 return new SKPaint
@@ -48,13 +61,22 @@ namespace QuestPDF.Drawing
 
 
             static SKTypeface GetTypeface(TextStyle style)
             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.");
+                }
             }
             }
         }
         }
 
 

+ 108 - 0
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<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;
+        }
+    }
+}