Browse Source

Merge pull request #10857 from Gillibald/optimizeMatchCharacter

Optimize FontManager Caching
pull/10982/head
Max Katz 3 years ago
committed by GitHub
parent
commit
694f0bfacb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 56
      src/Avalonia.Base/Media/FontManager.cs
  2. 206
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  3. 259
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  4. 17
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  5. 66
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  6. 4
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  7. 3
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  8. 41
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  9. 3
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  10. 2
      tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
  11. 2
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  12. 159
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  13. 24
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
  14. 4
      tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
  15. 18
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs

56
src/Avalonia.Base/Media/FontManager.cs

@ -30,13 +30,15 @@ namespace Avalonia.Media
_fontFallbacks = options?.FontFallbacks; _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."); throw new InvalidOperationException("Default font family name can't be null or empty.");
} }
DefaultFontFamily = new FontFamily(defaultFontFamilyName);
AddFontCollection(new SystemFontCollection(this)); AddFontCollection(new SystemFontCollection(this));
} }
@ -65,9 +67,9 @@ namespace Avalonia.Media
} }
/// <summary> /// <summary>
/// Gets the system's default font family's name. /// Gets the system's default font family.
/// </summary> /// </summary>
public string DefaultFontFamilyName public FontFamily DefaultFontFamily
{ {
get; get;
} }
@ -93,6 +95,11 @@ namespace Avalonia.Media
var fontFamily = typeface.FontFamily; 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) if (fontFamily.Key is FontFamilyKey key)
{ {
var source = key.Source; 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)) 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);
} }
/// <summary> /// <summary>
@ -199,16 +212,37 @@ namespace Avalonia.Media
{ {
foreach (var fallback in _fontFallbacks) 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);
} }
} }
} }

206
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@ -8,10 +8,8 @@ using Avalonia.Platform;
namespace Avalonia.Media.Fonts namespace Avalonia.Media.Fonts
{ {
public class EmbeddedFontCollection : IFontCollection public class EmbeddedFontCollection : FontCollectionBase
{ {
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(1); private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(1);
private readonly Uri _key; private readonly Uri _key;
@ -25,13 +23,13 @@ namespace Avalonia.Media.Fonts
_source = source; _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<IAssetLoader>(); var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
@ -45,7 +43,7 @@ namespace Avalonia.Media.Fonts
{ {
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
{ {
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>(); glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces)) 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 override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
}
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) FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{ {
var key = new FontCollectionKey(style, weight, stretch); var key = new FontCollectionKey(style, weight, stretch);
@ -116,175 +95,6 @@ namespace Avalonia.Media.Fonts
return false; return false;
} }
private static bool TryGetNearestMatch( public override IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
ConcurrentDictionary<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(
ConcurrentDictionary<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(
ConcurrentDictionary<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;
}
} }
} }

259
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<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> _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<FontFamily> 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<FontCollectionKey,
IGlyphTypeface?> 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<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) && 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<FontCollectionKey,
IGlyphTypeface?> 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;
}
}
}

17
src/Avalonia.Base/Media/Fonts/IFontCollection.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Platform; using Avalonia.Platform;
namespace Avalonia.Media.Fonts namespace Avalonia.Media.Fonts
@ -29,5 +30,21 @@ namespace Avalonia.Media.Fonts
/// <returns>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns> /// <returns>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns>
bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
/// <summary>
/// Tries to match a specified character to a <see cref="Typeface"/> that supports specified font properties.
/// </summary>
/// <param name="codepoint">The codepoint to match against.</param>
/// <param name="fontStyle">The font style.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStretch">The font stretch.</param>
/// <param name="familyName">The family name. This is optional and used for fallback lookup.</param>
/// <param name="culture">The culture.</param>
/// <param name="typeface">The matching <see cref="Typeface"/>.</param>
/// <returns>
/// <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface);
} }
} }

66
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -7,10 +6,8 @@ using Avalonia.Platform;
namespace Avalonia.Media.Fonts namespace Avalonia.Media.Fonts
{ {
internal class SystemFontCollection : IFontCollection internal class SystemFontCollection : FontCollectionBase
{ {
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
private readonly FontManager _fontManager; private readonly FontManager _fontManager;
private readonly string[] _familyNames; private readonly string[] _familyNames;
@ -20,9 +17,9 @@ namespace Avalonia.Media.Fonts
_familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames(); _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 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) FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{ {
if (familyName == FontFamily.DefaultFontFamilyName) glyphTypeface = null;
{
familyName = _fontManager.DefaultFontFamilyName;
}
var key = new FontCollectionKey(style, weight, stretch); var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName, (key) => new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>());
{
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;
}
}
}
if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) if (!glyphTypefaces.TryGetValue(key, out glyphTypeface))
{ {
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>(); _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. //We initialize the system font collection during construction.
} }
IEnumerator IEnumerable.GetEnumerator() public override IEnumerator<FontFamily> GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<FontFamily> GetEnumerator()
{ {
foreach (var familyName in _familyNames) foreach (var familyName in _familyNames)
{ {
yield return new FontFamily(familyName); 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);
}
} }
} }

4
src/Avalonia.Base/Platform/IFontManagerImpl.cs

@ -27,15 +27,13 @@ namespace Avalonia.Platform
/// <param name="fontStyle">The font style.</param> /// <param name="fontStyle">The font style.</param>
/// <param name="fontWeight">The font weight.</param> /// <param name="fontWeight">The font weight.</param>
/// <param name="fontStretch">The font stretch.</param> /// <param name="fontStretch">The font stretch.</param>
/// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
/// <param name="culture">The culture.</param> /// <param name="culture">The culture.</param>
/// <param name="typeface">The matching typeface.</param> /// <param name="typeface">The matching typeface.</param>
/// <returns> /// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise. /// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns> /// </returns>
bool TryMatchCharacter(int codepoint, FontStyle fontStyle, bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontWeight fontWeight, FontStretch fontStretch, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface);
FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface);
/// <summary> /// <summary>
/// Tries to get a glyph typeface for specified parameters. /// Tries to get a glyph typeface for specified parameters.

3
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -179,8 +179,7 @@ namespace Avalonia.Headless
return true; return true;
} }
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, out Typeface typeface)
FontFamily fontFamily, CultureInfo culture, out Typeface typeface)
{ {
typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch); typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch);
return true; return true;

41
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -30,8 +30,7 @@ namespace Avalonia.Skia
[ThreadStatic] private static string[]? t_languageTagBuffer; [ThreadStatic] private static string[]? t_languageTagBuffer;
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontWeight fontWeight, FontStretch fontStretch, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface fontKey)
FontFamily? fontFamily, CultureInfo? culture, out Typeface fontKey)
{ {
SKFontStyle skFontStyle; SKFontStyle skFontStyle;
@ -60,35 +59,13 @@ namespace Avalonia.Skia
t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName;
if (fontFamily is not null && fontFamily.FamilyNames.HasFallbacks) var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint);
{
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;
}
fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight, fontStretch); if (skTypeface != null)
return true;
}
}
else
{ {
var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight, fontStretch);
if (skTypeface != null) return true;
{
fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight, fontStretch);
return true;
}
} }
fontKey = default; fontKey = default;
@ -96,7 +73,7 @@ namespace Avalonia.Skia
return false; 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) FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{ {
glyphTypeface = null; glyphTypeface = null;
@ -111,12 +88,6 @@ namespace Avalonia.Skia
return false; 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; var fontSimulations = FontSimulations.None;
if ((int)weight >= 600 && !skTypeface.IsBold) if ((int)weight >= 600 && !skTypeface.IsBold)

3
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@ -33,8 +33,7 @@ namespace Avalonia.Direct2D1.Media
} }
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontWeight fontWeight, FontStretch fontStretch, FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, out Typeface typeface)
FontFamily fontFamily, CultureInfo culture, out Typeface typeface)
{ {
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;

2
tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs

@ -43,7 +43,7 @@ namespace Avalonia.Base.UnitTests.Media
{ {
AvaloniaLocator.CurrentMutable.Bind<FontManagerOptions>().ToConstant(options); AvaloniaLocator.CurrentMutable.Bind<FontManagerOptions>().ToConstant(options);
Assert.Equal("MyFont", FontManager.Current.DefaultFontFamilyName); Assert.Equal("MyFont", FontManager.Current.DefaultFontFamily.Name);
} }
} }

2
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 glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface;
var defaultName = FontManager.Current.DefaultFontFamilyName; var defaultName = FontManager.Current.DefaultFontFamily.Name;
Assert.Equal(defaultName, glyphTypeface.FamilyName); Assert.Equal(defaultName, glyphTypeface.FamilyName);
} }

159
tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs

@ -12,24 +12,17 @@ namespace Avalonia.Skia.UnitTests.Media
{ {
public class CustomFontManagerImpl : IFontManagerImpl public class CustomFontManagerImpl : IFontManagerImpl
{ {
private readonly Typeface[] _customTypefaces;
private readonly string _defaultFamilyName; private readonly string _defaultFamilyName;
private readonly IFontCollection _customFonts;
private readonly Typeface _defaultTypeface = private bool _isInitialized;
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");
public CustomFontManagerImpl() public CustomFontManagerImpl()
{ {
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _hebrewTypeface, _defaultTypeface }; _defaultFamilyName = "Noto Mono";
_defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests");
_customFonts = new EmbeddedFontCollection(source, source);
} }
public string GetDefaultFontFamilyName() public string GetDefaultFontFamilyName()
@ -39,28 +32,32 @@ namespace Avalonia.Skia.UnitTests.Media
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) 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 }; private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,
FontFamily fontFamily,
CultureInfo culture, out Typeface typeface) CultureInfo culture, out Typeface typeface)
{ {
foreach (var customTypeface in _customTypefaces) if (!_isInitialized)
{ {
if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0) _customFonts.Initialize(this);
{ }
continue;
}
typeface = new Typeface(customTypeface.FontFamily, fontStyle, fontWeight);
if(_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out typeface))
{
return true; 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); (SKFontStyleWidth)fontStretch, (SKFontStyleSlant)fontStyle, _bcp47, codepoint);
typeface = new Typeface(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); typeface = new Typeface(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight);
@ -68,123 +65,21 @@ namespace Avalonia.Skia.UnitTests.Media
return true; 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<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, public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{ {
SKTypeface skTypeface; if (!_isInitialized)
Uri source = null;
switch (familyName)
{ {
case "Twitter Color Emoji": _customFonts.Initialize(this);
{
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) if (_customFonts.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
{ {
skTypeface = SKTypeface.FromFamilyName(familyName, return true;
(SKFontStyleWeight)weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)style);
} }
else
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var assetUri = FontFamilyLoader.LoadFontAssets(source).First();
var stream = assetLoader.Open(assetUri); var skTypeface = SKTypeface.FromFamilyName(familyName,
(SKFontStyleWeight)weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)style);
skTypeface = SKTypeface.FromStream(stream);
}
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);

24
tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

@ -17,7 +17,7 @@ namespace Avalonia.Skia.UnitTests.Media
{ {
var fontManager = FontManager.Current; 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); Assert.Equal(SKTypeface.Default.FamilyName, glyphTypeface.FamilyName);
} }
@ -41,7 +41,7 @@ namespace Avalonia.Skia.UnitTests.Media
{ {
var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface; 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] [Fact]
public void Should_Load_Embedded_DefaultFontFamily() public void Should_Load_Embedded_DefaultFontFamily()
{ {
@ -96,7 +114,7 @@ namespace Avalonia.Skia.UnitTests.Media
{ {
AvaloniaLocator.CurrentMutable.BindToSelf(new FontManagerOptions { DefaultFamilyName = s_fontUri }); 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); Assert.True(result);

4
tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

@ -35,8 +35,8 @@ namespace Avalonia.UnitTests
return _customTypefaces.Select(x => x.FontFamily!.Name).ToArray(); return _customTypefaces.Select(x => x.FontFamily!.Name).ToArray();
} }
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontFamily fontFamily, CultureInfo culture, out Typeface fontKey) FontStretch fontStretch, CultureInfo culture, out Typeface fontKey)
{ {
foreach (var customTypeface in _customTypefaces) foreach (var customTypeface in _customTypefaces)
{ {

18
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@ -15,6 +15,8 @@ namespace Avalonia.UnitTests
_defaultFamilyName = defaultFamilyName; _defaultFamilyName = defaultFamilyName;
} }
public int TryCreateGlyphTypefaceCount { get; private set; }
public string GetDefaultFontFamilyName() public string GetDefaultFontFamilyName()
{ {
return _defaultFamilyName; return _defaultFamilyName;
@ -26,7 +28,7 @@ namespace Avalonia.UnitTests
} }
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch, FontFamily fontFamily, FontStretch fontStretch,
CultureInfo culture, out Typeface fontKey) CultureInfo culture, out Typeface fontKey)
{ {
fontKey = new Typeface(_defaultFamilyName); fontKey = new Typeface(_defaultFamilyName);
@ -34,14 +36,24 @@ namespace Avalonia.UnitTests
return false; 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(); glyphTypeface = new MockGlyphTypeface();
return true; return true;
} }
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{ {
glyphTypeface = new MockGlyphTypeface(); glyphTypeface = new MockGlyphTypeface();

Loading…
Cancel
Save