From a4a96d6ff4da43b1e49635ecd518973038c87507 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 4 Apr 2023 13:51:27 +0200 Subject: [PATCH] Optimize FontManager caching --- src/Avalonia.Base/Media/FontManager.cs | 56 +++- .../Media/Fonts/EmbeddedFontCollection.cs | 206 +------------- .../Media/Fonts/FontCollectionBase.cs | 259 ++++++++++++++++++ .../Media/Fonts/IFontCollection.cs | 17 ++ .../Media/Fonts/SystemFontCollection.cs | 66 +---- .../Platform/IFontManagerImpl.cs | 4 +- .../HeadlessPlatformStubs.cs | 3 +- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 41 +-- .../Media/FontManagerImpl.cs | 3 +- .../Media/FontManagerTests.cs | 2 +- .../Media/FontManagerImplTests.cs | 2 +- .../Media/CustomFontManagerImpl.cs | 159 ++--------- .../Media/FontManagerTests.cs | 24 +- .../HarfBuzzFontManagerImpl.cs | 4 +- .../Avalonia.UnitTests/MockFontManagerImpl.cs | 18 +- 15 files changed, 419 insertions(+), 445 deletions(-) create mode 100644 src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 2e8d8e415d..4425147098 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -30,13 +30,15 @@ namespace Avalonia.Media _fontFallbacks = options?.FontFallbacks; - DefaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName(); + var defaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName(); - if (string.IsNullOrEmpty(DefaultFontFamilyName)) + if (string.IsNullOrEmpty(defaultFontFamilyName)) { throw new InvalidOperationException("Default font family name can't be null or empty."); } + DefaultFontFamily = new FontFamily(defaultFontFamilyName); + AddFontCollection(new SystemFontCollection(this)); } @@ -65,9 +67,9 @@ namespace Avalonia.Media } /// - /// Gets the system's default font family's name. + /// Gets the system's default font family. /// - public string DefaultFontFamilyName + public FontFamily DefaultFontFamily { get; } @@ -93,6 +95,11 @@ namespace Avalonia.Media var fontFamily = typeface.FontFamily; + if(typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName) + { + return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); + } + if (fontFamily.Key is FontFamilyKey key) { var source = key.Source; @@ -131,15 +138,21 @@ namespace Avalonia.Media } } - foreach (var familyName in fontFamily.FamilyNames) + for (var i = 0; i < fontFamily.FamilyNames.Count; i++) { + var familyName = fontFamily.FamilyNames[i]; + if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { - return true; + if (!fontFamily.FamilyNames.HasFallbacks || glyphTypeface.FamilyName != DefaultFontFamily.Name) + { + return true; + } } } - return TryGetGlyphTypeface(new Typeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); + //Nothing was found so use the default + return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); } /// @@ -199,16 +212,37 @@ namespace Avalonia.Media { foreach (var fallback in _fontFallbacks) { - typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); + if (fallback.UnicodeRange.IsInRange(codepoint)) + { + typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); + + if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { + return true; + } + } + } + } - if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + //Try to match against fallbacks first + if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks) + { + for (int i = 1; i < fontFamily.FamilyNames.Count; i++) + { + var familyName = fontFamily.FamilyNames[i]; + + foreach (var fontCollection in _fontCollections.Values) { - return true; + if (fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) + { + return true; + }; } } } - return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily, culture, out typeface); + //Try to find a match with the system font manager + return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface); } } } diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index f2fb490592..4d4751db02 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -8,10 +8,8 @@ using Avalonia.Platform; namespace Avalonia.Media.Fonts { - public class EmbeddedFontCollection : IFontCollection + public class EmbeddedFontCollection : FontCollectionBase { - private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); - private readonly List _fontFamilies = new List(1); private readonly Uri _key; @@ -25,13 +23,13 @@ namespace Avalonia.Media.Fonts _source = source; } - public Uri Key => _key; + public override Uri Key => _key; - public FontFamily this[int index] => _fontFamilies[index]; + public override FontFamily this[int index] => _fontFamilies[index]; - public int Count => _fontFamilies.Count; + public override int Count => _fontFamilies.Count; - public void Initialize(IFontManagerImpl fontManager) + public override void Initialize(IFontManagerImpl fontManager) { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); @@ -45,7 +43,7 @@ namespace Avalonia.Media.Fonts { if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) { - glyphTypefaces = new ConcurrentDictionary(); + glyphTypefaces = new ConcurrentDictionary(); if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces)) { @@ -63,27 +61,8 @@ namespace Avalonia.Media.Fonts } } - 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 GetEnumerator() => _fontFamilies.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { var key = new FontCollectionKey(style, weight, stretch); @@ -116,175 +95,6 @@ namespace Avalonia.Media.Fonts return false; } - private static bool TryGetNearestMatch( - ConcurrentDictionary 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( - ConcurrentDictionary 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( - ConcurrentDictionary 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; - } + public override IEnumerator GetEnumerator() => _fontFamilies.GetEnumerator(); } } diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs new file mode 100644 index 0000000000..713b3dafcd --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + public abstract class FontCollectionBase : IFontCollection + { + protected readonly ConcurrentDictionary> _glyphTypefaceCache = new(); + + public abstract Uri Key { get; } + + public abstract int Count { get; } + + public abstract FontFamily this[int index] { get; } + + public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + + public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, + string? familyName, CultureInfo? culture, out Typeface match) + { + match = default; + + if (string.IsNullOrEmpty(familyName)) + { + foreach (var typefaces in _glyphTypefaceCache.Values) + { + if (TryGetNearestMatch(typefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface)) + { + if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { + match = new Typeface(glyphTypeface.FamilyName, style, weight, stretch); + + return true; + } + } + } + } + else + { + if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface)) + { + if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { + match = new Typeface(familyName, style, weight, stretch); + + return true; + } + } + } + + return false; + } + + public abstract void Initialize(IFontManagerImpl fontManager); + + public abstract IEnumerator GetEnumerator(); + + void IDisposable.Dispose() + { + foreach (var glyphTypefaces in _glyphTypefaceCache.Values) + { + foreach (var pair in glyphTypefaces) + { + pair.Value?.Dispose(); + } + } + + GC.SuppressFinalize(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + internal static bool TryGetNearestMatch( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) + { + 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) + { + if(typeface != null) + { + glyphTypeface = typeface; + + return true; + } + } + + return false; + } + + internal static bool TryFindStretchFallback( + ConcurrentDictionary 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) && glyphTypeface != null) + { + return true; + } + } + } + else + { + for (var i = 0; stretch - i > 1; i++) + { + if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + + return false; + } + + internal static bool TryFindWeightFallback( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + glyphTypeface = 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 glyphTypeface) && glyphTypeface != null) + { + 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 glyphTypeface) && glyphTypeface != null) + { + 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 glyphTypeface) && glyphTypeface != null) + { + 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 glyphTypeface) && glyphTypeface != null) + { + 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 glyphTypeface) && glyphTypeface != null) + { + 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 glyphTypeface) && glyphTypeface != null) + { + 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 glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + + return false; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs index 814230bcf3..1a30f168f1 100644 --- a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Avalonia.Platform; namespace Avalonia.Media.Fonts @@ -29,5 +30,21 @@ namespace Avalonia.Media.Fonts /// Returns true if a glyph typface can be found; otherwise, false bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + + /// + /// Tries to match a specified character to a that supports specified font properties. + /// + /// The codepoint to match against. + /// The font style. + /// The font weight. + /// The font stretch. + /// The family name. This is optional and used for fallback lookup. + /// The culture. + /// The matching . + /// + /// True, if the could match the character to specified parameters, False otherwise. + /// + bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, + FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface); } } diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index fd332c6ebe..2f2948cb3e 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -7,10 +6,8 @@ using Avalonia.Platform; namespace Avalonia.Media.Fonts { - internal class SystemFontCollection : IFontCollection + internal class SystemFontCollection : FontCollectionBase { - private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); - private readonly FontManager _fontManager; private readonly string[] _familyNames; @@ -20,9 +17,9 @@ namespace Avalonia.Media.Fonts _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames(); } - public Uri Key => FontManager.SystemFontsKey; + public override Uri Key => FontManager.SystemFontsKey; - public FontFamily this[int index] + public override FontFamily this[int index] { get { @@ -32,76 +29,41 @@ namespace Avalonia.Media.Fonts } } - public int Count => _familyNames.Length; + public override int Count => _familyNames.Length; - public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - if (familyName == FontFamily.DefaultFontFamilyName) - { - familyName = _fontManager.DefaultFontFamilyName; - } + glyphTypeface = null; var key = new FontCollectionKey(style, weight, stretch); - if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) - { - if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) - { - return true; - } - else - { - if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) && - glyphTypefaces.TryAdd(key, glyphTypeface)) - { - return true; - } - } - } + var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName, (key) => new ConcurrentDictionary()); - if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + if (!glyphTypefaces.TryGetValue(key, out glyphTypeface)) { - glyphTypefaces = new ConcurrentDictionary(); + _fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface); - if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces)) + if (!glyphTypefaces.TryAdd(key, glyphTypeface)) { - return true; + return false; } } - return false; + return glyphTypeface != null; } - public void Initialize(IFontManagerImpl fontManager) + public override void Initialize(IFontManagerImpl fontManager) { //We initialize the system font collection during construction. } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public IEnumerator GetEnumerator() + public override IEnumerator 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); - } } } diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index 116f7cd6e2..222e7196bb 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -27,15 +27,13 @@ namespace Avalonia.Platform /// The font style. /// The font weight. /// The font stretch. - /// The font family. This is optional and used for fallback lookup. /// The culture. /// The matching typeface. /// /// True, if the could match the character to specified parameters, False otherwise. /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, - FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface); + FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface); /// /// Tries to get a glyph typeface for specified parameters. diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index ee4cd5af98..aa400ab3e6 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -179,8 +179,7 @@ namespace Avalonia.Headless return true; } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - FontFamily fontFamily, CultureInfo culture, out Typeface typeface) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, out Typeface typeface) { typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch); return true; diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 29e5687423..a97a198621 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -30,8 +30,7 @@ namespace Avalonia.Skia [ThreadStatic] private static string[]? t_languageTagBuffer; public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, - FontFamily? fontFamily, CultureInfo? culture, out Typeface fontKey) + FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface fontKey) { SKFontStyle skFontStyle; @@ -60,35 +59,13 @@ namespace Avalonia.Skia t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; - if (fontFamily is not null && fontFamily.FamilyNames.HasFallbacks) - { - var familyNames = fontFamily.FamilyNames; - - for (var i = 1; i < familyNames.Count; i++) - { - var skTypeface = - _skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint); - - if (skTypeface == null) - { - continue; - } + var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); - fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight, fontStretch); - - return true; - } - } - else + if (skTypeface != null) { - var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); + fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight, fontStretch); - if (skTypeface != null) - { - fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight, fontStretch); - - return true; - } + return true; } fontKey = default; @@ -96,7 +73,7 @@ namespace Avalonia.Skia return false; } - public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { glyphTypeface = null; @@ -111,12 +88,6 @@ namespace Avalonia.Skia return false; } - //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)) - { - return false; - } - var fontSimulations = FontSimulations.None; if ((int)weight >= 600 && !skTypeface.IsBold) diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index ec2f6385da..85bf2b6c4c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -33,8 +33,7 @@ namespace Avalonia.Direct2D1.Media } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, - FontFamily fontFamily, CultureInfo culture, out Typeface typeface) + FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, out Typeface typeface) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs index 89e609eb10..3ccec872d2 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs @@ -43,7 +43,7 @@ namespace Avalonia.Base.UnitTests.Media { AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); - Assert.Equal("MyFont", FontManager.Current.DefaultFontFamilyName); + Assert.Equal("MyFont", FontManager.Current.DefaultFontFamily.Name); } } diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index 14e48b3b6c..81ac9030bf 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -50,7 +50,7 @@ namespace Avalonia.Direct2D1.UnitTests.Media var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface; - var defaultName = FontManager.Current.DefaultFontFamilyName; + var defaultName = FontManager.Current.DefaultFontFamily.Name; Assert.Equal(defaultName, glyphTypeface.FamilyName); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index e18344580b..617ab952fa 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -12,24 +12,17 @@ namespace Avalonia.Skia.UnitTests.Media { public class CustomFontManagerImpl : IFontManagerImpl { - private readonly Typeface[] _customTypefaces; private readonly string _defaultFamilyName; - - private readonly Typeface _defaultTypeface = - new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); - private readonly Typeface _arabicTypeface = - new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Arabic"); - private readonly Typeface _hebrewTypeface = - new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Hebrew"); - private readonly Typeface _italicTypeface = - new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic); - private readonly Typeface _emojiTypeface = - new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji"); + private readonly IFontCollection _customFonts; + private bool _isInitialized; public CustomFontManagerImpl() { - _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _hebrewTypeface, _defaultTypeface }; - _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; + _defaultFamilyName = "Noto Mono"; + + var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"); + + _customFonts = new EmbeddedFontCollection(source, source); } public string GetDefaultFontFamilyName() @@ -39,28 +32,32 @@ namespace Avalonia.Skia.UnitTests.Media public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { - return _customTypefaces.Select(x => x.FontFamily.Name).ToArray(); + if (!_isInitialized) + { + _customFonts.Initialize(this); + + _isInitialized = true; + } + + return _customFonts.Select(x=> x.Name).ToArray(); } private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - FontFamily fontFamily, CultureInfo culture, out Typeface typeface) { - foreach (var customTypeface in _customTypefaces) + if (!_isInitialized) { - if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0) - { - continue; - } - - typeface = new Typeface(customTypeface.FontFamily, fontStyle, fontWeight); + _customFonts.Initialize(this); + } + if(_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out typeface)) + { return true; } - var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight, + var fallback = SKFontManager.Default.MatchCharacter(null, (SKFontStyleWeight)fontWeight, (SKFontStyleWidth)fontStretch, (SKFontStyleSlant)fontStyle, _bcp47, codepoint); typeface = new Typeface(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); @@ -68,123 +65,21 @@ namespace Avalonia.Skia.UnitTests.Media return true; } - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) - { - SKTypeface skTypeface; - - Uri source = null; - - switch (typeface.FontFamily.Name) - { - 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(typeface.FontFamily.Name, - (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); - } - else - { - var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - - 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) + if (!_isInitialized) { - 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; - } + _customFonts.Initialize(this); } - if (source is null) + if (_customFonts.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) { - skTypeface = SKTypeface.FromFamilyName(familyName, - (SKFontStyleWeight)weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)style); + return true; } - else - { - var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - - var assetUri = FontFamilyLoader.LoadFontAssets(source).First(); - var stream = assetLoader.Open(assetUri); - - skTypeface = SKTypeface.FromStream(stream); - } + var skTypeface = SKTypeface.FromFamilyName(familyName, + (SKFontStyleWeight)weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)style); glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index c15cbfb845..8ca16bd873 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -17,7 +17,7 @@ namespace Avalonia.Skia.UnitTests.Media { var fontManager = FontManager.Current; - var glyphTypeface = new Typeface(new FontFamily("A, B, " + fontManager.DefaultFontFamilyName)).GlyphTypeface; + var glyphTypeface = new Typeface(new FontFamily("A, B, " + FontFamily.DefaultFontFamilyName)).GlyphTypeface; Assert.Equal(SKTypeface.Default.FamilyName, glyphTypeface.FamilyName); } @@ -41,7 +41,7 @@ namespace Avalonia.Skia.UnitTests.Media { var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface; - Assert.Equal(FontManager.Current.DefaultFontFamilyName, glyphTypeface.FamilyName); + Assert.Equal(FontManager.Current.DefaultFontFamily.Name, glyphTypeface.FamilyName); } } @@ -87,6 +87,24 @@ namespace Avalonia.Skia.UnitTests.Media } } + [Fact] + public void Should_Only_Try_To_Create_GlyphTypeface_Once() + { + var fontManagerImpl = new MockFontManagerImpl(); + + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: fontManagerImpl))) + { + Assert.True(FontManager.Current.TryGetGlyphTypeface(Typeface.Default, out _)); + + for (int i = 0;i < 10; i++) + { + FontManager.Current.TryGetGlyphTypeface(new Typeface("Unknown"), out _); + } + + Assert.Equal(fontManagerImpl.TryCreateGlyphTypefaceCount, 2); + } + } + [Fact] public void Should_Load_Embedded_DefaultFontFamily() { @@ -96,7 +114,7 @@ namespace Avalonia.Skia.UnitTests.Media { AvaloniaLocator.CurrentMutable.BindToSelf(new FontManagerOptions { DefaultFamilyName = s_fontUri }); - var result = FontManager.Current.TryGetGlyphTypeface(new Typeface(FontFamily.DefaultFontFamilyName), out var glyphTypeface); + var result = FontManager.Current.TryGetGlyphTypeface(Typeface.Default, out var glyphTypeface); Assert.True(result); diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs index a819cbd5e3..38897d28c5 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs @@ -35,8 +35,8 @@ namespace Avalonia.UnitTests return _customTypefaces.Select(x => x.FontFamily!.Name).ToArray(); } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - FontFamily fontFamily, CultureInfo culture, out Typeface fontKey) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, + FontStretch fontStretch, CultureInfo culture, out Typeface fontKey) { foreach (var customTypeface in _customTypefaces) { diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs index eda4544877..16423884b3 100644 --- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs @@ -15,6 +15,8 @@ namespace Avalonia.UnitTests _defaultFamilyName = defaultFamilyName; } + public int TryCreateGlyphTypefaceCount { get; private set; } + public string GetDefaultFontFamilyName() { return _defaultFamilyName; @@ -26,7 +28,7 @@ namespace Avalonia.UnitTests } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, FontFamily fontFamily, + FontStretch fontStretch, CultureInfo culture, out Typeface fontKey) { fontKey = new Typeface(_defaultFamilyName); @@ -34,14 +36,24 @@ namespace Avalonia.UnitTests return false; } - public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) + public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) { + glyphTypeface = null; + + TryCreateGlyphTypefaceCount++; + + if (familyName == "Unknown") + { + return false; + } + glyphTypeface = new MockGlyphTypeface(); return true; } - public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) { glyphTypeface = new MockGlyphTypeface();