Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 15 additions & 18 deletions src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,42 +30,39 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon
{
var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);

if (base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
{
return true;
}

// Use normalized values for cache lookup to ensure consistent cache keys
style = typeface.Style;

weight = typeface.Weight;

stretch = typeface.Stretch;

var key = new FontCollectionKey(style, weight, stretch);

//Check cache first to avoid unnecessary calls to the font manager
// Check cache first
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
return glyphTypeface != null;
}

//Try to create the glyph typeface via system font manager
if (!_platformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
// Try base class (handles nearest match and synthetic creation)
if (base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
{
//Add null to cache to avoid future calls
TryAddGlyphTypeface(familyName, key, null);

return false;
// Cache under requested key to prevent leaks when FamilyName differs
TryAddGlyphTypeface(familyName, key, glyphTypeface);
return true;
}

//Add to cache
if (!TryAddGlyphTypeface(glyphTypeface))
// Create via platform
if (!_platformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
{
TryAddGlyphTypeface(familyName, key, null);
return false;
}

//Requested glyph typeface should be in cache now
return base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface);
// Cache under requested key (prevents leaks) and actual properties
TryAddGlyphTypeface(familyName, key, glyphTypeface);
TryAddGlyphTypeface(glyphTypeface);

return true;
}

public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
Expand Down
89 changes: 89 additions & 0 deletions tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,95 @@ public TestSystemFontCollection(IFontManagerImpl platformImpl) : base(platformIm
public IDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> GlyphTypefaceCache => _glyphTypefaceCache;
}

/// <summary>
/// Verifies no leak when the returned typeface's FamilyName differs from requested.
/// This happens with fonts like "Segoe UI Variable Text" returning FamilyName="Segoe UI Variable".
/// </summary>
[Fact]
public void Should_Not_Leak_When_Typeface_FamilyName_Differs_From_Requested()
{
var fontManager = new MockFontManagerImpl(returnDifferentFamilyName: true);

using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: fontManager)))
{
var fontCollection = new TestSystemFontCollection(fontManager);

// First request creates the typeface
fontCollection.TryGetGlyphTypeface("TestFont", FontStyle.Normal, FontWeight.Bold, FontStretch.Normal, out _);
var countAfterFirst = fontManager.CreateCount;

// Subsequent requests should hit cache, not create new typefaces
for (int i = 0; i < 100; i++)
{
fontCollection.TryGetGlyphTypeface("TestFont", FontStyle.Normal, FontWeight.Bold, FontStretch.Normal, out _);
}

Assert.Equal(countAfterFirst, fontManager.CreateCount);
}
}

private class MockFontManagerImpl : IFontManagerImpl
{
public int CreateCount { get; private set; }
private readonly bool _returnDifferentFamilyName;

public MockFontManagerImpl(bool returnDifferentFamilyName) => _returnDifferentFamilyName = returnDifferentFamilyName;

public string GetDefaultFontFamilyName() => "TestFont";
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) => new[] { "TestFont" };

public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface)
{
typeface = new Typeface("TestFont");
return true;
}

public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
CreateCount++;
var returnedName = _returnDifferentFamilyName ? familyName + " UI" : familyName;
glyphTypeface = new MockGlyphTypeface(returnedName, style, weight, stretch);
return true;
}

public bool TryCreateGlyphTypeface(System.IO.Stream stream, FontSimulations fontSimulations,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = new MockGlyphTypeface("TestFont", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal);
return true;
}
}

private class MockGlyphTypeface : IGlyphTypeface
{
public MockGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch)
{
FamilyName = familyName;
Style = style;
Weight = weight;
Stretch = stretch;
}

public string FamilyName { get; }
public FontStyle Style { get; }
public FontWeight Weight { get; }
public FontStretch Stretch { get; }
public int GlyphCount => 1;
public FontMetrics Metrics => new FontMetrics { DesignEmHeight = 1000, Ascent = -800, Descent = 200 };
public FontSimulations FontSimulations => FontSimulations.None;

public ushort GetGlyph(uint codepoint) => 1;
public bool TryGetGlyph(uint codepoint, out ushort glyph) { glyph = 1; return true; }
public int GetGlyphAdvance(ushort glyph) => 500;
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs) => new int[glyphs.Length];
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints) => new ushort[codepoints.Length];
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) { metrics = default; return true; }
public bool TryGetTable(uint tag, out byte[] table) { table = Array.Empty<byte>(); return false; }
public void Dispose() { }
}

[Fact]
public void Should_Use_Fallback()
{
Expand Down
Loading