From 1936725f2d8a3184c538f5767b372aa381850109 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Sat, 8 Nov 2025 14:35:20 +0100 Subject: [PATCH] Cache platform font manager TryMatchCharacter result (#19987) --- src/Avalonia.Base/Media/FontManager.cs | 7 +- .../Media/Fonts/SystemFontCollection.cs | 62 +++++++++++----- .../Platform/IFontManagerImpl.cs | 15 ++++ src/Skia/Avalonia.Skia/FontManagerImpl.cs | 71 ++++++++++++++----- .../Media/FontManagerTests.cs | 21 ++++++ 5 files changed, 141 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index c8d8042e83..5a49511a5a 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -287,6 +287,8 @@ namespace Avalonia.Media } if (TryGetFontCollection(source, out var fontCollection) && + // With composite fonts we need to first check if the font collection contains the family if not we skip it + fontCollection.TryGetGlyphTypeface(familyName, fontStyle, fontWeight, fontStretch, out _) && fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) { return true; @@ -306,8 +308,9 @@ namespace Avalonia.Media } } - //Try to find a match with the system font manager - return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface); + //Try to find a match with the system font collection + return SystemFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily?.Name, + culture, out typeface); } internal IReadOnlyList GetFamilyTypefaces(FontFamily fontFamily) diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index 3a98a30b90..3b0c71ce20 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using Avalonia.Platform; @@ -15,7 +16,7 @@ namespace Avalonia.Media.Fonts public SystemFontCollection(FontManager fontManager) { _fontManager = fontManager; - _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().Where(x=> !string.IsNullOrEmpty(x)).ToList(); + _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().Where(x => !string.IsNullOrEmpty(x)).ToList(); } public override Uri Key => FontManager.SystemFontsKey; @@ -144,21 +145,6 @@ namespace Avalonia.Media.Fonts } return; - - void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface) - { - var typefaces = _glyphTypefaceCache.GetOrAdd(familyName, - x => - { - _familyNames.Insert(0, familyName); - - return new ConcurrentDictionary(); - }); - - typefaces.TryAdd( - new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch), - glyphTypeface); - } } public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) @@ -172,5 +158,49 @@ namespace Avalonia.Media.Fonts return false; } + + 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; + + if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2) + { + if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, culture, out var glyphTypeface)) + { + AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface); + + match = new Typeface(glyphTypeface.FamilyName, glyphTypeface.Style, glyphTypeface.Weight, + glyphTypeface.Stretch); + + return true; + } + + return false; + } + else + { + return _fontManager.PlatformImpl.TryMatchCharacter(codepoint, style, weight, stretch, culture, out match); + } + } + + private void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface) + { + // Add family name to the collection if not exists + if (!_familyNames.Contains(familyName)) + { + _familyNames.Add(familyName); + } + + // Get or create the typefaces dictionary for the family name + if (!_glyphTypefaceCache.TryGetValue(familyName, out var typefaces)) + { + _glyphTypefaceCache[familyName] = typefaces = new ConcurrentDictionary(); + } + + // Add the glyph typeface to the cache + typefaces.TryAdd(new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch), glyphTypeface); + } } } diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index ce9f85a5e2..42c9b3623f 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -65,6 +65,21 @@ namespace Avalonia.Platform internal interface IFontManagerImpl2 : IFontManagerImpl { + /// + /// Tries to match a specified character to a typeface that supports specified font properties. + /// + /// The codepoint to match against. + /// The font style. + /// The font weight. + /// The font stretch. + /// The culture. + /// The matching typeface. + /// + /// True, if the could match the character to specified parameters, False otherwise. + /// + bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, [NotNullWhen(true)] out IGlyphTypeface? typeface); + /// /// Tries to get a list of typefaces for the specified family name. /// diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index eb1833193c..e013124cf1 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -1,8 +1,11 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.Text.RegularExpressions; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -15,6 +18,7 @@ namespace Avalonia.Skia public string GetDefaultFontFamilyName() { + return SKTypeface.Default.FamilyName; } @@ -32,6 +36,53 @@ namespace Avalonia.Skia public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface fontKey) + { + if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out SKTypeface? skTypeface)) + { + fontKey = default; + + return false; + } + + fontKey = new Typeface( + skTypeface.FamilyName, + skTypeface.FontStyle.Slant.ToAvalonia(), + (FontWeight)skTypeface.FontStyle.Weight, + (FontStretch)skTypeface.FontStyle.Width); + + skTypeface.Dispose(); + + return true; + + } + + public bool TryMatchCharacter( + int codepoint, + FontStyle fontStyle, + FontWeight fontWeight, + FontStretch fontStretch, + CultureInfo? culture, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out SKTypeface? skTypeface)) + { + glyphTypeface = null; + + return false; + } + + glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + + return true; + } + + private bool TryMatchCharacter( + int codepoint, + FontStyle fontStyle, + FontWeight fontWeight, + FontStretch fontStretch, + CultureInfo? culture, + [NotNullWhen(true)] out SKTypeface? skTypeface) { SKFontStyle skFontStyle; @@ -59,23 +110,9 @@ namespace Avalonia.Skia t_languageTagBuffer ??= new string[1]; t_languageTagBuffer[0] = culture.Name; - using var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); + skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); - if (skTypeface != null) - { - // ToDo: create glyph typeface here to get the correct style/weight/stretch - fontKey = new Typeface( - skTypeface.FamilyName, - skTypeface.FontStyle.Slant.ToAvalonia(), - (FontWeight)skTypeface.FontStyle.Weight, - (FontStretch)skTypeface.FontStyle.Width); - - return true; - } - - fontKey = default; - - return false; + return skTypeface != null; } public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index 2713e7133b..788815ec41 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -110,6 +110,27 @@ namespace Avalonia.Skia.UnitTests.Media } } + [Fact] + public void Should_Cache_MatchCharacter() + { + var fontManagerImpl = new CustomFontManagerImpl(); + + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: fontManagerImpl))) + { + var emoji = Codepoint.ReadAt("😀", 0, out _); + + Assert.True(FontManager.Current.TryMatchCharacter((int)emoji, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var firstMatch)); + + var firstGlyphTypeface = firstMatch.GlyphTypeface; + + Assert.True(FontManager.Current.TryMatchCharacter((int)emoji, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var secondMatch)); + + var secondGlyphTypeface = secondMatch.GlyphTypeface; + + Assert.Equal(firstGlyphTypeface, secondGlyphTypeface); + } + } + [Fact] public void Should_Load_Embedded_DefaultFontFamily() {