Browse Source

Fix FontCollection MatchCharacter (#19494)

* Fix FontCollection.MatchCharacter usage inside the FontManager implementation
Fix default MatchCharacter implementation
Add a unit test

* Reuse existing font collection key

* Fix resm FontFamilyIdentifier

* Fix FontFamily definition

---------

Co-authored-by: Julien Lebosquain <[email protected]>
Benedikt Stebner 1 month ago
parent
commit
51443cb089

+ 25 - 6
src/Avalonia.Base/Media/FontFamily.cs

@@ -42,7 +42,9 @@ namespace Avalonia.Media
 
 
             if (fontSources.Count == 1)
             if (fontSources.Count == 1)
             {
             {
-                if(fontSources[0].Source is Uri source)
+                var singleSource = fontSources[0];
+
+                if (singleSource.Source is Uri source)
                 {
                 {
                     if (baseUri != null && !baseUri.IsAbsoluteUri)
                     if (baseUri != null && !baseUri.IsAbsoluteUri)
                     {
                     {
@@ -51,6 +53,13 @@ namespace Avalonia.Media
 
 
                     Key = new FontFamilyKey(source, baseUri);
                     Key = new FontFamilyKey(source, baseUri);
                 }
                 }
+                else
+                {
+                    if(baseUri != null && baseUri.IsAbsoluteUri)
+                    {
+                        Key = new FontFamilyKey(baseUri);
+                    }
+                }
             }
             }
             else
             else
             {
             {
@@ -141,11 +150,21 @@ namespace Avalonia.Media
 
 
                     case 2:
                     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);
+                            var path = innerSegments[0].Trim();
+                            var innerName = innerSegments[1].Trim();
+
+                            if (string.IsNullOrEmpty(path))
+                            {
+                                identifier = new FontSourceIdentifier(innerName, null);
+                            }
+                            else
+                            {
+                                var source = path.StartsWith("/", StringComparison.Ordinal)
+                                   ? new Uri(path, UriKind.Relative)
+                                   : new Uri(path, UriKind.RelativeOrAbsolute);
+
+                                identifier = new FontSourceIdentifier(innerName, source);
+                            }                              
 
 
                             break;
                             break;
                         }
                         }

+ 24 - 10
src/Avalonia.Base/Media/FontManager.cs

@@ -271,21 +271,35 @@ namespace Avalonia.Media
             }
             }
 
 
             //Try to match against fallbacks first
             //Try to match against fallbacks first
-            if (fontFamily != null && fontFamily.Key is CompositeFontFamilyKey compositeKey)
+            if (fontFamily?.Key != null)
             {
             {
-                for (int i = 0; i < compositeKey.Keys.Count; i++)
-                {
-                    var key = compositeKey.Keys[i];
-                    var familyName = fontFamily.FamilyNames[i];
-                    var source = key.Source.EnsureAbsolute(key.BaseUri);
+                var fontUri = fontFamily.Key.Source.EnsureAbsolute(fontFamily.Key.BaseUri);
 
 
-                    if(familyName == FontFamily.DefaultFontFamilyName)
+                if (fontFamily.Key is CompositeFontFamilyKey compositeKey)
+                {
+                    for (int i = 0; i < compositeKey.Keys.Count; i++)
                     {
                     {
-                        familyName = DefaultFontFamily.Name;
+                        var key = compositeKey.Keys[i];
+                        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))
+                        {
+                            return true;
+                        }
                     }
                     }
+                }
 
 
-                    if (TryGetFontCollection(source, out var fontCollection) &&
-                        fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
+                if (fontUri.IsFontCollection())
+                {
+                    if (TryGetFontCollection(fontUri, out var fontCollection) &&
+                            fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily.Name, culture, out typeface))
                     {
                     {
                         return true;
                         return true;
                     }
                     }

+ 0 - 4
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@@ -15,8 +15,6 @@ namespace Avalonia.Media.Fonts
 
 
         private readonly Uri _source;
         private readonly Uri _source;
 
 
-        private IFontManagerImpl? _fontManager;
-
         public EmbeddedFontCollection(Uri key, Uri source)
         public EmbeddedFontCollection(Uri key, Uri source)
         {
         {
             _key = key;
             _key = key;
@@ -32,8 +30,6 @@ namespace Avalonia.Media.Fonts
 
 
         public override void Initialize(IFontManagerImpl fontManager)
         public override void Initialize(IFontManagerImpl fontManager)
         {
         {
-            _fontManager = fontManager;
-
             var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
             var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
 
 
             var fontAssets = FontFamilyLoader.LoadFontAssets(_source);
             var fontAssets = FontFamilyLoader.LoadFontAssets(_source);

+ 21 - 13
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@@ -27,29 +27,37 @@ namespace Avalonia.Media.Fonts
             string? familyName, CultureInfo? culture, out Typeface match)
             string? familyName, CultureInfo? culture, out Typeface match)
         {
         {
             match = default;
             match = default;
-
-            if (string.IsNullOrEmpty(familyName))
+        
+            //If a font family is defined we try to find a match inside that family first
+            if (familyName != null && _glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
             {
             {
-                foreach (var typefaces in _glyphTypefaceCache.Values)
+                if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
                 {
                 {
-                    if (TryGetNearestMatch(typefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
+                    if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
                     {
                     {
-                        if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
-                        {
-                            match = new Typeface(Key.AbsoluteUri + "#" + glyphTypeface.FamilyName, style, weight, stretch);
+                        match = new Typeface(new FontFamily(Key, "#" + glyphTypeface.FamilyName), style, weight, stretch);
 
 
-                            return true;
-                        }
+                        return true;
                     }
                     }
                 }
                 }
             }
             }
-            else
+
+            //Try to find a match in any font family
+            foreach (var pair in _glyphTypefaceCache)
             {
             {
-                if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface))
+                if(pair.Key == familyName)
+                {
+                    //We already tried this before
+                    continue;
+                }
+
+                glyphTypefaces = pair.Value;
+
+                if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
                 {
                 {
-                    if (glyphTypeface.FamilyName.Contains(familyName) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+                    if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
                     {
                     {
-                        match = new Typeface(Key.AbsoluteUri + "#" + familyName, style, weight, stretch);
+                        match = new Typeface(new FontFamily(Key, "#" + glyphTypeface.FamilyName) , style, weight, stretch);
 
 
                         return true;
                         return true;
                     }
                     }

+ 0 - 3
src/Avalonia.Base/Utilities/UriExtensions.cs

@@ -22,9 +22,6 @@ internal static class UriExtensions
             throw new ArgumentException($"Relative uri {uri} without base url");
             throw new ArgumentException($"Relative uri {uri} without base url");
         if (!baseUri.IsAbsoluteUri)
         if (!baseUri.IsAbsoluteUri)
             throw new ArgumentException($"Base uri {baseUri} is relative");
             throw new ArgumentException($"Base uri {baseUri} is relative");
-        if (baseUri.IsResm())
-            throw new ArgumentException(
-                $"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm");
         return new Uri(baseUri, uri);
         return new Uri(baseUri, uri);
     }
     }
 
 

+ 2 - 2
tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs

@@ -18,10 +18,10 @@ namespace Avalonia.Skia.UnitTests.Media
 
 
         public CustomFontManagerImpl()
         public CustomFontManagerImpl()
         {
         {
-            _defaultFamilyName = "Noto Mono";
-
             var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests");
             var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests");
 
 
+            _defaultFamilyName = source.AbsoluteUri + "#Noto Mono";
+
             _customFonts = new EmbeddedFontCollection(source, source);
             _customFonts = new EmbeddedFontCollection(source, source);
         }
         }
 
 

+ 14 - 11
tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs

@@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Globalization;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Media.Fonts;
 using Avalonia.Media.Fonts;
+using Avalonia.Platform;
 using Avalonia.UnitTests;
 using Avalonia.UnitTests;
 using Xunit;
 using Xunit;
 
 
@@ -72,7 +73,7 @@ namespace Avalonia.Skia.UnitTests.Media
         [Fact]
         [Fact]
         public void Should_Use_Fallback()
         public void Should_Use_Fallback()
         {
         {
-            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
             {
             {
                 var source = new Uri(NotoMono, UriKind.Absolute);
                 var source = new Uri(NotoMono, UriKind.Absolute);
 
 
@@ -80,7 +81,7 @@ namespace Avalonia.Skia.UnitTests.Media
 
 
                 var fontCollection = new CustomizableFontCollection(source, source, new[] { fallback  });
                 var fontCollection = new CustomizableFontCollection(source, source, new[] { fallback  });
 
 
-                fontCollection.Initialize(new CustomFontManagerImpl());
+                fontCollection.Initialize(FontManager.Current.PlatformImpl);
 
 
                 Assert.True(fontCollection.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match));
                 Assert.True(fontCollection.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match));
 
 
@@ -91,23 +92,25 @@ namespace Avalonia.Skia.UnitTests.Media
         [Fact]
         [Fact]
         public void Should_Ignore_FontFamily()
         public void Should_Ignore_FontFamily()
         {
         {
-            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
             {
             {
-                var source = new Uri(NotoMono + "#Noto Mono", UriKind.Absolute);
+                var key = new Uri(NotoMono, UriKind.Absolute);
 
 
                 var ignorable = new FontFamily(new Uri(NotoMono, UriKind.Absolute), "Noto Mono");
                 var ignorable = new FontFamily(new Uri(NotoMono, UriKind.Absolute), "Noto Mono");
 
 
-                var typeface = new Typeface(ignorable);
+                var fontCollection = new CustomizableFontCollection(key, key, null, new[] { ignorable });
+
+                fontCollection.Initialize(FontManager.Current.PlatformImpl);
 
 
-                var fontCollection = new CustomizableFontCollection(source, source, null, new[] { ignorable });
+                var typeface = new Typeface(ignorable);
 
 
-                fontCollection.Initialize(new CustomFontManagerImpl());
+                var glyphTypeface = typeface.GlyphTypeface;
 
 
                 Assert.False(fontCollection.TryCreateSyntheticGlyphTypeface(
                 Assert.False(fontCollection.TryCreateSyntheticGlyphTypeface(
-                    typeface.GlyphTypeface, 
-                    FontStyle.Italic, 
-                    FontWeight.DemiBold, 
-                    FontStretch.Normal, 
+                    typeface.GlyphTypeface,
+                    FontStyle.Italic,
+                    FontWeight.DemiBold,
+                    FontStretch.Normal,
                     out var syntheticGlyphTypeface));
                     out var syntheticGlyphTypeface));
             }
             }
         }
         }

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

@@ -6,6 +6,7 @@ using Avalonia.Fonts.Inter;
 using Avalonia.Headless;
 using Avalonia.Headless;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Media.Fonts;
 using Avalonia.Media.Fonts;
+using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.UnitTests;
 using Avalonia.UnitTests;
 using SkiaSharp;
 using SkiaSharp;
 using Xunit;
 using Xunit;
@@ -380,5 +381,33 @@ namespace Avalonia.Skia.UnitTests.Media
                 }
                 }
             }
             }
         }
         }
+
+        [Fact]
+        public void Should_Use_FontCollection_MatchCharacter()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+            {
+                using (AvaloniaLocator.EnterScope())
+                {
+                    FontManager.Current.AddFontCollection(
+                        new EmbeddedFontCollection(
+                            new Uri("fonts:MyCollection"), //key
+                            new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"))); //source
+
+                    var fontFamily = new FontFamily("fonts:MyCollection#Noto Mono");
+
+                    var character = "א";
+
+                    var codepoint = Codepoint.ReadAt(character, 0, out _);
+
+                    Assert.True(FontManager.Current.TryMatchCharacter(codepoint, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface));
+
+                    //Typeface should come from the font collection
+                    Assert.NotNull(typeface.FontFamily.Key);
+
+                    Assert.Equal("Noto Sans Hebrew", typeface.GlyphTypeface.FamilyName);
+                }
+            }
+        }
     }
     }
 }
 }

+ 1 - 1
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@@ -1095,7 +1095,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             {
             {
                 var text = "𖾇";
                 var text = "𖾇";
 
 
-                var typeface = new Typeface(new FontFamily(new Uri("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests"), "Noto Mono"));
+                var typeface = new Typeface(new FontFamily(new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"), "Noto Mono"));
                 var defaultRunProperties = new GenericTextRunProperties(typeface);
                 var defaultRunProperties = new GenericTextRunProperties(typeface);
                 var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrapping: TextWrapping.Wrap);
                 var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrapping: TextWrapping.Wrap);
                 var textLine = TextFormatter.Current.FormatLine(new SimpleTextSource(text, defaultRunProperties), 0, 120, paragraphProperties);
                 var textLine = TextFormatter.Current.FormatLine(new SimpleTextSource(text, defaultRunProperties), 0, 120, paragraphProperties);

+ 6 - 5
tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

@@ -1,4 +1,5 @@
-using System.Diagnostics.CodeAnalysis;
+using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
@@ -13,13 +14,13 @@ namespace Avalonia.UnitTests
         private readonly string _defaultFamilyName;
         private readonly string _defaultFamilyName;
 
 
         private static readonly Typeface _defaultTypeface =
         private static readonly Typeface _defaultTypeface =
-            new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono");
+            new Typeface(new FontFamily(new Uri("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests", UriKind.Absolute), "Noto Mono"));
         private  static readonly Typeface _italicTypeface =
         private  static readonly Typeface _italicTypeface =
-            new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Sans");
+            new Typeface(new FontFamily(new Uri("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests", UriKind.Absolute), "Noto Sans"));
         private  static readonly Typeface _emojiTypeface =
         private  static readonly Typeface _emojiTypeface =
-            new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Twitter Color Emoji");
+            new Typeface(new FontFamily(new Uri("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests"), "Twitter Color Emoji"));
 
 
-        public HarfBuzzFontManagerImpl(string defaultFamilyName = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono")
+        public HarfBuzzFontManagerImpl(string defaultFamilyName = "Noto Mono")
         {
         {
             _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
             _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
             _defaultFamilyName = defaultFamilyName;
             _defaultFamilyName = defaultFamilyName;