Browse Source

Find perfect match before nearest in font collection (#20851)

* Find perfect match before nearest in font collection

* Update API suppressions

* Address review
pull/20857/head
Julien Lebosquain 2 weeks ago
committed by GitHub
parent
commit
f13bfe6c1b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 16
      api/Avalonia.nupkg.xml
  2. 45
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  3. 15
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  4. BIN
      tests/Avalonia.RenderTests/Assets/Inter-Bold.ttf
  5. 46
      tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs
  6. 24
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

16
api/Avalonia.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
@ -1375,6 +1375,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.GlyphTypeface@)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
@ -2773,6 +2779,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.GlyphTypeface@)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
@ -4969,4 +4981,4 @@
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
</Suppressions>
</Suppressions>

45
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@ -168,7 +168,7 @@ namespace Avalonia.Media.Fonts
var key = typeface.ToFontCollectionKey();
return TryGetGlyphTypeface(familyName, key, out glyphTypeface);
return TryGetGlyphTypeface(familyName, key, allowNearestMatch: true, out glyphTypeface);
}
public virtual bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
@ -455,25 +455,25 @@ namespace Avalonia.Media.Fonts
/// find the best match based on the provided <paramref name="key"/>.</remarks>
/// <param name="familyName">The name of the font family to search for. This parameter is case-insensitive.</param>
/// <param name="key">The key representing the desired font collection attributes.</param>
/// <param name="allowNearestMatch">Whether to allow a nearest match (as opposed to only an exact match).</param>
/// <param name="glyphTypeface">When this method returns, contains the matching <see cref="GlyphTypeface"/> if a match is found; otherwise,
/// <see langword="null"/>.</param>
/// <returns><see langword="true"/> if a matching glyph typeface is found; otherwise, <see langword="false"/>.</returns>
protected bool TryGetGlyphTypeface(string familyName, FontCollectionKey key, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
protected bool TryGetGlyphTypeface(
string familyName,
FontCollectionKey key,
bool allowNearestMatch,
[NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
if (TryGetMatch(glyphTypefaces, key, allowNearestMatch, out glyphTypeface, out var isNearestMatch))
{
var matchedKey = glyphTypeface.ToFontCollectionKey();
if (matchedKey != key)
if (isNearestMatch && matchedKey != key)
{
if (TryCreateSyntheticGlyphTypeface(glyphTypeface, key.Style, key.Weight, key.Stretch, out var syntheticGlyphTypeface))
{
@ -511,7 +511,7 @@ namespace Avalonia.Media.Fonts
{
// Exact match found in snapshot. Use the exact family name for lookup
if (_glyphTypefaceCache.TryGetValue(snapshot[mid].Name, out var exactGlyphTypefaces) &&
TryGetNearestMatch(exactGlyphTypefaces, key, out glyphTypeface))
TryGetMatch(exactGlyphTypefaces, key, allowNearestMatch, out glyphTypeface, out _))
{
return true;
}
@ -549,7 +549,7 @@ namespace Avalonia.Media.Fonts
}
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
TryGetMatch(glyphTypefaces, key, allowNearestMatch, out glyphTypeface, out _))
{
return true;
}
@ -559,6 +559,29 @@ namespace Avalonia.Media.Fonts
return false;
}
private bool TryGetMatch(
IDictionary<FontCollectionKey, GlyphTypeface?> glyphTypefaces,
FontCollectionKey key,
bool allowNearestMatch,
[NotNullWhen(true)] out GlyphTypeface? glyphTypeface,
out bool isNearestMatch)
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface is not null)
{
isNearestMatch = false;
return true;
}
if (allowNearestMatch && TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
isNearestMatch = true;
return true;
}
isNearestMatch = false;
return false;
}
/// <summary>
/// Attempts to retrieve the nearest matching <see cref="GlyphTypeface"/> for the specified font key from the
/// provided collection of glyph typefaces.

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

@ -29,14 +29,14 @@ namespace Avalonia.Media.Fonts
FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
{
var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
var key = typeface.ToFontCollectionKey();
if (base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
// Find an exact match first
if (TryGetGlyphTypeface(familyName, key, allowNearestMatch: false, out glyphTypeface))
{
return true;
}
var key = typeface.ToFontCollectionKey();
//Check cache first to avoid unnecessary calls to the font manager
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
@ -52,6 +52,13 @@ namespace Avalonia.Media.Fonts
return false;
}
// The font manager didn't return a perfect match either. Find the nearest match ourselves.
if (key != platformTypeface.ToFontCollectionKey() &&
TryGetGlyphTypeface(familyName, key, allowNearestMatch: true, out glyphTypeface))
{
return true;
}
glyphTypeface = GlyphTypeface.TryCreate(platformTypeface);
if (glyphTypeface is null)
{
@ -77,7 +84,7 @@ namespace Avalonia.Media.Fonts
}
//Requested glyph typeface should be in cache now
return base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface);
return TryGetGlyphTypeface(familyName, key, allowNearestMatch: false, out glyphTypeface);
}
public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)

BIN
tests/Avalonia.RenderTests/Assets/Inter-Bold.ttf

Binary file not shown.

46
tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs

@ -29,20 +29,21 @@ namespace Avalonia.Skia.UnitTests.Media
var infos = new[]
{
new FontAssetInfo($"{AssetsNamespace}.AdobeBlank2VF.ttf", "Adobe Blank 2 VF R"),
new FontAssetInfo($"{AssetsNamespace}.Inter-Regular.ttf", "Inter"),
new FontAssetInfo($"{AssetsNamespace}.Manrope-Light.ttf", "Manrope Light"),
new FontAssetInfo($"{AssetsNamespace}.MiSans-Normal.ttf", "MiSans Normal"),
new FontAssetInfo($"{AssetsNamespace}.NISC18030.ttf", "GB18030 Bitmap"),
new FontAssetInfo($"{AssetsNamespace}.NotoMono-Regular.ttf", "Noto Mono"),
new FontAssetInfo($"{AssetsNamespace}.NotoSans-Italic.ttf", "Noto Sans"),
new FontAssetInfo($"{AssetsNamespace}.NotoSansArabic-Regular.ttf", "Noto Sans Arabic"),
new FontAssetInfo($"{AssetsNamespace}.NotoSansDeseret-Regular.ttf", "Noto Sans Deseret"),
new FontAssetInfo($"{AssetsNamespace}.NotoSansHebrew-Regular.ttf", "Noto Sans Hebrew"),
new FontAssetInfo($"{AssetsNamespace}.NotoSansMiao-Regular.ttf", "Noto Sans Miao"),
new FontAssetInfo($"{AssetsNamespace}.NotoSansTamil-Regular.ttf", "Noto Sans Tamil"),
new FontAssetInfo($"{AssetsNamespace}.SourceSerif4_36pt-Italic.ttf", "Source Serif 4 36pt"),
new FontAssetInfo($"{AssetsNamespace}.TwitterColorEmoji-SVGinOT.ttf", "Twitter Color Emoji")
new FontAssetInfo($"{AssetsNamespace}.AdobeBlank2VF.ttf", "Adobe Blank 2 VF R", FontWeight.Normal),
new FontAssetInfo($"{AssetsNamespace}.Inter-Bold.ttf", "Inter", FontWeight.Bold),
new FontAssetInfo($"{AssetsNamespace}.Inter-Regular.ttf", "Inter", FontWeight.Normal),
new FontAssetInfo($"{AssetsNamespace}.Manrope-Light.ttf", "Manrope Light", FontWeight.Light),
new FontAssetInfo($"{AssetsNamespace}.MiSans-Normal.ttf", "MiSans Normal", (FontWeight)305),
new FontAssetInfo($"{AssetsNamespace}.NISC18030.ttf", "GB18030 Bitmap", FontWeight.Normal),
new FontAssetInfo($"{AssetsNamespace}.NotoMono-Regular.ttf", "Noto Mono", FontWeight.Normal),
new FontAssetInfo($"{AssetsNamespace}.NotoSans-Italic.ttf", "Noto Sans", FontWeight.Normal),
new FontAssetInfo($"{AssetsNamespace}.NotoSansArabic-Regular.ttf", "Noto Sans Arabic", FontWeight.Normal),
new FontAssetInfo($"{AssetsNamespace}.NotoSansDeseret-Regular.ttf", "Noto Sans Deseret", FontWeight.Normal),
new FontAssetInfo($"{AssetsNamespace}.NotoSansHebrew-Regular.ttf", "Noto Sans Hebrew", FontWeight.Normal),
new FontAssetInfo($"{AssetsNamespace}.NotoSansMiao-Regular.ttf", "Noto Sans Miao", FontWeight.Normal),
new FontAssetInfo($"{AssetsNamespace}.NotoSansTamil-Regular.ttf", "Noto Sans Tamil", FontWeight.Normal),
new FontAssetInfo($"{AssetsNamespace}.SourceSerif4_36pt-Italic.ttf", "Source Serif 4 36pt", FontWeight.Normal),
new FontAssetInfo($"{AssetsNamespace}.TwitterColorEmoji-SVGinOT.ttf", "Twitter Color Emoji", FontWeight.Normal)
};
var assets = assetLoader.GetAssets(new Uri(AssetFonts, UriKind.Absolute), null)
@ -51,6 +52,9 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.Equal(infos.Length, assets.Length);
var glyphTypefaces = new GlyphTypeface[infos.Length];
// Load fonts
for (var i = 0; i < infos.Length; ++i)
{
var info = infos[i];
@ -63,8 +67,18 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.True(fontCollection.TryAddGlyphTypeface(fontStream, out var glyphTypeface));
Assert.Equal(info.FamilyName, glyphTypeface.FamilyName);
Assert.Equal(info.Weight, glyphTypeface.Weight);
glyphTypefaces[i] = glyphTypeface;
}
// Check against the custom collection
for (var i = 0; i < infos.Length; ++i)
{
var info = infos[i];
var glyphTypeface = glyphTypefaces[i];
Assert.True(fontManager.TryGetGlyphTypeface(new Typeface($"fonts:custom#{info.FamilyName}"), out var secondGlyphTypeface));
Assert.True(fontManager.TryGetGlyphTypeface(new Typeface($"fonts:custom#{info.FamilyName}", weight: info.Weight), out var secondGlyphTypeface));
Assert.Same(glyphTypeface, secondGlyphTypeface);
}
}
@ -207,6 +221,6 @@ namespace Avalonia.Skia.UnitTests.Media
public override Uri Key { get; } = key;
}
private record struct FontAssetInfo(string Path, string FamilyName);
private record struct FontAssetInfo(string Path, string FamilyName, FontWeight Weight);
}
}

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

@ -619,5 +619,29 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.Equal("Inter", typeface.GlyphTypeface.FamilyName);
Assert.Equal(requestedStretch, typeface.Stretch);
}
[Fact]
public void TryGetGlyphTypeface_Should_Use_Perfect_Match_In_Collection_Before_Nearest_Match()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()));
using var scope = AvaloniaLocator.EnterScope();
// Load bold font (Inter-Bold.ttf) first
Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Inter", FontStyle.Normal, FontWeight.Bold), out var boldGlyphTypeface));
Assert.NotNull(boldGlyphTypeface);
Assert.Equal("Inter", boldGlyphTypeface.FamilyName);
Assert.Equal(FontWeight.Bold, boldGlyphTypeface.Weight);
// Normal font (Inter-Regular.ttf) should be loaded since it's a perfect match, instead of falling back
Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Inter", FontStyle.Normal, FontWeight.Normal), out var regularGlyphTypeface));
Assert.NotNull(regularGlyphTypeface);
Assert.NotSame(regularGlyphTypeface, boldGlyphTypeface);
Assert.Equal("Inter", regularGlyphTypeface.FamilyName);
Assert.Equal(FontWeight.Normal, regularGlyphTypeface.Weight);
// Nearest match should still work (650 falls back to 700 Bold)
Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Inter", FontStyle.Normal, (FontWeight)650), out var nearestMatchTypeface));
Assert.Same(boldGlyphTypeface, nearestMatchTypeface);
}
}
}

Loading…
Cancel
Save