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;
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
}
/// <summary>
/// Gets the system's default font family's name.
/// Gets the system's default font family.
/// </summary>
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);
}
/// <summary>
@ -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);
}
}
}

206
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<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(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<IAssetLoader>();
@ -45,7 +43,7 @@ namespace Avalonia.Media.Fonts
{
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
{
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
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<FontFamily> 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<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;
}
public override IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
}
}

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.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>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns>
bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
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.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<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _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<FontCollectionKey, IGlyphTypeface?>());
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.
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<FontFamily> GetEnumerator()
public override 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);
}
}
}

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

@ -27,15 +27,13 @@ namespace Avalonia.Platform
/// <param name="fontStyle">The font style.</param>
/// <param name="fontWeight">The font weight.</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="typeface">The matching typeface.</param>
/// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
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);
/// <summary>
/// Tries to get a glyph typeface for specified parameters.

3
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;

41
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)

3
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;

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

@ -43,7 +43,7 @@ namespace Avalonia.Base.UnitTests.Media
{
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 defaultName = FontManager.Current.DefaultFontFamilyName;
var defaultName = FontManager.Current.DefaultFontFamily.Name;
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
{
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<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)
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<IAssetLoader>();
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);

24
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);

4
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)
{

18
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();

Loading…
Cancel
Save