diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
index c7b013729c..390f4f4719 100644
--- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
+++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
@@ -67,6 +67,10 @@ namespace Avalonia.Media.Fonts
//Add to cache with platform typeface family name first
TryAddGlyphTypeface(platformTypeface.FamilyName, key, glyphTypeface);
+
+ // Then the requested family name
+ if (familyName != platformTypeface.FamilyName)
+ TryAddGlyphTypeface(familyName, key, glyphTypeface);
//Add to cache
if (!TryAddGlyphTypeface(glyphTypeface))
diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
index b0d6e1bfd1..4f171b29ad 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
using Avalonia.Fonts.Inter;
using Avalonia.Logging;
using Avalonia.Media;
@@ -643,5 +646,108 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Inter", FontStyle.Normal, (FontWeight)650), out var nearestMatchTypeface));
Assert.Same(boldGlyphTypeface, nearestMatchTypeface);
}
+
+ [Fact]
+ public void TryGetGlyphTypeface_Should_Cache_Matched_GlyphTypeface_Under_Requested_FamilyName()
+ {
+ var fontManagerImpl = new FamilyRemappingFontManagerImpl("NotInstalled", "Noto Mono");
+ using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: fontManagerImpl));
+
+ // "NotInstalled" is not installed, so the platform substitutes it with a different
+ // family ("Noto Mono"), much like requesting "Arial" yields "Liberation Sans" on some Linux distributions.
+ Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("NotInstalled"), out var first));
+ Assert.Equal("Noto Mono", first.FamilyName);
+
+ // The substitute should now be cached under the requested "NotInstalled" name, so a second
+ // lookup must resolve from the cache instead of asking the platform again.
+ Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("NotInstalled"), out var second));
+ Assert.Same(first, second);
+ Assert.Equal(1, fontManagerImpl.RequestedFamilyCreateCount);
+ }
+
+ ///
+ /// A font manager whose every by-name lookup resolves to a single matched font whose family name
+ /// differs from the requested one.
+ ///
+ private sealed class FamilyRemappingFontManagerImpl(string requestedFamilyName, string matchedFamilyName)
+ : IFontManagerImpl, IDisposable
+ {
+ public int RequestedFamilyCreateCount { get; private set; }
+
+ public string GetDefaultFontFamilyName()
+ => matchedFamilyName;
+
+ public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
+ => [matchedFamilyName];
+
+ public bool TryCreateGlyphTypeface(
+ string familyName,
+ FontStyle style,
+ FontWeight weight,
+ FontStretch stretch,
+ [NotNullWhen(true)] out IPlatformTypeface? platformTypeface)
+ {
+ if (string.Equals(familyName, requestedFamilyName, StringComparison.OrdinalIgnoreCase))
+ RequestedFamilyCreateCount++;
+
+ platformTypeface = new SkiaTypeface(CreateMatchedTypeface(), FontSimulations.None);
+ return true;
+ }
+
+ public bool TryCreateGlyphTypeface(
+ Stream stream,
+ FontSimulations fontSimulations,
+ [NotNullWhen(true)] out IPlatformTypeface? platformTypeface)
+ {
+ platformTypeface = new SkiaTypeface(SKTypeface.FromStream(stream), fontSimulations);
+ return true;
+ }
+
+ public bool TryMatchCharacter(
+ int codepoint,
+ FontStyle fontStyle,
+ FontWeight fontWeight,
+ FontStretch fontStretch,
+ string? familyName,
+ CultureInfo? culture,
+ [NotNullWhen(true)] out IPlatformTypeface? platformTypeface)
+ {
+ platformTypeface = null;
+ return false;
+ }
+
+ public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces)
+ {
+ familyTypefaces = null;
+ return false;
+ }
+
+ private SKTypeface CreateMatchedTypeface()
+ {
+ var assetLoader = AvaloniaLocator.Current.GetRequiredService();
+
+ // LoadFontAssets ignores the family fragment and returns every embedded font asset,
+ // so pick the one whose family name matches the substitute we want to return.
+ foreach (var fontAsset in FontFamilyLoader.LoadFontAssets(new Uri(s_fontUri)))
+ {
+ var stream = assetLoader.Open(fontAsset);
+ var typeface = SKTypeface.FromStream(stream);
+
+ if (typeface is not null &&
+ string.Equals(typeface.FamilyName, matchedFamilyName, StringComparison.OrdinalIgnoreCase))
+ {
+ return typeface;
+ }
+
+ typeface?.Dispose();
+ }
+
+ throw new InvalidOperationException($"Could not load the '{matchedFamilyName}' font asset.");
+ }
+
+ public void Dispose()
+ {
+ }
+ }
}
}