Browse Source

[11.3] SystemFontCollection fixes (#20207)

* Prevents repeated platform calls for matches that are already found and cached.
Ensures family-name normalization logic correctly maps platform-provided family names.

* Add missing test font

---------

Co-authored-by: Gillibald <stebner@avaloniaui.net>
release/11.3.10
Benedikt Stebner 10 months ago
committed by GitHub
parent
commit
20a35493c8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  2. BIN
      tests/Avalonia.RenderTests/Assets/NotoSansTamil-Regular.ttf
  3. 52
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  4. 179
      tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs

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

@ -162,17 +162,20 @@ namespace Avalonia.Media.Fonts
public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName,
CultureInfo? culture, out Typeface match)
{
//TODO12: Think about removing familyName parameter
match = default;
// First try to match via base implementation
if (base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match))
{
return true;
}
if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2)
{
if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, culture, out var glyphTypeface))
{
AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface);
match = GetImplicitTypeface(new Typeface(glyphTypeface.FamilyName, glyphTypeface.Style, glyphTypeface.Weight,
glyphTypeface.Stretch), out var matchedFamilyName);
match = new Typeface(glyphTypeface.FamilyName, glyphTypeface.Style, glyphTypeface.Weight,
glyphTypeface.Stretch);
AddGlyphTypefaceByFamilyName(matchedFamilyName, glyphTypeface);
return true;
}

BIN
tests/Avalonia.RenderTests/Assets/NotoSansTamil-Regular.ttf

Binary file not shown.

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

@ -7,14 +7,16 @@ using Avalonia.Platform;
using SkiaSharp;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Collections.Generic;
namespace Avalonia.Skia.UnitTests.Media
{
public class CustomFontManagerImpl : IFontManagerImpl, IDisposable
public class CustomFontManagerImpl : IFontManagerImpl2, IDisposable
{
private readonly string _defaultFamilyName;
protected readonly string _defaultFamilyName;
private readonly IFontCollection _customFonts;
private bool _isInitialized;
protected int _tryMatchCharacterCount;
public CustomFontManagerImpl()
{
@ -25,6 +27,8 @@ namespace Avalonia.Skia.UnitTests.Media
_customFonts = new EmbeddedFontCollection(source, source);
}
public int TryMatchCharacterCount => _tryMatchCharacterCount;
public string GetDefaultFontFamilyName()
{
return _defaultFamilyName;
@ -39,7 +43,7 @@ namespace Avalonia.Skia.UnitTests.Media
_isInitialized = true;
}
return _customFonts.Select(x=> x.Name).ToArray();
return _customFonts.Select(x => x.Name).ToArray();
}
private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
@ -47,12 +51,14 @@ namespace Avalonia.Skia.UnitTests.Media
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,
CultureInfo culture, out Typeface typeface)
{
_tryMatchCharacterCount++;
if (!_isInitialized)
{
_customFonts.Initialize(this);
}
if(_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out typeface))
if (_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out typeface))
{
return true;
}
@ -99,5 +105,43 @@ namespace Avalonia.Skia.UnitTests.Media
{
_customFonts.Dispose();
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
_tryMatchCharacterCount++;
if (!_isInitialized)
{
_customFonts.Initialize(this);
}
if (_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out var typeface))
{
glyphTypeface = typeface.GlyphTypeface;
return true;
}
var fallback = SKFontManager.Default.MatchCharacter(null, (SKFontStyleWeight)fontWeight,
(SKFontStyleWidth)fontStretch, (SKFontStyleSlant)fontStyle, _bcp47, codepoint);
if (fallback == null)
{
glyphTypeface = null;
return false;
}
glyphTypeface = new GlyphTypefaceImpl(fallback, FontSimulations.None);
return true;
}
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface> familyTypefaces)
{
familyTypefaces = null;
return false;
}
}
}

179
tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs

@ -5,6 +5,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
@ -115,6 +116,61 @@ namespace Avalonia.Skia.UnitTests.Media
}
}
[Fact]
public void SystemFontCollection_Only_Calls_Platform_TryMatchCharacter_Once_On_Success()
{
var countingImpl = new CustomFontManagerImpl();
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: countingImpl)))
{
var fontManager = FontManager.Current;
var systemFonts = fontManager.SystemFonts as SystemFontCollection;
Assert.NotNull(systemFonts);
// First call should invoke platform TryMatchCharacter and populate cache
Assert.True(systemFonts.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match1));
// Second call should be served from cache and should not call platform TryMatchCharacter again
Assert.True(systemFonts.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match2));
Assert.Equal(1, countingImpl.TryMatchCharacterCount);
Assert.Equal(match1.FontFamily.Name, match2.FontFamily.Name);
}
}
[Fact]
public void Should_Cache_Font_By_Normalized_Name_When_Platform_Returns_Regular_Suffix()
{
var impl = new RegularSuffixFontManagerImpl("Default");
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: impl)))
{
var fontManager = FontManager.Current;
var systemFonts = new TestSystemFontCollection(fontManager);
Assert.NotNull(systemFonts);
// Call TryMatchCharacter which should invoke platform TryMatchCharacter and add to cache
Assert.True(systemFonts.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match));
var normalized = fontManager.DefaultFontFamily.Name;
// Ensure the cache contains the normalized name (without 'Regular')
Assert.True(systemFonts.GlyphTypefaceCache.ContainsKey(normalized));
// Ensure the raw returned name with ' Regular' is not used as cache key
Assert.False(systemFonts.GlyphTypefaceCache.ContainsKey(normalized + " Regular"));
Assert.True(systemFonts.TryGetGlyphTypeface(normalized + " Regular", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var cachedGlyphTypeface));
Assert.Equal(match.FontFamily.Name, cachedGlyphTypeface.FamilyName);
}
}
private class CustomizableFontCollection : EmbeddedFontCollection
{
private readonly IReadOnlyList<FontFallback>? _fallbacks;
@ -174,5 +230,128 @@ namespace Avalonia.Skia.UnitTests.Media
return base.TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out syntheticGlyphTypeface);
}
}
private class RegularSuffixFontManagerImpl : IFontManagerImpl2
{
private readonly string _defaultFamilyName;
public RegularSuffixFontManagerImpl(string defaultFamilyName)
{
_defaultFamilyName = defaultFamilyName;
}
public int TryMatchCharacterCount { get; private set; }
public string GetDefaultFontFamilyName() => _defaultFamilyName;
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) => new[] { _defaultFamilyName };
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface)
{
if (TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out IGlyphTypeface? glyphTypeface))
{
typeface = new Typeface(glyphTypeface.FamilyName, fontStyle, fontWeight);
return true;
}
typeface = default;
return false;
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
TryMatchCharacterCount++;
// Return a glyph typeface with ' Regular' appended so it will be normalized
glyphTypeface = new SimpleGlyphTypeface(_defaultFamilyName + " Regular", fontStyle, fontWeight, fontStretch);
return true;
}
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
return false;
}
public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
return false;
}
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
familyTypefaces = null;
return false;
}
}
// Minimal IGlyphTypeface implementation for testing
private class SimpleGlyphTypeface : IGlyphTypeface
{
public SimpleGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch)
{
FamilyName = familyName;
Style = style;
Weight = weight;
Stretch = stretch;
}
public FontMetrics Metrics => new FontMetrics { DesignEmHeight = 10, Ascent = 5, Descent = 3, LineGap = 0, IsFixedPitch = false };
public int GlyphCount => 1;
public FontSimulations FontSimulations => FontSimulations.None;
public string FamilyName { get; }
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
public void Dispose() { }
public ushort GetGlyph(uint codepoint) => 1;
public bool TryGetGlyph(uint codepoint, out ushort glyph)
{
glyph = 1;
return true;
}
public int GetGlyphAdvance(ushort glyph) => 1;
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
var arr = new int[glyphs.Length];
for (var i = 0; i < arr.Length; i++) arr[i] = 1;
return arr;
}
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
{
var arr = new ushort[codepoints.Length];
for (var i = 0; i < arr.Length; i++) arr[i] = 1;
return arr;
}
public bool TryGetTable(uint tag, out byte[] table)
{
table = null!;
return false;
}
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = new GlyphMetrics { Width = 1, Height = 1 };
return true;
}
}
}
}

Loading…
Cancel
Save