Browse Source

Allow multiple font sources per FontFamily and make sure combinations of system and embedded fonts can be used (#12871)

Benedikt Stebner 2 years ago
parent
commit
cb461ab043

+ 16 - 0
src/Avalonia.Base/Media/CompositeFontFamilyKey.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Media.Fonts;
+
+namespace Avalonia.Media
+{
+    internal class CompositeFontFamilyKey : FontFamilyKey
+    {
+        public CompositeFontFamilyKey(Uri source, FontFamilyKey[] keys) : base(source, null)
+        {
+            Keys = keys;
+        }
+
+        public IReadOnlyList<FontFamilyKey> Keys { get; }
+    }
+}

+ 67 - 37
src/Avalonia.Base/Media/FontFamily.cs

@@ -1,5 +1,7 @@
 using System;
+using System.Collections.Generic;
 using Avalonia.Media.Fonts;
+using Avalonia.Utilities;
 
 namespace Avalonia.Media
 {
@@ -34,19 +36,42 @@ namespace Avalonia.Media
                 throw new ArgumentNullException(nameof(name));
             }
 
-            var fontFamilySegment = GetFontFamilyIdentifier(name);
+            var fontSources = GetFontSourceIdentifier(name);
 
-            if (fontFamilySegment.Source != null)
+            FamilyNames = new FamilyNameCollection(fontSources);
+
+            if (fontSources.Count == 1)
             {
-                if (baseUri != null && !baseUri.IsAbsoluteUri)
+                if(fontSources[0].Source is Uri source)
                 {
-                    throw new ArgumentException("Base uri must be an absolute uri.", nameof(baseUri));
-                }
+                    if (baseUri != null && !baseUri.IsAbsoluteUri)
+                    {
+                        throw new ArgumentException("Base uri must be an absolute uri.", nameof(baseUri));
+                    }
 
-                Key = new FontFamilyKey(fontFamilySegment.Source, baseUri);
+                    Key = new FontFamilyKey(source, baseUri);
+                }
             }
+            else
+            {
+                var keys = new FontFamilyKey[fontSources.Count];
+
+                for (int i = 0; i < fontSources.Count; i++)
+                {
+                    var fontSource = fontSources[i];
 
-            FamilyNames = new FamilyNameCollection(fontFamilySegment.Name);
+                    if(fontSource.Source is not null)
+                    {
+                        keys[i] = new FontFamilyKey(fontSource.Source, baseUri);
+                    }
+                    else
+                    {
+                        keys[i] = new FontFamilyKey(new Uri(FontManager.SystemFontScheme + ":" + fontSource.Name, UriKind.Absolute));
+                    }
+                }
+
+                Key = new CompositeFontFamilyKey(new Uri(FontManager.CompositeFontScheme + ":" + name, UriKind.Absolute), keys);
+            }
         }
 
         /// <summary>
@@ -88,44 +113,49 @@ namespace Avalonia.Media
             return new FontFamily(s);
         }
 
-        private struct FontFamilyIdentifier
+        private static FrugalStructList<FontSourceIdentifier> GetFontSourceIdentifier(string name)
         {
-            public FontFamilyIdentifier(string name, Uri? source)
-            {
-                Name = name;
-                Source = source;
-            }
-
-            public string Name { get; }
-
-            public Uri? Source { get; }
-        }
+            var result = new FrugalStructList<FontSourceIdentifier>(1);
 
-        private static FontFamilyIdentifier GetFontFamilyIdentifier(string name)
-        {
-            var segments = name.Split('#');
+            var segments = name.Split(',');
 
-            switch (segments.Length)
+            for (int i = 0; i < segments.Length; i++)
             {
-                case 1:
-                    {
-                        return new FontFamilyIdentifier(segments[0], null);
-                    }
+                var segment = segments[i];
+                var innerSegments = segment.Split('#');
 
-                case 2:
-                    {
-                        var source = segments[0].StartsWith("/", StringComparison.Ordinal)
-                            ? new Uri(segments[0], UriKind.Relative)
-                            : new Uri(segments[0], UriKind.RelativeOrAbsolute);
+                FontSourceIdentifier identifier;
 
-                        return new FontFamilyIdentifier(segments[1], source);
-                    }
+                switch (innerSegments.Length)
+                {
+                    case 1:
+                        {
+                            identifier = new FontSourceIdentifier(innerSegments[0].Trim(), null);
+                            break;
+                        }
+
+                    case 2:
+                        {
+                            var source = innerSegments[0].StartsWith("/", StringComparison.Ordinal)
+                                ? new Uri(innerSegments[0], UriKind.Relative)
+                                : new Uri(innerSegments[0], UriKind.RelativeOrAbsolute);
+
+                            identifier = new FontSourceIdentifier(innerSegments[1].Trim(), source);
+
+                            break;
+                        }
+
+                    default:
+                        {
+                            identifier = new FontSourceIdentifier(name, null);
+                            break;
+                        }
+                }
 
-                default:
-                    {
-                        return new FontFamilyIdentifier(name, null);
-                    }
+                result.Add(identifier);
             }
+
+            return result;
         }
 
         /// <summary>

+ 83 - 43
src/Avalonia.Base/Media/FontManager.cs

@@ -15,9 +15,11 @@ namespace Avalonia.Media
     /// </summary>
     public sealed class FontManager
     {
-        internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts");
+        internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts", UriKind.Absolute);
 
         public const string FontCollectionScheme = "fonts";
+        public const string SystemFontScheme = "systemfont";
+        public const string CompositeFontScheme = "compositefont";
 
         private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
         private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
@@ -95,69 +97,86 @@ namespace Avalonia.Media
 
             var fontFamily = typeface.FontFamily;
 
-            if(typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName)
+            if (typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName)
             {
                 return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
             }
 
-            if (fontFamily.Key is FontFamilyKey key)
+            if (fontFamily.Key is FontFamilyKey)
             {
-                var source = key.Source;
-
-                if (!source.IsAbsoluteUri)
+                if (fontFamily.Key is CompositeFontFamilyKey compositeKey)
                 {
-                    if (key.BaseUri == null)
+                    for (int i = 0; i < compositeKey.Keys.Count; i++)
                     {
-                        throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
-                    }
+                        var key = compositeKey.Keys[i];
 
-                    source = new Uri(key.BaseUri, source);
+                        var familyName = fontFamily.FamilyNames[i];
+                        
+                        if (TryGetGlyphTypefaceByKeyAndName(typeface, key, familyName, out glyphTypeface) && 
+                            glyphTypeface.FamilyName.Contains(familyName))
+                        {
+                            return true;
+                        }
+                    }
                 }
-
-                if (!_fontCollections.TryGetValue(source, out var fontCollection) && (source.IsAbsoluteResm() || source.IsAvares()))
+                else
                 {
-                    var embeddedFonts = new EmbeddedFontCollection(source, source);
-
-                    embeddedFonts.Initialize(PlatformImpl);
-
-                    if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
+                    if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, fontFamily.FamilyNames.PrimaryFamilyName, out glyphTypeface))
                     {
-                        fontCollection = embeddedFonts;
+                        return true;
                     }
-                }
 
-                if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
-                    typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
+                    return false;
+                }
+            }
+            else
+            {
+                if (SystemFonts.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
                 {
                     return true;
                 }
+            }
 
-                if (!fontFamily.FamilyNames.HasFallbacks)
-                {
-                    return false;
-                }
+            if (typeface.FontFamily == DefaultFontFamily)
+            {
+                return false;
             }
 
-            for (var i = 0; i < fontFamily.FamilyNames.Count; i++)
+            //Nothing was found so use the default
+            return TryGetGlyphTypeface(new Typeface(FontFamily.DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
+        }
+
+        private bool TryGetGlyphTypefaceByKeyAndName(Typeface typeface, FontFamilyKey key, string familyName, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            var source = key.Source;
+
+            if (source.Scheme == SystemFontScheme)
             {
-                var familyName = fontFamily.FamilyNames[i];
+                return SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface);
+            }
 
-                if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
+            if (!source.IsAbsoluteUri)
+            {
+                if (key.BaseUri == null)
                 {
-                    if (!fontFamily.FamilyNames.HasFallbacks || glyphTypeface.FamilyName != DefaultFontFamily.Name)
-                    {
-                        return true;
-                    }
+                    throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
                 }
+
+                source = new Uri(key.BaseUri, source);
             }
 
-            if(typeface.FontFamily == DefaultFontFamily)
+            if (TryGetFontCollection(source, out var fontCollection) && 
+                fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
             {
-                return false;
+                if (glyphTypeface.FamilyName.Contains(familyName))
+                {
+                    return true;
+                }
             }
 
-            //Nothing was found so use the default
-            return TryGetGlyphTypeface(new Typeface(FontFamily.DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
+            glyphTypeface = null;
+
+            return false;
         }
 
         /// <summary>
@@ -230,18 +249,17 @@ namespace Avalonia.Media
             }
 
             //Try to match against fallbacks first
-            if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks)
+            if (fontFamily != null && fontFamily.Key is CompositeFontFamilyKey compositeKey)
             {
-                for (int i = 1; i < fontFamily.FamilyNames.Count; i++)
+                for (int i = 0; i < compositeKey.Keys.Count; i++)
                 {
+                    var key = compositeKey.Keys[i];
                     var familyName = fontFamily.FamilyNames[i];
 
-                    foreach (var fontCollection in _fontCollections.Values)
+                    if (TryGetFontCollection(key.Source, out var fontCollection) &&
+                        fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
                     {
-                        if (fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
-                        {
-                            return true;
-                        };
+                        return true;
                     }
                 }
             }
@@ -249,5 +267,27 @@ namespace Avalonia.Media
             //Try to find a match with the system font manager
             return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface);
         }
+
+        private bool TryGetFontCollection(Uri source, [NotNullWhen(true)] out IFontCollection? fontCollection)
+        {
+            if(source.Scheme == SystemFontScheme)
+            {
+                source = SystemFontsKey;
+            }
+
+            if (!_fontCollections.TryGetValue(source, out fontCollection) && (source.IsAbsoluteResm() || source.IsAvares()))
+            {
+                var embeddedFonts = new EmbeddedFontCollection(source, source);
+
+                embeddedFonts.Initialize(PlatformImpl);
+
+                if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
+                {
+                    fontCollection = embeddedFonts;
+                }
+            }
+
+            return fontCollection != null;
+        }
     }
 }

+ 17 - 0
src/Avalonia.Base/Media/FontSourceIdentifier.cs

@@ -0,0 +1,17 @@
+using System;
+
+namespace Avalonia.Media
+{
+    internal readonly record struct FontSourceIdentifier
+    {
+        public FontSourceIdentifier(string name, Uri? source)
+        {
+            Name = name;
+            Source = source;
+        }
+
+        public string Name { get; init; }
+
+        public Uri? Source { get; init; }
+    }
+}

+ 14 - 0
src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs

@@ -28,6 +28,20 @@ namespace Avalonia.Media.Fonts
             HasFallbacks = _names.Length > 1;
         }
 
+        internal FamilyNameCollection(FrugalStructList<FontSourceIdentifier> fontSources) 
+        { 
+            _names = new string[fontSources.Count];
+
+            for (int i = 0; i < fontSources.Count; i++)
+            {
+                _names[i] = fontSources[i].Name;
+            }
+
+            PrimaryFamilyName = _names[0];
+
+            HasFallbacks = _names.Length > 1;
+        }
+
         private static string[] SplitNames(string names)
 #if NET6_0_OR_GREATER
             => names.Split(',', StringSplitOptions.TrimEntries);

+ 3 - 3
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@@ -34,7 +34,7 @@ namespace Avalonia.Media.Fonts
                     {
                         if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
                         {
-                            match = new Typeface(glyphTypeface.FamilyName, style, weight, stretch);
+                            match = new Typeface(Key.AbsoluteUri + "#" + glyphTypeface.FamilyName, style, weight, stretch);
 
                             return true;
                         }
@@ -45,9 +45,9 @@ namespace Avalonia.Media.Fonts
             {
                 if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface))
                 {
-                    if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+                    if (glyphTypeface.FamilyName.Contains(familyName) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
                     {
-                        match = new Typeface(familyName, style, weight, stretch);
+                        match = new Typeface(Key.AbsoluteUri + "#" + familyName, style, weight, stretch);
 
                         return true;
                     }

+ 1 - 1
tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs

@@ -75,7 +75,7 @@ namespace Avalonia.Base.UnitTests.Media
 
             Assert.Equal("Courier New", fontFamily.Name);
 
-            Assert.Equal(2, fontFamily.FamilyNames.Count());
+            Assert.Equal(2, fontFamily.FamilyNames.Count);
 
             Assert.Equal("Times New Roman", fontFamily.FamilyNames.Last());
         }

+ 7 - 5
tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs

@@ -1,5 +1,4 @@
-using System;
-using Avalonia.Direct2D1.Media;
+using Avalonia.Direct2D1.Media;
 using Avalonia.Media;
 using Avalonia.UnitTests;
 using Xunit;
@@ -17,8 +16,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
             {
                 Direct2D1Platform.Initialize();
 
-                var glyphTypeface =
-                    new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface;
+                var typeface = new Typeface(new FontFamily("A, B, Arial"));
+
+                var glyphTypeface = typeface.GlyphTypeface;
 
                 Assert.Equal("Arial", glyphTypeface.FamilyName);
             }
@@ -31,7 +31,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
             {
                 Direct2D1Platform.Initialize();
 
-                var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
+                var typeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold);
+
+                var glyphTypeface = typeface.GlyphTypeface;
 
                 Assert.Equal("Arial", glyphTypeface.FamilyName);
 

+ 78 - 0
tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

@@ -139,5 +139,83 @@ namespace Avalonia.Skia.UnitTests.Media
                 }
             }
         }
+
+        [Fact]
+        public void Should_Load_Embedded_Fallbacks()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+            {
+                using (AvaloniaLocator.EnterScope())
+                {
+                    var fontFamily = FontFamily.Parse("NotFound, " + s_fontUri);
+
+                    var typeface = new Typeface(fontFamily);
+
+                    var glyphTypeface = typeface.GlyphTypeface;
+
+                    Assert.NotNull(glyphTypeface);
+
+                    Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
+                }
+            }
+        }
+
+        [Fact]
+        public void Should_Match_Chararcter_Width_Embedded_Fallbacks()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+            {
+                using (AvaloniaLocator.EnterScope())
+                {
+                    var fontFamily = FontFamily.Parse("NotFound, " + s_fontUri);
+
+                    Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface));
+
+                    var glyphTypeface = typeface.GlyphTypeface;
+
+                    Assert.NotNull(glyphTypeface);
+
+                    Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
+                }
+            }
+        }
+
+        [Fact]
+        public void Should_Match_Chararcter_From_SystemFonts()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+            {
+                using (AvaloniaLocator.EnterScope())
+                {
+                    Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var typeface));
+
+                    var glyphTypeface = typeface.GlyphTypeface;
+
+                    Assert.NotNull(glyphTypeface);
+
+                    Assert.Equal(FontManager.Current.DefaultFontFamily.Name, glyphTypeface.FamilyName);
+                }
+            }
+        }
+
+        [Fact]
+        public void Should_Match_Chararcter_Width_Fallbacks()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+            {
+                using (AvaloniaLocator.EnterScope())
+                {
+                    var fontFamily = FontFamily.Parse("NotFound, Unknown");
+
+                    Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface));
+
+                    var glyphTypeface = typeface.GlyphTypeface;
+
+                    Assert.NotNull(glyphTypeface);
+
+                    Assert.Equal(FontManager.Current.DefaultFontFamily.Name, glyphTypeface.FamilyName);
+                }
+            }
+        }
     }
 }