浏览代码

Make typeface matching and synthetic typeface creation customizable (#18890)

* Make typeface matching and synthetic typeface creation customizable

* Rename test collection

* Revert breaking change

* Directly use the DefaultFontFamily name when the alias is being used
Benedikt Stebner 4 月之前
父节点
当前提交
ae0573a789

+ 12 - 54
src/Avalonia.Base/Media/FontManager.cs

@@ -109,9 +109,9 @@ namespace Avalonia.Media
 
                         var familyName = fontFamily.FamilyNames[i];
 
-                        if(_fontFamilyMappings != null && _fontFamilyMappings.TryGetValue(familyName, out var mappedFontFamily))
+                        if (_fontFamilyMappings != null && _fontFamilyMappings.TryGetValue(familyName, out var mappedFontFamily))
                         {
-                            if(mappedFontFamily.Key != null)
+                            if (mappedFontFamily.Key != null)
                             {
                                 key = mappedFontFamily.Key;
                             }
@@ -123,6 +123,11 @@ namespace Avalonia.Media
                             familyName = mappedFontFamily.FamilyNames.PrimaryFamilyName;
                         }
 
+                        if (familyName == FontFamily.DefaultFontFamilyName)
+                        {
+                            return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
+                        }
+
                         if (TryGetGlyphTypefaceByKeyAndName(typeface, key, familyName, out glyphTypeface) &&
                             glyphTypeface.FamilyName.Contains(familyName))
                         {
@@ -274,6 +279,11 @@ namespace Avalonia.Media
                     var familyName = fontFamily.FamilyNames[i];
                     var source = key.Source.EnsureAbsolute(key.BaseUri);
 
+                    if(familyName == FontFamily.DefaultFontFamilyName)
+                    {
+                        familyName = DefaultFontFamily.Name;
+                    }
+
                     if (TryGetFontCollection(source, out var fontCollection) &&
                         fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
                     {
@@ -286,58 +296,6 @@ namespace Avalonia.Media
             return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface);
         }
 
-        /// <summary>
-        /// Tries to create a synthetic glyph typefacefor specified source glyph typeface and font properties.
-        /// </summary>
-        /// <param name="fontManager">The font manager implementation.</param>
-        /// <param name="glyphTypeface">The source glyph typeface.</param>
-        /// <param name="style">The requested font style.</param>
-        /// <param name="weight">The requested font weight.</param>
-        /// <param name="syntheticGlyphTypeface">The created synthetic glyph typeface.</param>
-        /// <returns>
-        ///     <c>True</c>, if the <see cref="FontManager"/> could create a synthetic glyph typeface, <c>False</c> otherwise.
-        /// </returns>
-        internal static bool TryCreateSyntheticGlyphTypeface(IFontManagerImpl fontManager, IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight,
-           [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
-        {
-            if (fontManager == null)
-            {
-                syntheticGlyphTypeface = null;
-
-                return false;
-            }
-
-            if (glyphTypeface is IGlyphTypeface2 glyphTypeface2)
-            {
-                var fontSimulations = FontSimulations.None;
-
-                if (style != FontStyle.Normal && glyphTypeface2.Style != style)
-                {
-                    fontSimulations |= FontSimulations.Oblique;
-                }
-
-                if ((int)weight >= 600 && glyphTypeface2.Weight < weight)
-                {
-                    fontSimulations |= FontSimulations.Bold;
-                }
-
-                if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream))
-                {
-                    using (stream)
-                    {
-                        fontManager.TryCreateGlyphTypeface(stream, fontSimulations,
-                            out syntheticGlyphTypeface);
-
-                        return syntheticGlyphTypeface != null;
-                    }
-                }
-            }
-
-            syntheticGlyphTypeface = null;
-
-            return false;
-        }
-
         internal IReadOnlyList<Typeface> GetFamilyTypefaces(FontFamily fontFamily)
         {
             var key = fontFamily.Key;

+ 8 - 6
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@@ -71,14 +71,16 @@ namespace Avalonia.Media.Fonts
 
                 if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
                 {
-                    if(_fontManager != null && FontManager.TryCreateSyntheticGlyphTypeface(_fontManager, glyphTypeface, style, weight, out var syntheticGlyphTypeface))
+                    var matchedKey = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
+
+                    if(matchedKey != key)
                     {
-                        glyphTypeface = syntheticGlyphTypeface;
+                        if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface))
+                        {
+                            glyphTypeface = syntheticGlyphTypeface;
+                        }
                     }
 
-                    //Make sure we cache the found match
-                    glyphTypefaces.TryAdd(key, glyphTypeface);
-
                     return true;
                 }
             }
@@ -143,7 +145,7 @@ namespace Avalonia.Media.Fonts
             }
         }
 
-        bool IFontCollection2.TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
+        public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
         {
             familyTypefaces = null;
 

+ 83 - 1
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@@ -23,7 +23,7 @@ namespace Avalonia.Media.Fonts
         public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch,
            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
 
-        public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch,
+        public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch,
             string? familyName, CultureInfo? culture, out Typeface match)
         {
             match = default;
@@ -59,6 +59,88 @@ namespace Avalonia.Media.Fonts
             return false;
         }
 
+        public virtual bool TryCreateSyntheticGlyphTypeface(
+            IGlyphTypeface glyphTypeface,
+            FontStyle style, 
+            FontWeight weight, 
+            FontStretch stretch, 
+            [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
+        {
+            syntheticGlyphTypeface = null;
+
+            //Source family should be present in the cache.
+            if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
+            {
+                return false;
+            }
+
+            var fontManager = FontManager.Current.PlatformImpl;
+
+            var key = new FontCollectionKey(style, weight, stretch);
+
+            var currentKey =
+                new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
+
+            if (currentKey == key)
+            {
+                return false;
+            }
+
+            if (glyphTypeface is not IGlyphTypeface2 glyphTypeface2)
+            {
+                return false;
+            }
+
+            var fontSimulations = FontSimulations.None;
+
+            if (style != FontStyle.Normal && glyphTypeface2.Style != style)
+            {
+                fontSimulations |= FontSimulations.Oblique;
+            }
+
+            if ((int)weight >= 600 && glyphTypeface2.Weight < weight)
+            {
+                fontSimulations |= FontSimulations.Bold;
+            }
+
+            if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream))
+            {
+                using (stream)
+                {
+                    if (fontManager.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface))
+                    {
+                        //Add the TypographicFamilyName to the cache
+                        if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
+                        {
+                            AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, syntheticGlyphTypeface);
+                        }
+
+                        foreach (var kvp in glyphTypeface2.FamilyNames)
+                        {
+                            AddGlyphTypefaceByFamilyName(kvp.Value, syntheticGlyphTypeface);
+                        }
+
+                        return true;
+                    }
+
+                    return false;
+                }
+            }
+
+            return false;
+
+            void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface)
+            {
+                var typefaces = _glyphTypefaceCache.GetOrAdd(familyName,
+                    x =>
+                    {
+                        return new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
+                    });
+
+                typefaces.TryAdd(key, glyphTypeface);
+            }
+        }
+
         public abstract void Initialize(IFontManagerImpl fontManager);
 
         public abstract IEnumerator<FontFamily> GetEnumerator();

+ 11 - 0
src/Avalonia.Base/Media/Fonts/IFontCollection.cs

@@ -59,5 +59,16 @@ namespace Avalonia.Media.Fonts
         ///     <c>True</c>, if the <see cref="IFontCollection2"/> could get the list of typefaces, <c>False</c> otherwise.
         /// </returns>
         bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces);
+
+        /// <summary>
+        /// Try to get a synthetic glyph typeface for given parameters.
+        /// </summary>
+        /// <param name="glyphTypeface">The glyph typeface we try to synthesize.</param>
+        /// <param name="style">The font style.</param>
+        /// <param name="weight">The font weight.</param>
+        /// <param name="stretch">The font stretch.</param>
+        /// <param name="syntheticGlyphTypeface"></param>
+        /// <returns>Returns <c>true</c> if a synthetic glyph typface can be created; otherwise, <c>false</c></returns>
+        bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface);
     }
 }

+ 4 - 2
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@@ -91,9 +91,11 @@ namespace Avalonia.Media.Fonts
                 }
 
                 //Try to create a synthetic glyph typeface
-                if (FontManager.TryCreateSyntheticGlyphTypeface(_fontManager.PlatformImpl, glyphTypeface, style, weight, out var syntheticGlyphTypeface))
+                if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface))
                 {
                     glyphTypeface = syntheticGlyphTypeface;
+
+                    return true;
                 }
             }
 
@@ -159,7 +161,7 @@ namespace Avalonia.Media.Fonts
             }
         }
 
-        bool IFontCollection2.TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
+        public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
         {
             familyTypefaces = null;
 

+ 5 - 1
src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs

@@ -245,7 +245,11 @@ namespace Avalonia.Headless
 
         public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface)
         {
-            glyphTypeface = new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal);
+            glyphTypeface = new HeadlessGlyphTypefaceImpl(
+                FontFamily.DefaultFontFamilyName, 
+                fontSimulations.HasFlag(FontSimulations.Oblique) ? FontStyle.Italic : FontStyle.Normal, 
+                fontSimulations.HasFlag(FontSimulations.Bold) ? FontWeight.Bold : FontWeight.Normal, 
+                FontStretch.Normal);
 
             TryCreateGlyphTypefaceCount++;
 

+ 28 - 6
tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using Avalonia.Media;
 using Avalonia.Media.Fonts;
 using Avalonia.UnitTests;
@@ -18,6 +19,7 @@ namespace Avalonia.Skia.UnitTests.Media
 
         private const string s_manrope = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope";
 
+
         [InlineData(FontWeight.SemiLight, FontStyle.Normal)]
         [InlineData(FontWeight.Bold, FontStyle.Italic)]
         [InlineData(FontWeight.Heavy, FontStyle.Oblique)]
@@ -28,7 +30,7 @@ namespace Avalonia.Skia.UnitTests.Media
             {
                 var source = new Uri(s_notoMono, UriKind.Absolute);
 
-                var fontCollection = new EmbeddedFontCollection(source, source);
+                var fontCollection = new TestEmbeddedFontCollection(source, source);
 
                 fontCollection.Initialize(new CustomFontManagerImpl());
 
@@ -47,7 +49,7 @@ namespace Avalonia.Skia.UnitTests.Media
             {
                 var source = new Uri(s_notoMono, UriKind.Absolute);
 
-                var fontCollection = new EmbeddedFontCollection(source, source);
+                var fontCollection = new TestEmbeddedFontCollection(source, source);
 
                 fontCollection.Initialize(new CustomFontManagerImpl());
 
@@ -62,7 +64,7 @@ namespace Avalonia.Skia.UnitTests.Media
             {
                 var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T", UriKind.Absolute);
 
-                var fontCollection = new EmbeddedFontCollection(source, source);
+                var fontCollection = new TestEmbeddedFontCollection(source, source);
 
                 fontCollection.Initialize(new CustomFontManagerImpl());
 
@@ -79,7 +81,7 @@ namespace Avalonia.Skia.UnitTests.Media
             {
                 var source = new Uri(s_manrope, UriKind.Absolute);
 
-                var fontCollection = new EmbeddedFontCollection(source, source);
+                var fontCollection = new TestEmbeddedFontCollection(source, source);
 
                 fontCollection.Initialize(new CustomFontManagerImpl());
 
@@ -102,7 +104,7 @@ namespace Avalonia.Skia.UnitTests.Media
             {
                 var source = new Uri(s_manrope, UriKind.Absolute);
 
-                var fontCollection = new TestEmbeddedFontCollection(source, source);
+                var fontCollection = new TestEmbeddedFontCollection(source, source, true);
 
                 fontCollection.Initialize(new CustomFontManagerImpl());
 
@@ -120,11 +122,31 @@ namespace Avalonia.Skia.UnitTests.Media
 
         private class TestEmbeddedFontCollection : EmbeddedFontCollection
         {
-            public TestEmbeddedFontCollection(Uri key, Uri source) : base(key, source)
+            private bool _createSyntheticTypefaces;
+
+            public TestEmbeddedFontCollection(Uri key, Uri source, bool createSyntheticTypefaces = false) : base(key, source)
             {
+                _createSyntheticTypefaces = createSyntheticTypefaces;
             }
 
             public IDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> GlyphTypefaceCache => _glyphTypefaceCache;
+
+            public override bool TryCreateSyntheticGlyphTypeface(
+               IGlyphTypeface glyphTypeface,
+               FontStyle style, 
+               FontWeight weight,
+               FontStretch stretch,
+               [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
+            {
+                if (!_createSyntheticTypefaces)
+                {
+                    syntheticGlyphTypeface = null;
+
+                    return false;
+                }
+
+                return base.TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out syntheticGlyphTypeface);
+            }
         }
     }
 }

+ 109 - 2
tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs

@@ -1,7 +1,10 @@
 #nullable enable
 
+using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
 using Avalonia.Media;
 using Avalonia.Media.Fonts;
 using Avalonia.UnitTests;
@@ -11,6 +14,9 @@ namespace Avalonia.Skia.UnitTests.Media
 {
     public class FontCollectionTests
     {
+        private const string NotoMono =
+          "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests";
+
         [InlineData("Hello World 6", "Hello World 6", FontStyle.Normal, FontWeight.Normal)]
         [InlineData("Hello World Italic", "Hello World", FontStyle.Italic, FontWeight.Normal)]
         [InlineData("Hello World Italic Bold", "Hello World", FontStyle.Italic, FontWeight.Bold)]
@@ -41,8 +47,6 @@ namespace Avalonia.Skia.UnitTests.Media
 
                 Assert.True(fontCollection.TryGetGlyphTypeface("Arial", FontStyle.Normal, FontWeight.ExtraBlack, FontStretch.Normal, out var glyphTypeface));
 
-                Assert.True(glyphTypeface.FontSimulations == FontSimulations.Bold);
-
                 Assert.True(fontCollection.GlyphTypefaceCache.TryGetValue("Arial", out var glyphTypefaces));
 
                 Assert.Equal(2, glyphTypefaces.Count);
@@ -64,5 +68,108 @@ namespace Avalonia.Skia.UnitTests.Media
 
             public IDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> GlyphTypefaceCache => _glyphTypefaceCache;
         }
+
+        [Fact]
+        public void Should_Use_Fallback()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                var source = new Uri(NotoMono, UriKind.Absolute);
+
+                var fallback = new FontFallback { FontFamily = new FontFamily("Arial"), UnicodeRange = new UnicodeRange('A', 'A') };
+
+                var fontCollection = new CustomizableFontCollection(source, source, new[] { fallback  });
+
+                fontCollection.Initialize(new CustomFontManagerImpl());
+
+                Assert.True(fontCollection.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match));
+
+                Assert.Equal("Arial", match.FontFamily.Name);
+            }
+        }
+
+        [Fact]
+        public void Should_Ignore_FontFamily()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                var source = new Uri(NotoMono + "#Noto Mono", UriKind.Absolute);
+
+                var ignorable = new FontFamily(new Uri(NotoMono, UriKind.Absolute), "Noto Mono");
+
+                var typeface = new Typeface(ignorable);
+
+                var fontCollection = new CustomizableFontCollection(source, source, null, new[] { ignorable });
+
+                fontCollection.Initialize(new CustomFontManagerImpl());
+
+                Assert.False(fontCollection.TryCreateSyntheticGlyphTypeface(
+                    typeface.GlyphTypeface, 
+                    FontStyle.Italic, 
+                    FontWeight.DemiBold, 
+                    FontStretch.Normal, 
+                    out var syntheticGlyphTypeface));
+            }
+        }
+
+        private class CustomizableFontCollection : EmbeddedFontCollection
+        {
+            private readonly IReadOnlyList<FontFallback>? _fallbacks;
+            private readonly IReadOnlyList<FontFamily>? _ignorables;
+
+            public CustomizableFontCollection(Uri key, Uri source, IReadOnlyList<FontFallback>? fallbacks = null, IReadOnlyList<FontFamily>? ignorables = null) : base(key, source)
+            {
+                _fallbacks = fallbacks;
+                _ignorables = ignorables;
+            }
+
+            public override bool TryMatchCharacter(
+                int codepoint, 
+                FontStyle style, 
+                FontWeight weight, 
+                FontStretch stretch, 
+                string? familyName, 
+                CultureInfo? culture, 
+                out Typeface match)
+            {
+                if(_fallbacks is not null)
+                {
+                    foreach (var fallback in _fallbacks)
+                    {
+                        if (fallback.UnicodeRange.IsInRange(codepoint))
+                        {
+                            match = new Typeface(fallback.FontFamily, style, weight, stretch);
+
+                            return true;
+                        }
+                    }
+                }
+
+                return base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match);
+            }
+
+            public override bool TryCreateSyntheticGlyphTypeface(
+                IGlyphTypeface glyphTypeface, 
+                FontStyle style, 
+                FontWeight weight,
+                FontStretch stretch, 
+                [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
+            {
+                syntheticGlyphTypeface = null;
+
+                if(_ignorables is not null)
+                {
+                    foreach (var ignorable in _ignorables)
+                    {
+                        if (glyphTypeface.FamilyName == ignorable.Name || glyphTypeface is IGlyphTypeface2 glyphTypeface2 && glyphTypeface2.TypographicFamilyName == ignorable.Name)
+                        {
+                            return false;
+                        }
+                    }
+                }
+
+                return base.TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out syntheticGlyphTypeface);
+            }
+        }
     }
 }