Преглед изворни кода

Introduce font collections

Benedikt Stebner пре 2 година
родитељ
комит
d08083bbf3
36 измењених фајлова са 1059 додато и 656 уклоњено
  1. 2 0
      samples/ControlCatalog.NetCore/Program.cs
  2. 1 1
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  3. 78 31
      src/Avalonia.Base/Media/FontManager.cs
  4. 298 0
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  5. 4 0
      src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs
  6. 24 34
      src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
  7. 17 0
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  8. 100 0
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  9. 20 0
      src/Avalonia.Base/Media/IGlyphTypeface.cs
  10. 8 7
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  11. 12 1
      src/Avalonia.Base/Media/Typeface.cs
  12. 23 7
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  13. 4 1
      src/Avalonia.Base/Utilities/UriExtensions.cs
  14. 35 11
      src/Avalonia.Controls/AppBuilder.cs
  15. 14 0
      src/Avalonia.Fonts.Inter/InterFontCollection.cs
  16. 23 6
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  17. 1 1
      src/Avalonia.Themes.Fluent/Accents/Base.xaml
  18. 36 49
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  19. 14 0
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  20. 0 198
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  21. 0 73
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  22. 4 9
      src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs
  23. 11 2
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  24. 52 5
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  25. 18 2
      src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
  26. 4 2
      tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
  27. 6 10
      tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs
  28. 19 47
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  29. 101 15
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  30. 58 0
      tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs
  31. 27 52
      tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs
  32. 0 63
      tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs
  33. 13 24
      tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
  34. 9 0
      tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs
  35. 15 5
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  36. 8 0
      tests/Avalonia.UnitTests/MockGlyphTypeface.cs

+ 2 - 0
samples/ControlCatalog.NetCore/Program.cs

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Fonts.Inter;
 using Avalonia.Headless;
 using Avalonia.LogicalTree;
 using Avalonia.Threading;
@@ -124,6 +125,7 @@ namespace ControlCatalog.NetCore
                     EnableIme = true
                 })
                 .UseSkia()
+                .WithFonts(new InterFontCollection())
                 .AfterSetup(builder =>
                 {
                     builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()

+ 1 - 1
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
         {
             AvaloniaXamlLoader.Load(this);
             var fontComboBox = this.Get<ComboBox>("fontComboBox");
-            fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
+            fontComboBox.Items = FontManager.Current.SystemFonts;
             fontComboBox.SelectedIndex = 0;
         }
     }

+ 78 - 31
src/Avalonia.Base/Media/FontManager.cs

@@ -1,9 +1,11 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using Avalonia.Media.Fonts;
 using Avalonia.Platform;
+using Avalonia.Utilities;
 
 namespace Avalonia.Media
 {
@@ -13,9 +15,10 @@ namespace Avalonia.Media
     /// </summary>
     public sealed class FontManager
     {
-        private readonly ConcurrentDictionary<Typeface, IGlyphTypeface> _glyphTypefaceCache =
-            new ConcurrentDictionary<Typeface, IGlyphTypeface>();
-        private readonly FontFamily _defaultFontFamily;
+        public const string FontCollectionScheme = "fonts";
+
+        private readonly SystemFontCollection _systemFonts;
+        private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
         private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
 
         public FontManager(IFontManagerImpl platformImpl)
@@ -33,7 +36,7 @@ namespace Avalonia.Media
                 throw new InvalidOperationException("Default font family name can't be null or empty.");
             }
 
-            _defaultFontFamily = new FontFamily(DefaultFontFamilyName);
+            _systemFonts = new SystemFontCollection(this);
         }
 
         public static FontManager Current
@@ -57,11 +60,6 @@ namespace Avalonia.Media
             }
         }
 
-        /// <summary>
-        /// 
-        /// </summary>
-        public IFontManagerImpl PlatformImpl { get; }
-
         /// <summary>
         ///     Gets the system's default font family's name.
         /// </summary>
@@ -71,41 +69,92 @@ namespace Avalonia.Media
         }
 
         /// <summary>
-        ///     Get all installed font family names.
+        ///     Get all system fonts.
         /// </summary>
-        /// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
-        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
-            PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
+        public IFontCollection SystemFonts => _systemFonts;
+
+        internal IFontManagerImpl PlatformImpl { get; }
 
         /// <summary>
-        ///     Returns a new <see cref="IGlyphTypeface"/>, or an existing one if a matching <see cref="IGlyphTypeface"/> exists.
+        ///     Tries to get a glyph typeface for specified typeface.
         /// </summary>
         /// <param name="typeface">The typeface.</param>
+        /// <param name="glyphTypeface">The created glyphTypeface</param>
         /// <returns>
-        ///     The <see cref="IGlyphTypeface"/>.
+        ///     <c>True</c>, if the <see cref="FontManager"/> could create the glyph typeface, <c>False</c> otherwise.
         /// </returns>
-        public IGlyphTypeface GetOrAddGlyphTypeface(Typeface typeface)
+        public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
         {
-            while (true)
+            glyphTypeface = null;
+
+            var fontFamily = typeface.FontFamily;
+
+            if (fontFamily.Key is FontFamilyKey key)
             {
-                if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface))
+                var source = key.Source;
+
+                if (!source.IsAbsoluteUri)
                 {
-                    return glyphTypeface;
+                    if (key.BaseUri == null)
+                    {
+                        throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
+                    }
+
+                    source = new Uri(key.BaseUri, source);
                 }
 
-                glyphTypeface = PlatformImpl.CreateGlyphTypeface(typeface);
+                if (!_fontCollections.TryGetValue(source, out var fontCollection))
+                {
+                    var embeddedFonts = new EmbeddedFontCollection(source, source);
+
+                    embeddedFonts.Initialize(PlatformImpl);
+
+                    if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
+                    {
+                        fontCollection = embeddedFonts;
+                    }
+                }
 
-                if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface))
+                if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
+                    typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
                 {
-                    return glyphTypeface;
+                    return true;
                 }
+            }
 
-                if (typeface.FontFamily == _defaultFontFamily)
+            foreach (var familyName in fontFamily.FamilyNames)
+            {
+                if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
                 {
-                   throw new InvalidOperationException($"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
+                    return true;
                 }
+            }
+
+            return false;
+        }
+
+        public void AddFontCollection(IFontCollection fontCollection)
+        {
+            var key = fontCollection.Key;
+
+            if (!fontCollection.Key.IsFontCollection())
+            {
+                throw new ArgumentException(nameof(fontCollection), "Font collection Key should follow the fontCollection: scheme.");
+            }
 
-                typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight);
+            if (!_fontCollections.TryAdd(key, fontCollection))
+            {
+                throw new ArgumentException(nameof(fontCollection), "Font collection is already registered.");
+            }
+
+            fontCollection.Initialize(PlatformImpl);
+        }
+
+        public void RemoveFontCollection(Uri key)
+        {
+            if (_fontCollections.TryRemove(key, out var fontCollection))
+            {
+                fontCollection.Dispose();
             }
         }
 
@@ -123,18 +172,16 @@ namespace Avalonia.Media
         ///     <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
         /// </returns>
         public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
-            FontStretch fontStretch,
-            FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
+            FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
         {
-            if(_fontFallbacks != null)
+            if (_fontFallbacks != null)
             {
                 foreach (var fallback in _fontFallbacks)
                 {
                     typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
 
-                    var glyphTypeface = GetOrAddGlyphTypeface(typeface);
-
-                    if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
+                    if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+                    {
                         return true;
                     }
                 }

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

@@ -0,0 +1,298 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Avalonia.Platform;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.Fonts
+{
+    public class EmbeddedFontCollection : IFontCollection
+    {
+        private readonly Dictionary<string, Dictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache =
+            new Dictionary<string, Dictionary<FontCollectionKey, IGlyphTypeface>>();
+
+        private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(1);
+
+        private readonly Uri _key;
+
+        private readonly Uri _source;
+
+        public EmbeddedFontCollection(Uri key, Uri source)
+        {
+            _key = key;
+
+            if(!source.IsAvares() && !source.IsAbsoluteResm())
+            {
+                throw new ArgumentOutOfRangeException(nameof(source), "Specified source uri does not follow the resm: or avares: scheme.");
+            }
+
+            _source = source;
+        }
+
+        public Uri Key => _key;
+
+        public FontFamily this[int index] => _fontFamilies[index];
+
+        public int Count => _fontFamilies.Count;
+
+        public void Initialize(IFontManagerImpl fontManager)
+        {
+            var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
+
+            var fontAssets = FontFamilyLoader.LoadFontAssets(_source);
+
+            foreach (var fontAsset in fontAssets)
+            {
+                var stream = assetLoader.Open(fontAsset);
+
+                if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface))
+                {
+                    if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
+                    {
+                        glyphTypefaces = new Dictionary<FontCollectionKey, IGlyphTypeface>();
+
+                        _glyphTypefaceCache.Add(glyphTypeface.FamilyName, glyphTypefaces);
+
+                        _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName));
+                    }
+
+                    var key = new FontCollectionKey(
+                           glyphTypeface.Style,
+                           glyphTypeface.Weight,
+                           glyphTypeface.Stretch);
+
+                    if (!glyphTypefaces.ContainsKey(key))
+                    {
+                        glyphTypefaces.Add(key, glyphTypeface);
+                    }
+                }
+            }
+        }
+
+        public void Dispose()
+        {
+            foreach (var fontFamily in _fontFamilies)
+            {
+                if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces))
+                {
+                    foreach (var glyphTypeface in glyphTypefaces.Values)
+                    {
+                        glyphTypeface.Dispose();
+                    }
+                }
+            }
+
+            GC.SuppressFinalize(this);
+        }
+
+        public IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
+
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+        public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            var key = new FontCollectionKey(style, weight, stretch);
+
+            if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
+            {
+                if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
+                {
+                    return true;
+                }
+            }
+
+            //Try to find a partially matching font
+            for (var i = 0; i < Count; i++)
+            {
+                var fontFamily = _fontFamilies[i];
+
+                if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture)))
+                {
+                    if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
+                        TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            glyphTypeface = null;
+
+            return false;
+        }
+
+        private static bool TryGetNearestMatch(
+            Dictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
+            FontCollectionKey key,
+            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
+            {
+                return true;
+            }
+
+            if (key.Style != FontStyle.Normal)
+            {
+                key = key with { Style = FontStyle.Normal };
+            }
+
+            if (key.Stretch != FontStretch.Normal)
+            {
+                if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
+                {
+                    return true;
+                }
+
+                if (key.Weight != FontWeight.Normal)
+                {
+                    if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface))
+                    {
+                        return true;
+                    }
+                }
+
+                key = key with { Stretch = FontStretch.Normal };
+            }
+
+            if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface))
+            {
+                return true;
+            }
+
+            if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
+            {
+                return true;
+            }
+
+            //Take the first glyph typeface we can find.
+            foreach (var typeface in glyphTypefaces.Values)
+            {
+                glyphTypeface = typeface;
+
+                return true;
+            }
+
+            return false;
+        }
+
+        private static bool TryFindStretchFallback(
+            Dictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
+            FontCollectionKey key,
+            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            glyphTypeface = null;
+
+            var stretch = (int)key.Stretch;
+
+            if (stretch < 5)
+            {
+                for (var i = 0; stretch + i < 9; i++)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+            else
+            {
+                for (var i = 0; stretch - i > 1; i++)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        private static bool TryFindWeightFallback(
+            Dictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
+            FontCollectionKey key,
+            [NotNullWhen(true)] out IGlyphTypeface? typeface)
+        {
+            typeface = null;
+            var weight = (int)key.Weight;
+
+            //If the target weight given is between 400 and 500 inclusive          
+            if (weight >= 400 && weight <= 500)
+            {
+                //Look for available weights between the target and 500, in ascending order.
+                for (var i = 0; weight + i <= 500; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights less than the target, in descending order.
+                for (var i = 0; weight - i >= 100; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights greater than 500, in ascending order.
+                for (var i = 0; weight + i <= 900; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            //If a weight less than 400 is given, look for available weights less than the target, in descending order.           
+            if (weight < 400)
+            {
+                for (var i = 0; weight - i >= 100; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights less than the target, in descending order.
+                for (var i = 0; weight + i <= 900; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
+            if (weight > 500)
+            {
+                for (var i = 0; weight + i <= 900; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights less than the target, in descending order.
+                for (var i = 0; weight - i >= 100; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+    }
+}

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

@@ -0,0 +1,4 @@
+namespace Avalonia.Media.Fonts
+{
+    public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch);
+}

+ 24 - 34
src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs

@@ -11,22 +11,22 @@ namespace Avalonia.Media.Fonts
         /// <summary>
         /// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
         /// </summary>
-        /// <param name="fontFamilyKey"></param>
+        /// <param name="source"></param>
         /// <returns></returns>
-        public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey) =>
-            IsFontTtfOrOtf(fontFamilyKey.Source) ?
-                GetFontAssetsByExpression(fontFamilyKey) :
-                GetFontAssetsBySource(fontFamilyKey);
+        public static IEnumerable<Uri> LoadFontAssets(Uri source) =>
+            IsFontTtfOrOtf(source) ?
+                GetFontAssetsByExpression(source) :
+                GetFontAssetsBySource(source);
 
         /// <summary>
         /// Searches for font assets at a given location and returns a quantity of found assets
         /// </summary>
-        /// <param name="fontFamilyKey"></param>
+        /// <param name="source"></param>
         /// <returns></returns>
-        private static IEnumerable<Uri> GetFontAssetsBySource(FontFamilyKey fontFamilyKey)
+        private static IEnumerable<Uri> GetFontAssetsBySource(Uri source)
         {
             var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
-            var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
+            var availableAssets = assetLoader.GetAssets(source, null);
             return availableAssets.Where(x => IsFontTtfOrOtf(x));
         }
 
@@ -34,60 +34,50 @@ namespace Avalonia.Media.Fonts
         /// Searches for font assets at a given location and only accepts assets that fit to a given filename expression.
         /// <para>File names can target multiple files with * wildcard. For example "FontFile*.ttf"</para>
         /// </summary>
-        /// <param name="fontFamilyKey"></param>
+        /// <param name="source"></param>
         /// <returns></returns>
-        private static IEnumerable<Uri> GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
+        private static IEnumerable<Uri> GetFontAssetsByExpression(Uri source)
         {
-            var (fileNameWithoutExtension, extension) = GetFileName(fontFamilyKey, out var location);
-            var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension);
+            var (fileNameWithoutExtension, extension) = GetFileName(source, out var location);
+            var filePattern = CreateFilePattern(source, location, fileNameWithoutExtension);
 
             var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
-            var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
+            var availableResources = assetLoader.GetAssets(location, null);
 
             return availableResources.Where(x => IsContainsFile(x, filePattern, extension));
         }
 
         private static (string fileNameWithoutExtension, string extension) GetFileName(
-            FontFamilyKey fontFamilyKey, out Uri location)
+            Uri source, out Uri location)
         {
-            if (fontFamilyKey.Source.IsAbsoluteResm())
+            if (source.IsAbsoluteResm())
             {
-                var fileName = GetFileNameAndExtension(fontFamilyKey.Source.GetUnescapeAbsolutePath(), '.');
+                var fileName = GetFileNameAndExtension(source.GetUnescapeAbsolutePath(), '.');
 
-                var uriLocation = fontFamilyKey.Source.GetUnescapeAbsoluteUri()
+                var uriLocation = source.GetUnescapeAbsoluteUri()
                     .Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty);
                 location = new Uri(uriLocation, UriKind.RelativeOrAbsolute);
 
                 return fileName;
             }
 
-            var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString);
+            var filename = GetFileNameAndExtension(source.OriginalString);
             var fullFilename = filename.fileNameWithoutExtension + filename.extension;
 
-            if (fontFamilyKey.BaseUri != null)
-            {
-                var relativePath = fontFamilyKey.Source.OriginalString
-                    .Replace(fullFilename, string.Empty);
-
-                location = new Uri(fontFamilyKey.BaseUri, relativePath);
-            }
-            else
-            {
-                var uriString = fontFamilyKey.Source
-                    .GetUnescapeAbsoluteUri()
-                    .Replace(fullFilename, string.Empty);
-                location = new Uri(uriString);
-            }
+            var uriString = source
+                .GetUnescapeAbsoluteUri()
+                .Replace(fullFilename, string.Empty);
+            location = new Uri(uriString);
 
             return filename;
         }
 
         private static string CreateFilePattern(
-            FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension)
+            Uri source, Uri location, string fileNameWithoutExtension)
         {
             var path = location.GetUnescapeAbsolutePath();
             var file = GetSubString(fileNameWithoutExtension, '*');
-            return fontFamilyKey.Source.IsAbsoluteResm()
+            return source.IsAbsoluteResm()
                 ? path + "." + file
                 : path + file;
         }

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

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Platform;
+
+namespace Avalonia.Media.Fonts
+{
+    public interface IFontCollection : IReadOnlyList<FontFamily>, IDisposable
+    {
+        Uri Key { get; }
+
+        void Initialize(IFontManagerImpl fontManager);
+
+        bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
+    }
+}

+ 100 - 0
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@@ -0,0 +1,100 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Platform;
+
+namespace Avalonia.Media.Fonts
+{
+    internal class SystemFontCollection : IFontCollection
+    {
+        private readonly Dictionary<string, Dictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache =
+           new Dictionary<string, Dictionary<FontCollectionKey, IGlyphTypeface>>();
+
+        private readonly FontManager _fontManager;
+        private readonly string[] _familyNames;
+
+        public SystemFontCollection(FontManager fontManager)
+        {
+            _fontManager = fontManager;
+            _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames();
+        }
+
+        public Uri Key => new Uri("fontCollection:SystemFonts");
+
+        public FontFamily this[int index]
+        {
+            get
+            {
+                var familyName = _familyNames[index];
+
+                return new FontFamily(familyName);
+            }
+        }
+
+        public int Count => _familyNames.Length;
+
+        public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            if (familyName == FontFamily.DefaultFontFamilyName)
+            {
+                familyName = _fontManager.DefaultFontFamilyName;
+            }
+
+            var key = new FontCollectionKey(style, weight, stretch);
+
+            if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
+            {
+                glyphTypefaces = new Dictionary<FontCollectionKey, IGlyphTypeface>();
+
+                _glyphTypefaceCache.Add(familyName, glyphTypefaces);
+            }
+
+            if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
+            {
+                return true;
+            }
+
+            if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
+            {
+                glyphTypefaces.Add(key, glyphTypeface);
+
+                return true;
+            }
+
+            return false;
+        }
+
+        public void Initialize(IFontManagerImpl fontManager)
+        {
+            //We initialize the system font collection during construction.
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+
+        public IEnumerator<FontFamily> GetEnumerator()
+        {
+            foreach (var familyName in _familyNames)
+            {
+                yield return new FontFamily(familyName);
+            }
+        }
+
+        void IDisposable.Dispose()
+        {
+            foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
+            {
+                foreach (var pair in glyphTypefaces)
+                {
+                    pair.Value.Dispose();
+                }
+            }
+
+            GC.SuppressFinalize(this);
+        }
+    }
+}

+ 20 - 0
src/Avalonia.Base/Media/IGlyphTypeface.cs

@@ -6,6 +6,26 @@ namespace Avalonia.Media
     [Unstable]
     public interface IGlyphTypeface : IDisposable
     {
+        /// <summary>
+        /// Gets the family name for the <see cref="IGlyphTypeface"/> object.
+        /// </summary>
+        string FamilyName { get; }
+
+        /// <summary>
+        /// Gets the designed weight of the font represented by the <see cref="IGlyphTypeface"/> object.
+        /// </summary>
+        FontWeight Weight { get; }
+
+        /// <summary>
+        /// Gets the style for the <see cref="IGlyphTypeface"/> object.
+        /// </summary>
+        FontStyle Style { get; }
+
+        /// <summary>
+        /// Gets the <see cref="FontStretch"/> value for the <see cref="IGlyphTypeface"/> object.
+        /// </summary>
+        FontStretch Stretch { get; }
+
         /// <summary>
         ///     Gets the number of glyphs held by this glyph typeface. 
         /// </summary>

+ 8 - 7
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@@ -122,13 +122,14 @@ namespace Avalonia.Media.TextFormatting
             if (matchFound)
             {
                 // Fallback found
-                var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
-                                
-                if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
-                {                    
-                    return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
-                        biDiLevel);
-                }                
+                if(fontManager.TryGetGlyphTypeface(fallbackTypeface, out var fallbackGlyphTypeface))
+                {
+                    if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
+                    {
+                        return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
+                            biDiLevel);
+                    }
+                }          
             }
 
             // no fallback found

+ 12 - 1
src/Avalonia.Base/Media/Typeface.cs

@@ -80,7 +80,18 @@ namespace Avalonia.Media
         /// <value>
         /// The glyph typeface.
         /// </value>
-        public IGlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this);
+        public IGlyphTypeface GlyphTypeface
+        {
+            get
+            {
+                if(FontManager.Current.TryGetGlyphTypeface(this, out var glyphTypeface))
+                {
+                    return glyphTypeface;
+                }
+
+                throw new InvalidOperationException("Could not create glyphTypeface.");
+            }
+        }
 
         public static bool operator !=(Typeface a, Typeface b)
         {

+ 23 - 7
src/Avalonia.Base/Platform/IFontManagerImpl.cs

@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
+using System.IO;
 using Avalonia.Media;
 using Avalonia.Metadata;
 
@@ -17,7 +18,7 @@ namespace Avalonia.Platform
         ///     Get all installed fonts in the system.
         /// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
         /// </summary>
-        IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
+        string[] GetInstalledFontFamilyNames(bool checkForUpdates = false);
 
         /// <summary>
         ///     Tries to match a specified character to a typeface that supports specified font properties.
@@ -37,12 +38,27 @@ namespace Avalonia.Platform
             FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface);
 
         /// <summary>
-        ///     Creates a glyph typeface.
+        ///     Tries to get a glyph typeface for specified parameters.
         /// </summary>
-        /// <param name="typeface">The typeface.</param>
-        /// <returns>0
-        ///     The created glyph typeface. Can be <c>Null</c> if it was not possible to create a glyph typeface.
+        /// <param name="familyName">The family name.</param>
+        /// <param name="style">The font style.</param>
+        /// <param name="weight">The font weiht.</param>
+        /// <param name="stretch">The font stretch.</param>
+        /// <param name="glyphTypeface">The created glyphTypeface</param>
+        /// <returns>
+        ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
+        /// </returns>
+        bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+            FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
+
+        /// <summary>
+        ///     Tries to create a glyph typeface from specified stream.
+        /// </summary>
+        /// <param name="stream">A stream that holds the font's data.</param>
+        /// <param name="glyphTypeface">The created glyphTypeface</param>
+        /// <returns>
+        ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
         /// </returns>
-        IGlyphTypeface CreateGlyphTypeface(Typeface typeface);
+        bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
     }
 }

+ 4 - 1
src/Avalonia.Base/Utilities/UriExtensions.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Media;
 
 namespace Avalonia.Utilities;
 
@@ -10,7 +11,9 @@ internal static class UriExtensions
     public static bool IsResm(this Uri uri) => uri.Scheme == "resm";
     
     public static bool IsAvares(this Uri uri) => uri.Scheme == "avares";
-    
+
+    public static bool IsFontCollection(this Uri uri) => uri.Scheme == FontManager.FontCollectionScheme;
+
     public static Uri EnsureAbsolute(this Uri uri, Uri? baseUri)
     {
         if (uri.IsAbsoluteUri)

+ 35 - 11
src/Avalonia.Controls/AppBuilder.cs

@@ -4,6 +4,9 @@ using System.Reflection;
 using System.Linq;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Platform;
+using Avalonia.Media.Fonts;
+using Avalonia.Media;
+using System.Xml.Linq;
 
 namespace Avalonia
 {
@@ -16,7 +19,7 @@ namespace Avalonia
         private Action? _optionsInitializers;
         private Func<Application>? _appFactory;
         private IApplicationLifetime? _lifetime;
-        
+
         /// <summary>
         /// Gets or sets the <see cref="IRuntimePlatform"/> instance.
         /// </summary>
@@ -31,12 +34,12 @@ namespace Avalonia
         /// Gets the <see cref="Application"/> instance being initialized.
         /// </summary>
         public Application? Instance { get; private set; }
-        
+
         /// <summary>
         /// Gets the type of the Instance (even if it's not created yet)
         /// </summary>
         public Type? ApplicationType { get; private set; }
-        
+
         /// <summary>
         /// Gets or sets a method to call the initialize the windowing subsystem.
         /// </summary>
@@ -64,7 +67,7 @@ namespace Avalonia
 
 
         public Action<AppBuilder> AfterPlatformServicesSetupCallback { get; private set; } = builder => { };
-        
+
         /// <summary>
         /// Initializes a new instance of the <see cref="AppBuilder"/> class.
         /// </summary>
@@ -73,7 +76,7 @@ namespace Avalonia
                 builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly))
         {
         }
-        
+
         /// <summary>
         /// Initializes a new instance of the <see cref="AppBuilder"/> class.
         /// </summary>
@@ -123,8 +126,8 @@ namespace Avalonia
             AfterSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterSetupCallback, callback);
             return Self;
         }
-        
-        
+
+
         public AppBuilder AfterPlatformServicesSetup(Action<AppBuilder> callback)
         {
             AfterPlatformServicesSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterPlatformServicesSetupCallback, callback);
@@ -132,7 +135,7 @@ namespace Avalonia
         }
 
         public delegate void AppMainDelegate(Application app, string[] args);
-        
+
         public void Start(AppMainDelegate main, string[] args)
         {
             Setup();
@@ -160,7 +163,7 @@ namespace Avalonia
             Setup();
             return Self;
         }
-        
+
         /// <summary>
         /// Specifies a windowing subsystem to use.
         /// </summary>
@@ -195,7 +198,7 @@ namespace Avalonia
             _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToConstant(options); };
             return Self;
         }
-        
+
         /// <summary>
         /// Configures platform-specific options
         /// </summary>
@@ -204,7 +207,28 @@ namespace Avalonia
             _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToFunc(options); };
             return Self;
         }
-        
+
+        /// <summary>
+        /// Registers a custom font collection with the font manager.
+        /// </summary>
+        /// <param name="fontCollection">The font collection.</param>
+        /// <returns>An <see cref="AppBuilder"/> instance.</returns>
+        /// <exception cref="ArgumentNullException"></exception>
+        public AppBuilder WithFonts(IFontCollection fontCollection)
+        {
+            if(fontCollection == null)
+            {
+                throw new ArgumentNullException(nameof(fontCollection), "Font collection can't be null.");
+            }
+
+            return AfterSetup(appBuilder =>
+            {
+                var fontManager = FontManager.Current;
+
+                fontManager.AddFontCollection(fontCollection);
+            });
+        }
+
         /// <summary>
         /// Sets up the platform-specific services for the <see cref="Application"/>.
         /// </summary>

+ 14 - 0
src/Avalonia.Fonts.Inter/InterFontCollection.cs

@@ -0,0 +1,14 @@
+using System;
+using Avalonia.Media.Fonts;
+
+namespace Avalonia.Fonts.Inter
+{
+    public sealed class InterFontCollection : EmbeddedFontCollection
+    {
+        public InterFontCollection() : base(
+            new Uri("fonts:Inter", UriKind.Absolute), 
+            new Uri("avares://Avalonia.Fonts.Inter/Assets", UriKind.Absolute))
+        {
+        }
+    }
+}

+ 23 - 6
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@@ -84,6 +84,14 @@ namespace Avalonia.Headless
 
         public FontSimulations FontSimulations { get; }
 
+        public string FamilyName => "Arial";
+
+        public FontWeight Weight => FontWeight.Normal;
+
+        public FontStyle Style => FontStyle.Normal;
+
+        public FontStretch Stretch => FontStretch.Normal;
+
         public void Dispose()
         {
         }
@@ -147,19 +155,28 @@ namespace Avalonia.Headless
 
     class HeadlessFontManagerStub : IFontManagerImpl
     {
-        public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
+        public string GetDefaultFontFamilyName()
         {
-            return new HeadlessGlyphTypefaceImpl();
+            return "Arial";
         }
 
-        public string GetDefaultFontFamilyName()
+        public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
         {
-            return "Arial";
+            return new string[] { "Arial" };
         }
 
-        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
+        public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface)
         {
-            return new List<string> { "Arial" };
+            glyphTypeface= new HeadlessGlyphTypefaceImpl();
+
+            return true;
+        }
+
+        public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
+        {
+             glyphTypeface = new HeadlessGlyphTypefaceImpl();
+
+            return true;
         }
 
         public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,

+ 1 - 1
src/Avalonia.Themes.Fluent/Accents/Base.xaml

@@ -3,7 +3,7 @@
                     xmlns:sys="using:System"
                     xmlns:converters="using:Avalonia.Controls.Converters">
   <!-- https://docs.microsoft.com/en-us/previous-versions/windows/apps/dn518235(v=win.10)?redirectedfrom=MSDN -->
-  <FontFamily x:Key="ContentControlThemeFontFamily">avares://Avalonia.Fonts.Inter/Assets#Inter, $Default</FontFamily>
+  <FontFamily x:Key="ContentControlThemeFontFamily">fonts:Inter#Inter, $Default</FontFamily>
   <sys:Double x:Key="ControlContentThemeFontSize">14</sys:Double>
 
   <SolidColorBrush x:Key="SystemControlTransparentBrush" Color="Transparent" />

+ 36 - 49
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@@ -1,6 +1,7 @@
 using System;
-using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
+using System.IO;
 using Avalonia.Media;
 using Avalonia.Platform;
 using SkiaSharp;
@@ -16,14 +17,14 @@ namespace Avalonia.Skia
             return SKTypeface.Default.FamilyName;
         }
 
-        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
+        public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
         {
             if (checkForUpdates)
             {
                 _skFontManager = SKFontManager.CreateDefault();
             }
 
-            return _skFontManager.FontFamilies;
+            return _skFontManager.GetFontFamilies();
         }
 
         [ThreadStatic] private static string[]? t_languageTagBuffer;
@@ -95,72 +96,58 @@ namespace Avalonia.Skia
             return false;
         }
 
-        public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
+        public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, 
+            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
         {
-            SKTypeface? skTypeface = null;
+            glyphTypeface = null;
 
-            if(typeface.FontFamily.Key is not null)
-            {
-                var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
-
-                skTypeface = fontCollection.Get(typeface);
+            var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch,
+                (SKFontStyleSlant)style);
 
-                if (skTypeface is null && !typeface.FontFamily.FamilyNames.HasFallbacks)
-                {
-                    throw new InvalidOperationException(
-                        $"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
-                }
-            }
+            var skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
 
             if (skTypeface is null)
             {
-                var defaultName = SKTypeface.Default.FamilyName;
-
-                var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, (SKFontStyleWidth)typeface.Stretch,
-                    (SKFontStyleSlant)typeface.Style);
-
-                foreach (var familyName in typeface.FontFamily.FamilyNames)
-                {
-                    if(familyName == FontFamily.DefaultFontFamilyName)
-                    {
-                        continue;
-                    }
-
-                    skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
-
-                    if (skTypeface is null || defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal))
-                    {
-                        continue;
-                    }
-
-                    break;
-                }
-
-                // MatchTypeface can return "null" if matched typeface wasn't found for the style
-                // Fallback to the default typeface and styles instead.
-                skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle)
-                    ?? SKTypeface.Default;
+                return false;
             }
-           
-            if (skTypeface == null)
+
+            //MatchFamily can return a font other than we requested so we have to verify we got the expected.
+            if (!skTypeface.FamilyName.ToLower(CultureInfo.InvariantCulture).Equals(familyName.ToLower(CultureInfo.InvariantCulture), StringComparison.Ordinal))
             {
-                throw new InvalidOperationException(
-                    $"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
+                return false;
             }
 
             var fontSimulations = FontSimulations.None;
 
-            if((int)typeface.Weight >= 600 && !skTypeface.IsBold)
+            if ((int)weight >= 600 && !skTypeface.IsBold)
             {
                 fontSimulations |= FontSimulations.Bold;
             }
 
-            if(typeface.Style == FontStyle.Italic && !skTypeface.IsItalic)
+            if (style == FontStyle.Italic && !skTypeface.IsItalic)
             {
                 fontSimulations |= FontSimulations.Oblique;
             }
 
-            return new GlyphTypefaceImpl(skTypeface, fontSimulations);
+            glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);
+
+            return true;
+        }
+
+        public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            var skTypeface = SKTypeface.FromStream(stream);
+
+            if (skTypeface != null)
+            {
+                glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
+
+                return true;
+            }
+
+            glyphTypeface = null;
+
+            return false;
         }
     }
 }

+ 14 - 0
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@@ -51,6 +51,12 @@ namespace Avalonia.Skia
             GlyphCount = Typeface.GlyphCount;
 
             FontSimulations = fontSimulations;
+
+            Weight = (FontWeight)Typeface.FontWeight;
+
+            Style = Typeface.FontSlant.ToAvalonia();
+
+            Stretch = (FontStretch)Typeface.FontStyle.Width;
         }
 
         public Face Face { get; }
@@ -67,6 +73,14 @@ namespace Avalonia.Skia
 
         public int GlyphCount { get; }
 
+        public string FamilyName => Typeface.FamilyName;
+
+        public FontWeight Weight { get; }
+
+        public FontStyle Style { get; }
+
+        public FontStretch Stretch { get; }
+
         public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
         {
             metrics = default;

+ 0 - 198
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@@ -1,198 +0,0 @@
-using System.Collections.Concurrent;
-using System.Diagnostics.CodeAnalysis;
-using Avalonia.Media;
-using SkiaSharp;
-
-namespace Avalonia.Skia
-{
-    internal class SKTypefaceCollection
-    {
-        private readonly ConcurrentDictionary<Typeface, SKTypeface> _typefaces = new();
-
-        public void AddTypeface(Typeface key, SKTypeface typeface)
-        {
-            _typefaces.TryAdd(key, typeface);
-        }
-
-        public SKTypeface? Get(Typeface typeface)
-        {
-            return GetNearestMatch(typeface);
-        }
-
-        private SKTypeface? GetNearestMatch(Typeface key)
-        {
-            if (_typefaces.Count == 0)
-            {
-                return null;
-            }
-            
-            if (_typefaces.TryGetValue(key, out var typeface))
-            {
-                return typeface;
-            }
-
-            if(key.Style != FontStyle.Normal)
-            {
-                key = new Typeface(key.FontFamily, FontStyle.Normal, key.Weight, key.Stretch);
-            }
-
-            if(key.Stretch != FontStretch.Normal)
-            {
-                if(TryFindStretchFallback(key, out typeface))
-                {
-                    return typeface;
-                }
-                
-                if(key.Weight != FontWeight.Normal)
-                {
-                    if (TryFindStretchFallback(new Typeface(key.FontFamily, key.Style, FontWeight.Normal, key.Stretch), out typeface))
-                    {
-                        return typeface;
-                    }
-                }
-
-                key = new Typeface(key.FontFamily, key.Style, key.Weight, FontStretch.Normal);
-            }
-
-            if(TryFindWeightFallback(key, out typeface))
-            {
-                return typeface;
-            }
-
-            if (TryFindStretchFallback(key, out typeface))
-            {
-                return typeface;
-            }
-
-            //Nothing was found so we try some regular typeface.
-            if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface))
-            {
-                return typeface;
-            }
-
-            SKTypeface? skTypeface = null;
-
-            foreach(var pair in _typefaces)
-            {
-                skTypeface = pair.Value;
-
-                if (skTypeface.FamilyName.Contains(key.FontFamily.Name))
-                {
-                    return skTypeface;
-                }
-            }
-
-            return skTypeface;
-        }
-
-        private bool TryFindStretchFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface)
-        {
-            typeface = null;
-            var stretch = (int)key.Stretch;
-
-            if (stretch < 5)
-            {
-                for (var i = 0; stretch + i < 9; i++)
-                {
-                    if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch + i)), out typeface))
-                    {
-                        return true;
-                    }
-                }
-            }
-            else
-            {
-                for (var i = 0; stretch - i > 1; i++)
-                {
-                    if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch - i)), out typeface))
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            return false;
-        }
-
-        private bool TryFindWeightFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface)
-        {
-            typeface = null;
-            var weight = (int)key.Weight;
-
-            //If the target weight given is between 400 and 500 inclusive          
-            if (weight >= 400 && weight <= 500)
-            {
-                //Look for available weights between the target and 500, in ascending order.
-                for (var i = 0; weight + i <= 500; i += 50)
-                {
-                    if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
-                    {
-                        return true;
-                    }
-                }
-
-                //If no match is found, look for available weights less than the target, in descending order.
-                for (var i = 0; weight - i >= 100; i += 50)
-                {
-                    if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
-                    {
-                        return true;
-                    }
-                }
-
-                //If no match is found, look for available weights greater than 500, in ascending order.
-                for (var i = 0; weight + i <= 900; i += 50)
-                {
-                    if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            //If a weight less than 400 is given, look for available weights less than the target, in descending order.           
-            if (weight < 400)
-            {
-                for (var i = 0; weight - i >= 100; i += 50)
-                {
-                    if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
-                    {
-                        return true;
-                    }
-                }
-
-                //If no match is found, look for available weights less than the target, in descending order.
-                for (var i = 0; weight + i <= 900; i += 50)
-                {
-                    if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
-            if (weight > 500)
-            {
-                for (var i = 0; weight + i <= 900; i += 50)
-                {
-                    if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
-                    {
-                        return true;
-                    }
-                }
-
-                //If no match is found, look for available weights less than the target, in descending order.
-                for (var i = 0; weight - i >= 100; i += 50)
-                {
-                    if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            return false;
-        }
-    }
-}

+ 0 - 73
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@@ -1,73 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using Avalonia.Media;
-using Avalonia.Media.Fonts;
-using Avalonia.Platform;
-using SkiaSharp;
-
-namespace Avalonia.Skia
-{
-    internal static class SKTypefaceCollectionCache
-    {
-        private static readonly ConcurrentDictionary<FontFamily, SKTypefaceCollection> s_cachedCollections;
-
-        static SKTypefaceCollectionCache()
-        {
-            s_cachedCollections = new ConcurrentDictionary<FontFamily, SKTypefaceCollection>();
-        }
-
-        /// <summary>
-        /// Gets the or add typeface collection.
-        /// </summary>
-        /// <param name="fontFamily">The font family.</param>
-        /// <returns></returns>
-        public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily)
-        {
-            return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection);
-        }
-
-        /// <summary>
-        /// Creates the custom font collection.
-        /// </summary>
-        /// <param name="fontFamily">The font family.</param>
-        /// <returns></returns>
-        private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily)
-        {
-            var typeFaceCollection = new SKTypefaceCollection();
-
-            if (fontFamily.Key is not { } fontFamilyKey)
-            {
-                return typeFaceCollection;
-            }
-
-            var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamilyKey);
-
-            var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
-
-            foreach (var asset in fontAssets)
-            {
-                var assetStream = assetLoader.Open(asset);
-
-                if (assetStream == null)
-                    throw new InvalidOperationException("Asset could not be loaded.");
-
-                var typeface = SKTypeface.FromStream(assetStream);
-
-                if (typeface == null)
-                    throw new InvalidOperationException("Typeface could not be loaded.");
-
-                if (!typeface.FamilyName.Contains(fontFamily.Name))
-                {
-                    continue;
-                }
-
-                var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(),
-                    (FontWeight)typeface.FontWeight, (FontStretch)typeface.FontWidth);
-
-                typeFaceCollection.AddTypeface(key, typeface);
-            }
-
-            return typeFaceCollection;
-        }
-    }
-}

+ 4 - 9
src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs

@@ -1,11 +1,10 @@
 using System.Collections.Generic;
-using Avalonia.Platform;
 using SharpDX;
 using SharpDX.DirectWrite;
 
 namespace Avalonia.Direct2D1.Media
 {
-    using System;
+    using System.IO;
 
     internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader
     {
@@ -18,19 +17,15 @@ namespace Avalonia.Direct2D1.Media
         /// </summary>
         /// <param name="factory">The factory.</param>
         /// <param name="fontAssets"></param>
-        public DWriteResourceFontLoader(Factory factory, IEnumerable<Uri> fontAssets)
+        public DWriteResourceFontLoader(Factory factory, Stream[] fontAssets)
         {
             var factory1 = factory;
 
-            var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
-
             foreach (var asset in fontAssets)
             {
-                var assetStream = assetLoader.Open(asset);
-
-                var dataStream = new DataStream((int)assetStream.Length, true, true);
+                var dataStream = new DataStream((int)asset.Length, true, true);
 
-                assetStream.CopyTo(dataStream);
+                asset.CopyTo(dataStream);
 
                 dataStream.Position = 0;
 

+ 11 - 2
src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs

@@ -6,6 +6,9 @@ using FontFamily = Avalonia.Media.FontFamily;
 using FontStyle = SharpDX.DirectWrite.FontStyle;
 using FontWeight = SharpDX.DirectWrite.FontWeight;
 using FontStretch = SharpDX.DirectWrite.FontStretch;
+using Avalonia.Platform;
+using System.Linq;
+using System;
 
 namespace Avalonia.Direct2D1.Media
 {
@@ -53,9 +56,15 @@ namespace Avalonia.Direct2D1.Media
 
         private static FontCollection CreateFontCollection(FontFamilyKey key)
         {
-            var assets = FontFamilyLoader.LoadFontAssets(key);
+            var source = key.BaseUri != null ? new Uri(key.BaseUri, key.Source) : key.Source;
 
-            var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets);
+            var assets = FontFamilyLoader.LoadFontAssets(source);
+
+            var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
+
+            var fontAssets = assets.Select(x => assetLoader.Open(x)).ToArray();
+
+            var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, fontAssets);
 
             return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
         }

+ 52 - 5
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@@ -1,8 +1,8 @@
-using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
+using System.IO;
 using Avalonia.Media;
 using Avalonia.Platform;
-using SharpDX.DirectWrite;
 using FontFamily = Avalonia.Media.FontFamily;
 using FontStretch = Avalonia.Media.FontStretch;
 using FontStyle = Avalonia.Media.FontStyle;
@@ -18,7 +18,7 @@ namespace Avalonia.Direct2D1.Media
             return "Segoe UI";
         }
 
-        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
+        public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
         {
             var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
 
@@ -62,9 +62,56 @@ namespace Avalonia.Direct2D1.Media
             return false;
         }
 
-        public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
+        public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
         {
-            return new GlyphTypefaceImpl(typeface);
+            var systemFonts = Direct2D1FontCollectionCache.InstalledFontCollection;
+
+            if (familyName == FontFamily.DefaultFontFamilyName)
+            {
+                familyName = "Segoe UI";
+            }
+
+            if (systemFonts.FindFamilyName(familyName, out var index))
+            {
+                var font = systemFonts.GetFontFamily(index).GetFirstMatchingFont(
+                    (SharpDX.DirectWrite.FontWeight)weight,
+                    (SharpDX.DirectWrite.FontStretch)stretch,
+                    (SharpDX.DirectWrite.FontStyle)style);
+
+                glyphTypeface = new GlyphTypefaceImpl(font);
+
+                return true;
+            }
+
+            glyphTypeface = null;
+
+            return false;
+        }
+
+        public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
+        {
+            var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, new[] { stream });
+
+            var fontCollection = new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
+
+            if (fontCollection.FontFamilyCount > 0)
+            {
+                var fontFamily = fontCollection.GetFontFamily(0);
+
+                if (fontFamily.FontCount > 0)
+                {
+                    var font = fontFamily.GetFont(0);
+
+                    glyphTypeface = new GlyphTypefaceImpl(font);
+
+                    return true;
+                }
+            }
+
+            glyphTypeface = null;
+
+            return false;
         }
     }
 }

+ 18 - 2
src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs

@@ -12,9 +12,9 @@ namespace Avalonia.Direct2D1.Media
     {
         private bool _isDisposed;
 
-        public GlyphTypefaceImpl(Typeface typeface)
+        public GlyphTypefaceImpl(SharpDX.DirectWrite.Font font)
         {
-            DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
+            DWFont = font;
 
             FontFace = new FontFace(DWFont).QueryInterface<FontFace1>();
 
@@ -48,6 +48,14 @@ namespace Avalonia.Direct2D1.Media
                 StrikethroughThickness = strikethroughThickness,
                 IsFixedPitch = FontFace.IsMonospacedFont
             };
+
+            FamilyName = DWFont.FontFamily.FamilyNames.GetString(0);
+
+            Weight = (Avalonia.Media.FontWeight)DWFont.Weight;
+
+            Style = (Avalonia.Media.FontStyle)DWFont.Style;
+
+            Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch;
         }
 
         private Blob GetTable(Face face, Tag tag)
@@ -83,6 +91,14 @@ namespace Avalonia.Direct2D1.Media
 
         public FontSimulations FontSimulations => FontSimulations.None;
 
+        public string FamilyName { get; }
+
+        public Avalonia.Media.FontWeight Weight { get; }
+
+        public Avalonia.Media.FontStyle Style { get; }
+
+        public Avalonia.Media.FontStretch Stretch { get; }
+
         /// <inheritdoc cref="IGlyphTypeface"/>
         public ushort GetGlyph(uint codepoint)
         {

+ 4 - 2
tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs

@@ -16,9 +16,11 @@ namespace Avalonia.Base.UnitTests.Media
 
                 var typeface = new Typeface(fontFamily);
 
-                var glyphTypeface = FontManager.Current.GetOrAddGlyphTypeface(typeface);
+                Assert.True(FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface));
 
-                Assert.Same(glyphTypeface, FontManager.Current.GetOrAddGlyphTypeface(typeface));
+                FontManager.Current.TryGetGlyphTypeface(typeface, out var other);
+
+                Assert.Same(glyphTypeface, other);
             }
         }
 

+ 6 - 10
tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs

@@ -46,9 +46,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
         public void Should_Load_Single_FontAsset()
         {
             var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute);
-            var key = new FontFamilyKey(source);
 
-            var fontAssets = FontFamilyLoader.LoadFontAssets(key);
+            var fontAssets = FontFamilyLoader.LoadFontAssets(source);
 
             Assert.Single(fontAssets);
         }
@@ -57,9 +56,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
         public void Should_Load_Single_FontAsset_Avares_Without_BaseUri()
         {
             var source = new Uri(AssetYourFontAvares);
-            var key = new FontFamilyKey(source);
 
-            var fontAssets = FontFamilyLoader.LoadFontAssets(key);
+            var fontAssets = FontFamilyLoader.LoadFontAssets(source);
 
             Assert.Single(fontAssets);
         }
@@ -69,9 +67,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
         {
             var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute);
             var baseUri = new Uri(AssetLocationAvares);
-            var key = new FontFamilyKey(source, baseUri);
 
-            var fontAssets = FontFamilyLoader.LoadFontAssets(key);
+            var fontAssets = FontFamilyLoader.LoadFontAssets(new Uri(baseUri, source));
 
             Assert.Single(fontAssets);
         }
@@ -80,9 +77,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
         public void Should_Load_Matching_Assets()
         {
             var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute);
-            var key = new FontFamilyKey(source);
 
-            var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray();
+            var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray();
 
             foreach (var fontAsset in fontAssets)
             {
@@ -99,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
             {
                 var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
 
-                var fontFamily = new FontFamily("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono");
+                var source = new Uri("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono", UriKind.RelativeOrAbsolute);
 
-                var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key).ToArray();
+                var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray();
 
                 Assert.NotEmpty(fontAssets);
 

+ 19 - 47
tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs

@@ -1,4 +1,5 @@
-using Avalonia.Direct2D1.Media;
+using System;
+using Avalonia.Direct2D1.Media;
 using Avalonia.Media;
 using Avalonia.UnitTests;
 using Xunit;
@@ -16,18 +17,10 @@ namespace Avalonia.Direct2D1.UnitTests.Media
             {
                 Direct2D1Platform.Initialize();
 
-                var fontManager = new FontManagerImpl();
-
-                var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
-                    new Typeface(new FontFamily("A, B, Arial")));
-
-                var font = glyphTypeface.DWFont;
-
-                Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0));
-
-                Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
+                var glyphTypeface =
+                    new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface;
 
-                Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
+                Assert.Equal("Arial", glyphTypeface.FamilyName);
             }
         }
 
@@ -38,42 +31,29 @@ namespace Avalonia.Direct2D1.UnitTests.Media
             {
                 Direct2D1Platform.Initialize();
 
-                var fontManager = new FontManagerImpl();
-
-                var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
-                    new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold));
+                var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
 
-                var font = glyphTypeface.DWFont;
+                Assert.Equal("Arial", glyphTypeface.FamilyName);
 
-                Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0));
+                Assert.Equal(FontWeight.Bold, glyphTypeface.Weight);
 
-                Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight);
-
-                Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
+                Assert.Equal(FontStyle.Normal, glyphTypeface.Style);
             }
         }
 
         [Fact]
-        public void Should_Create_Typeface_For_Unknown_Font()
+        public void Should_Throw_InvalidOperationException_For_Unknown_Font()
         {
             using (AvaloniaLocator.EnterScope())
             {
                 Direct2D1Platform.Initialize();
 
-                var fontManager = new FontManagerImpl();
-
-                var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
-                    new Typeface(new FontFamily("Unknown")));
-
-                var font = glyphTypeface.DWFont;
-
-                var defaultName = fontManager.GetDefaultFontFamilyName();
-
-                Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0));
-
-                Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
+                var fontManager = FontManager.Current;
 
-                Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
+                Assert.Throws<InvalidOperationException>(() =>
+                {
+                    var glyphTypeface =new Typeface(new FontFamily("Unknown")).GlyphTypeface;
+                });
             }
         }
 
@@ -86,12 +66,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
 
                 var fontManager = new FontManagerImpl();
 
-                var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
-                    new Typeface(s_fontUri));
+                var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface;
 
-                var font = glyphTypeface.DWFont;
-
-                Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
+                Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
             }
         }
 
@@ -102,14 +79,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
             {
                 Direct2D1Platform.Initialize();
 
-                var fontManager = new FontManagerImpl();
-
-                var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
-                    new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
-
-                var font = glyphTypeface.DWFont;
+                var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface;
 
-                Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
+                Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
             }
         }
     }

+ 101 - 15
tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs

@@ -1,10 +1,12 @@
-using System.Collections.Generic;
+using System;
 using System.Globalization;
 using System.Linq;
 using Avalonia.Media;
 using Avalonia.Media.Fonts;
 using Avalonia.Platform;
 using SkiaSharp;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
 
 namespace Avalonia.Skia.UnitTests.Media
 {
@@ -35,9 +37,9 @@ namespace Avalonia.Skia.UnitTests.Media
             return _defaultFamilyName;
         }
 
-        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
+        public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
         {
-            return _customTypefaces.Select(x => x.FontFamily.Name);
+            return _customTypefaces.Select(x => x.FontFamily.Name).ToArray();
         }
 
         private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
@@ -70,48 +72,132 @@ namespace Avalonia.Skia.UnitTests.Media
         {
             SKTypeface skTypeface;
 
+            Uri source = null;
+
             switch (typeface.FontFamily.Name)
             {
                 case "Twitter Color Emoji":
                     {
-                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily);
-                        skTypeface = typefaceCollection.Get(typeface);
+                        source = _emojiTypeface.FontFamily.Key.Source;
                         break;
                     }
                 case "Noto Sans":
                     {
-                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
-                        skTypeface = typefaceCollection.Get(typeface);
+                        source = _italicTypeface.FontFamily.Key.Source;
                         break;
                     }
                 case "Noto Sans Arabic":
                     {
-                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_arabicTypeface.FontFamily);
-                        skTypeface = typefaceCollection.Get(typeface);
+                        source = _arabicTypeface.FontFamily.Key.Source;
                         break;
                     }
                 case "Noto Sans Hebrew":
                     {
-                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_hebrewTypeface.FontFamily);
-                        skTypeface = typefaceCollection.Get(typeface);
+                        source = _hebrewTypeface.FontFamily.Key.Source;
                         break;
                     }
                 case FontFamily.DefaultFontFamilyName:
                 case "Noto Mono":
                     {
-                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);
-                        skTypeface = typefaceCollection.Get(_defaultTypeface);
+                        source = _defaultTypeface.FontFamily.Key.Source;
                         break;
                     }
                 default:
                     {
-                        skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
-                            (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
+
                         break;
                     }
             }
 
+            if (source is null)
+            {
+                skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
+                           (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
+            }
+            else
+            {
+                var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
+
+                var assetUri = FontFamilyLoader.LoadFontAssets(source).First();
+
+                var stream = assetLoader.Open(assetUri);
+
+                skTypeface = SKTypeface.FromStream(stream);
+            }
+
             return new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
         }
+
+        public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
+        {
+            SKTypeface skTypeface;
+
+            Uri source = null;
+
+            switch (familyName)
+            {
+                case "Twitter Color Emoji":
+                    {
+                        source = _emojiTypeface.FontFamily.Key.Source;
+                        break;
+                    }
+                case "Noto Sans":
+                    {
+                        source = _italicTypeface.FontFamily.Key.Source;
+                        break;
+                    }
+                case "Noto Sans Arabic":
+                    {
+                        source = _arabicTypeface.FontFamily.Key.Source;
+                        break;
+                    }
+                case "Noto Sans Hebrew":
+                    {
+                        source = _hebrewTypeface.FontFamily.Key.Source;
+                        break;
+                    }
+                case FontFamily.DefaultFontFamilyName:
+                case "Noto Mono":
+                    {
+                        source = _defaultTypeface.FontFamily.Key.Source;
+                        break;
+                    }
+                default:
+                    {
+
+                        break;
+                    }
+            }
+
+            if (source is null)
+            {
+                skTypeface = SKTypeface.FromFamilyName(familyName,
+                           (SKFontStyleWeight)weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)style);
+            }
+            else
+            {
+                var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
+
+                var assetUri = FontFamilyLoader.LoadFontAssets(source).First();
+
+                var stream = assetLoader.Open(assetUri);
+
+                skTypeface = SKTypeface.FromStream(stream);
+            }
+
+            glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
+
+            return true;
+        }
+
+        public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
+        {
+            var skTypeface = SKTypeface.FromStream(stream);
+
+            glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
+
+            return true;
+        }
     }
 }

+ 58 - 0
tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs

@@ -0,0 +1,58 @@
+using System;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Skia.UnitTests.Media
+{
+    public class EmbeddedFontCollectionTests
+    {
+        private const string s_notoMono =
+            "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono";
+        
+        [InlineData(FontWeight.SemiLight, FontStyle.Normal)]
+        [InlineData(FontWeight.Bold, FontStyle.Italic)]
+        [InlineData(FontWeight.Heavy, FontStyle.Oblique)]
+        [Theory]
+        public void Should_Get_Near_Matching_Typeface(FontWeight fontWeight, FontStyle fontStyle)
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
+            {
+                var fontCollection = new EmbeddedFontCollection(FontManager.Current, new Uri(s_notoMono));
+
+                Assert.True(fontCollection.TryGetGlyphTypeface("Noto Mono", fontStyle, fontWeight, FontStretch.Normal, out var glyphTypeface));
+
+                var actual = glyphTypeface?.FamilyName;
+
+                Assert.Equal("Noto Mono", actual);
+            }
+        }
+        
+        [Fact]
+        public void Should_Not_Get_Typeface_For_Invalid_FamilyName()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
+            {
+                var fontCollection = new EmbeddedFontCollection(FontManager.Current, new Uri(s_notoMono));
+
+                Assert.False(fontCollection.TryGetGlyphTypeface("ABC", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface));
+            }
+        }
+
+        [Fact]
+        public void Should_Get_Typeface_For_Partial_FamilyName()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
+            {
+                var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T", UriKind.Absolute);
+
+                var fontCollection = new EmbeddedFontCollection(FontManager.Current, source);
+
+                Assert.True(fontCollection.TryGetGlyphTypeface("T", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface));
+
+                Assert.Equal("Twitter Color Emoji", glyphTypeface.FamilyName);
+            }
+        }
+    }
+}

+ 27 - 52
tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs

@@ -14,92 +14,67 @@ namespace Avalonia.Skia.UnitTests.Media
         [Fact]
         public void Should_Create_Typeface_From_Fallback()
         {
-            var fontManager = new FontManagerImpl();
-
-            var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
-                new Typeface(new FontFamily("A, B, " + fontManager.GetDefaultFontFamilyName())));
-
-            var skTypeface = glyphTypeface.Typeface;
-
-            Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+            {
+                var fontManager = FontManager.Current;
 
-            Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
+                var glyphTypeface = new Typeface(new FontFamily("A, B, " + fontManager.DefaultFontFamilyName)).GlyphTypeface;
 
-            Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
+                Assert.Equal(SKTypeface.Default.FamilyName, glyphTypeface.FamilyName);
+            }
         }
 
         [Fact]
         public void Should_Create_Typeface_From_Fallback_Bold()
         {
-            var fontManager = new FontManagerImpl();
-
-            var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
-                new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold));
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+            {
+                var glyphTypeface = new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
 
-            var skTypeface = glyphTypeface.Typeface;
-            
-            Assert.True(skTypeface.FontWeight >= 600);
+                Assert.True((int)glyphTypeface.Weight >= 600);
+            }
         }
 
         [Fact]
-        public void Should_Create_Typeface_For_Unknown_Font()
+        public void Should_Throw_InvalidOperationException_For_Invalid_FamilyName()
         {
-            var fontManager = new FontManagerImpl();
-
-            var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
-                new Typeface(new FontFamily("Unknown")));
-
-            var skTypeface = glyphTypeface.Typeface;
-
-            Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
-
-            Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
-
-            Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+            {
+                Assert.Throws<InvalidOperationException>(() =>
+                {
+                    var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface;
+                });
+            }
         }
 
         [Fact]
         public void Should_Load_Typeface_From_Resource()
         {
-            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
             {
-                var fontManager = new FontManagerImpl();
-
-                var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
-                    new Typeface(s_fontUri));
+                var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface;
 
-                var skTypeface = glyphTypeface.Typeface;
-
-                Assert.Equal("Noto Mono", skTypeface.FamilyName);
+                Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
             }
         }
 
         [Fact]
         public void Should_Load_Nearest_Matching_Font()
         {
-            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
             {
-                var fontManager = new FontManagerImpl();
-
-                var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
-                    new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
-
-                var skTypeface = glyphTypeface.Typeface;
+                var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface;
 
-                Assert.Equal("Noto Mono", skTypeface.FamilyName);
+                Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
             }
         }
 
         [Fact]
         public void Should_Throw_For_Invalid_Custom_Font()
         {
-            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
             {
-                var fontManager = new FontManagerImpl();
-
-                Assert.Throws<InvalidOperationException>(() =>
-                    fontManager.CreateGlyphTypeface(
-                        new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown")));
+                Assert.Throws<InvalidOperationException>(() => new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown").GlyphTypeface);
             }
         }
     }

+ 0 - 63
tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs

@@ -1,63 +0,0 @@
-using Avalonia.Media;
-using Avalonia.UnitTests;
-using Xunit;
-
-namespace Avalonia.Skia.UnitTests.Media
-{
-    public class SKTypefaceCollectionCacheTests
-    {
-        private const string s_notoMono =
-            "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono";
-        
-        [InlineData(s_notoMono, FontWeight.SemiLight, FontStyle.Normal)]
-        [InlineData(s_notoMono, FontWeight.Bold, FontStyle.Italic)]
-        [InlineData(s_notoMono, FontWeight.Heavy, FontStyle.Oblique)]
-        [Theory]
-        public void Should_Get_Near_Matching_Typeface(string familyName, FontWeight fontWeight, FontStyle fontStyle)
-        {
-            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
-            {
-                var fontFamily = new FontFamily(familyName);
-                
-                var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily);
-
-                var actual = typefaceCollection.Get(new Typeface(fontFamily, fontStyle, fontWeight))?.FamilyName;
-                
-                Assert.Equal("Noto Mono", actual);
-            }
-        }
-        
-        [Fact]
-        public void Should_Get_Typeface_For_Invalid_FamilyName()
-        {
-            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
-            {
-                var notoMono =
-                    new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
-                
-                var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono);
-
-                var typeface = notoMonoCollection.Get(new Typeface("ABC"));
-                
-                Assert.NotNull(typeface);
-            }
-        }
-
-        [Fact]
-        public void Should_Get_Typeface_For_Partial_FamilyName()
-        {
-            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
-            {
-                var fontFamily = new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T");
-
-                var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily);
-
-                var typeface = fontCollection.Get(new Typeface(fontFamily));
-
-                Assert.NotNull(typeface);
-
-                Assert.Equal("Twitter Color Emoji", typeface.FamilyName);
-            }
-        }
-    }
-}

+ 13 - 24
tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

@@ -1,9 +1,8 @@
-using System;
-using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
+using System.IO;
 using System.Linq;
 using Avalonia.Media;
-using Avalonia.Media.Fonts;
 using Avalonia.Platform;
 
 namespace Avalonia.UnitTests
@@ -31,9 +30,9 @@ namespace Avalonia.UnitTests
             return _defaultFamilyName;
         }
 
-        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
+        string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
         {
-            return _customTypefaces.Select(x => x.FontFamily!.Name);
+            return _customTypefaces.Select(x => x.FontFamily!.Name).ToArray();
         }
 
         public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,
@@ -58,29 +57,19 @@ namespace Avalonia.UnitTests
             return false;
         }
 
-        public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
+        public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
         {
-            var fontFamily = typeface.FontFamily;
+            glyphTypeface = new HarfBuzzGlyphTypefaceImpl(stream);
 
-            if (fontFamily.IsDefault)
-            {
-                fontFamily = _defaultTypeface.FontFamily;
-            }
-            
-            if (fontFamily!.Key == null)
-            {
-                return null;
-            }
-            
-            var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key);
+            return true;
+        }
 
-            var asset = fontAssets.First();
-            
-            var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
+        public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, 
+            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
+        {
+            glyphTypeface = null;
 
-            var stream = assetLoader.Open(asset);
-            
-            return new HarfBuzzGlyphTypefaceImpl(stream);
+            return false;
         }
     }
 }

+ 9 - 0
tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs

@@ -57,6 +57,15 @@ namespace Avalonia.UnitTests
 
         public FontSimulations FontSimulations { get; }
 
+        public string FamilyName => "$Default";
+
+        public FontWeight Weight { get; }
+
+        public FontStyle Style { get; }
+
+        public FontStretch Stretch { get; }
+
+
         /// <inheritdoc cref="IGlyphTypeface"/>
         public ushort GetGlyph(uint codepoint)
         {

+ 15 - 5
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
+using System.IO;
 using Avalonia.Media;
 using Avalonia.Platform;
 
@@ -19,12 +20,12 @@ namespace Avalonia.UnitTests
             return _defaultFamilyName;
         }
 
-        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
+        string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
         {
             return new[] { _defaultFamilyName };
         }
 
-        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, 
+        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
             FontStretch fontStretch, FontFamily fontFamily,
             CultureInfo culture, out Typeface fontKey)
         {
@@ -33,9 +34,18 @@ namespace Avalonia.UnitTests
             return false;
         }
 
-        public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
+        public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
         {
-            return new MockGlyphTypeface();
+            glyphTypeface = new MockGlyphTypeface();
+
+            return true;
+        }
+
+        public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
+        {
+            glyphTypeface = new MockGlyphTypeface();
+
+            return true;
         }
     }
 }

+ 8 - 0
tests/Avalonia.UnitTests/MockGlyphTypeface.cs

@@ -17,6 +17,14 @@ namespace Avalonia.UnitTests
 
         public FontSimulations FontSimulations => throw new NotImplementedException();
 
+        public string FamilyName => "$Default";
+
+        public FontWeight Weight { get; }
+
+        public FontStyle Style { get; }
+
+        public FontStretch Stretch { get; }
+
         public ushort GetGlyph(uint codepoint)
         {
             return (ushort)codepoint;