From b7c2881655e694abe3330be8f8bd20563582adfe Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 27 May 2025 12:17:46 +0200 Subject: [PATCH] Make typeface matching and synthetic typeface creation customizable (#18890) * Make typeface matching and synthetic typeface creation customizable * Rename test collection * Revert breaking change * Directly use the DefaultFontFamily name when the alias is being used --- src/Avalonia.Base/Media/FontManager.cs | 66 ++--------- .../Media/Fonts/EmbeddedFontCollection.cs | 14 ++- .../Media/Fonts/FontCollectionBase.cs | 84 ++++++++++++- .../Media/Fonts/IFontCollection.cs | 11 ++ .../Media/Fonts/SystemFontCollection.cs | 6 +- .../HeadlessPlatformStubs.cs | 6 +- .../Media/EmbeddedFontCollectionTests.cs | 34 +++++- .../Media/FontCollectionTests.cs | 111 +++++++++++++++++- 8 files changed, 260 insertions(+), 72 deletions(-) diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 8afb1537d6..2671305222 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -109,9 +109,9 @@ namespace Avalonia.Media var familyName = fontFamily.FamilyNames[i]; - if(_fontFamilyMappings != null && _fontFamilyMappings.TryGetValue(familyName, out var mappedFontFamily)) + if (_fontFamilyMappings != null && _fontFamilyMappings.TryGetValue(familyName, out var mappedFontFamily)) { - if(mappedFontFamily.Key != null) + if (mappedFontFamily.Key != null) { key = mappedFontFamily.Key; } @@ -123,6 +123,11 @@ namespace Avalonia.Media familyName = mappedFontFamily.FamilyNames.PrimaryFamilyName; } + if (familyName == FontFamily.DefaultFontFamilyName) + { + return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); + } + if (TryGetGlyphTypefaceByKeyAndName(typeface, key, familyName, out glyphTypeface) && glyphTypeface.FamilyName.Contains(familyName)) { @@ -274,6 +279,11 @@ namespace Avalonia.Media var familyName = fontFamily.FamilyNames[i]; var source = key.Source.EnsureAbsolute(key.BaseUri); + if(familyName == FontFamily.DefaultFontFamilyName) + { + familyName = DefaultFontFamily.Name; + } + if (TryGetFontCollection(source, out var fontCollection) && fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) { @@ -286,58 +296,6 @@ namespace Avalonia.Media return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface); } - /// - /// Tries to create a synthetic glyph typefacefor specified source glyph typeface and font properties. - /// - /// The font manager implementation. - /// The source glyph typeface. - /// The requested font style. - /// The requested font weight. - /// The created synthetic glyph typeface. - /// - /// True, if the could create a synthetic glyph typeface, False otherwise. - /// - internal static bool TryCreateSyntheticGlyphTypeface(IFontManagerImpl fontManager, IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, - [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface) - { - if (fontManager == null) - { - syntheticGlyphTypeface = null; - - return false; - } - - if (glyphTypeface is IGlyphTypeface2 glyphTypeface2) - { - var fontSimulations = FontSimulations.None; - - if (style != FontStyle.Normal && glyphTypeface2.Style != style) - { - fontSimulations |= FontSimulations.Oblique; - } - - if ((int)weight >= 600 && glyphTypeface2.Weight < weight) - { - fontSimulations |= FontSimulations.Bold; - } - - if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream)) - { - using (stream) - { - fontManager.TryCreateGlyphTypeface(stream, fontSimulations, - out syntheticGlyphTypeface); - - return syntheticGlyphTypeface != null; - } - } - } - - syntheticGlyphTypeface = null; - - return false; - } - internal IReadOnlyList GetFamilyTypefaces(FontFamily fontFamily) { var key = fontFamily.Key; diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index f06e5d1562..06f9e82858 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -71,14 +71,16 @@ namespace Avalonia.Media.Fonts if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) { - if(_fontManager != null && FontManager.TryCreateSyntheticGlyphTypeface(_fontManager, glyphTypeface, style, weight, out var syntheticGlyphTypeface)) + var matchedKey = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); + + if(matchedKey != key) { - glyphTypeface = syntheticGlyphTypeface; + if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface)) + { + glyphTypeface = syntheticGlyphTypeface; + } } - //Make sure we cache the found match - glyphTypefaces.TryAdd(key, glyphTypeface); - return true; } } @@ -143,7 +145,7 @@ namespace Avalonia.Media.Fonts } } - bool IFontCollection2.TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) { familyTypefaces = null; diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index 4f8376d267..a022fcfe4d 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -23,7 +23,7 @@ namespace Avalonia.Media.Fonts public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); - public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, + public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName, CultureInfo? culture, out Typeface match) { match = default; @@ -59,6 +59,88 @@ namespace Avalonia.Media.Fonts return false; } + public virtual bool TryCreateSyntheticGlyphTypeface( + IGlyphTypeface glyphTypeface, + FontStyle style, + FontWeight weight, + FontStretch stretch, + [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface) + { + syntheticGlyphTypeface = null; + + //Source family should be present in the cache. + if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) + { + return false; + } + + var fontManager = FontManager.Current.PlatformImpl; + + var key = new FontCollectionKey(style, weight, stretch); + + var currentKey = + new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); + + if (currentKey == key) + { + return false; + } + + if (glyphTypeface is not IGlyphTypeface2 glyphTypeface2) + { + return false; + } + + var fontSimulations = FontSimulations.None; + + if (style != FontStyle.Normal && glyphTypeface2.Style != style) + { + fontSimulations |= FontSimulations.Oblique; + } + + if ((int)weight >= 600 && glyphTypeface2.Weight < weight) + { + fontSimulations |= FontSimulations.Bold; + } + + if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream)) + { + using (stream) + { + if (fontManager.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface)) + { + //Add the TypographicFamilyName to the cache + if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) + { + AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, syntheticGlyphTypeface); + } + + foreach (var kvp in glyphTypeface2.FamilyNames) + { + AddGlyphTypefaceByFamilyName(kvp.Value, syntheticGlyphTypeface); + } + + return true; + } + + return false; + } + } + + return false; + + void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface) + { + var typefaces = _glyphTypefaceCache.GetOrAdd(familyName, + x => + { + return new ConcurrentDictionary(); + }); + + typefaces.TryAdd(key, glyphTypeface); + } + } + public abstract void Initialize(IFontManagerImpl fontManager); public abstract IEnumerator GetEnumerator(); diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs index a2fbdb69b0..2a30f0abd8 100644 --- a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs @@ -59,5 +59,16 @@ namespace Avalonia.Media.Fonts /// True, if the could get the list of typefaces, False otherwise. /// bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces); + + /// + /// Try to get a synthetic glyph typeface for given parameters. + /// + /// The glyph typeface we try to synthesize. + /// The font style. + /// The font weight. + /// The font stretch. + /// + /// Returns true if a synthetic glyph typface can be created; otherwise, false + bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface); } } diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index d59b1d1954..3a98a30b90 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -91,9 +91,11 @@ namespace Avalonia.Media.Fonts } //Try to create a synthetic glyph typeface - if (FontManager.TryCreateSyntheticGlyphTypeface(_fontManager.PlatformImpl, glyphTypeface, style, weight, out var syntheticGlyphTypeface)) + if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface)) { glyphTypeface = syntheticGlyphTypeface; + + return true; } } @@ -159,7 +161,7 @@ namespace Avalonia.Media.Fonts } } - bool IFontCollection2.TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) { familyTypefaces = null; diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 893bb7ec95..4595df43a9 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -245,7 +245,11 @@ namespace Avalonia.Headless public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) { - glyphTypeface = new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal); + glyphTypeface = new HeadlessGlyphTypefaceImpl( + FontFamily.DefaultFontFamilyName, + fontSimulations.HasFlag(FontSimulations.Oblique) ? FontStyle.Italic : FontStyle.Normal, + fontSimulations.HasFlag(FontSimulations.Bold) ? FontWeight.Bold : FontWeight.Normal, + FontStretch.Normal); TryCreateGlyphTypefaceCount++; diff --git a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs index fbf71795ba..a1ba9d92f8 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Media.Fonts; using Avalonia.UnitTests; @@ -18,6 +19,7 @@ namespace Avalonia.Skia.UnitTests.Media private const string s_manrope = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"; + [InlineData(FontWeight.SemiLight, FontStyle.Normal)] [InlineData(FontWeight.Bold, FontStyle.Italic)] [InlineData(FontWeight.Heavy, FontStyle.Oblique)] @@ -28,7 +30,7 @@ namespace Avalonia.Skia.UnitTests.Media { var source = new Uri(s_notoMono, UriKind.Absolute); - var fontCollection = new EmbeddedFontCollection(source, source); + var fontCollection = new TestEmbeddedFontCollection(source, source); fontCollection.Initialize(new CustomFontManagerImpl()); @@ -47,7 +49,7 @@ namespace Avalonia.Skia.UnitTests.Media { var source = new Uri(s_notoMono, UriKind.Absolute); - var fontCollection = new EmbeddedFontCollection(source, source); + var fontCollection = new TestEmbeddedFontCollection(source, source); fontCollection.Initialize(new CustomFontManagerImpl()); @@ -62,7 +64,7 @@ namespace Avalonia.Skia.UnitTests.Media { var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T", UriKind.Absolute); - var fontCollection = new EmbeddedFontCollection(source, source); + var fontCollection = new TestEmbeddedFontCollection(source, source); fontCollection.Initialize(new CustomFontManagerImpl()); @@ -79,7 +81,7 @@ namespace Avalonia.Skia.UnitTests.Media { var source = new Uri(s_manrope, UriKind.Absolute); - var fontCollection = new EmbeddedFontCollection(source, source); + var fontCollection = new TestEmbeddedFontCollection(source, source); fontCollection.Initialize(new CustomFontManagerImpl()); @@ -102,7 +104,7 @@ namespace Avalonia.Skia.UnitTests.Media { var source = new Uri(s_manrope, UriKind.Absolute); - var fontCollection = new TestEmbeddedFontCollection(source, source); + var fontCollection = new TestEmbeddedFontCollection(source, source, true); fontCollection.Initialize(new CustomFontManagerImpl()); @@ -120,11 +122,31 @@ namespace Avalonia.Skia.UnitTests.Media private class TestEmbeddedFontCollection : EmbeddedFontCollection { - public TestEmbeddedFontCollection(Uri key, Uri source) : base(key, source) + private bool _createSyntheticTypefaces; + + public TestEmbeddedFontCollection(Uri key, Uri source, bool createSyntheticTypefaces = false) : base(key, source) { + _createSyntheticTypefaces = createSyntheticTypefaces; } public IDictionary> GlyphTypefaceCache => _glyphTypefaceCache; + + public override bool TryCreateSyntheticGlyphTypeface( + IGlyphTypeface glyphTypeface, + FontStyle style, + FontWeight weight, + FontStretch stretch, + [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface) + { + if (!_createSyntheticTypefaces) + { + syntheticGlyphTypeface = null; + + return false; + } + + return base.TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out syntheticGlyphTypeface); + } } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs index 7d63b9a79e..2dc8d7e772 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs @@ -1,7 +1,10 @@ #nullable enable +using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Avalonia.Media; using Avalonia.Media.Fonts; using Avalonia.UnitTests; @@ -11,6 +14,9 @@ namespace Avalonia.Skia.UnitTests.Media { public class FontCollectionTests { + private const string NotoMono = + "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"; + [InlineData("Hello World 6", "Hello World 6", FontStyle.Normal, FontWeight.Normal)] [InlineData("Hello World Italic", "Hello World", FontStyle.Italic, FontWeight.Normal)] [InlineData("Hello World Italic Bold", "Hello World", FontStyle.Italic, FontWeight.Bold)] @@ -41,8 +47,6 @@ namespace Avalonia.Skia.UnitTests.Media Assert.True(fontCollection.TryGetGlyphTypeface("Arial", FontStyle.Normal, FontWeight.ExtraBlack, FontStretch.Normal, out var glyphTypeface)); - Assert.True(glyphTypeface.FontSimulations == FontSimulations.Bold); - Assert.True(fontCollection.GlyphTypefaceCache.TryGetValue("Arial", out var glyphTypefaces)); Assert.Equal(2, glyphTypefaces.Count); @@ -64,5 +68,108 @@ namespace Avalonia.Skia.UnitTests.Media public IDictionary> GlyphTypefaceCache => _glyphTypefaceCache; } + + [Fact] + public void Should_Use_Fallback() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var source = new Uri(NotoMono, UriKind.Absolute); + + var fallback = new FontFallback { FontFamily = new FontFamily("Arial"), UnicodeRange = new UnicodeRange('A', 'A') }; + + var fontCollection = new CustomizableFontCollection(source, source, new[] { fallback }); + + fontCollection.Initialize(new CustomFontManagerImpl()); + + Assert.True(fontCollection.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match)); + + Assert.Equal("Arial", match.FontFamily.Name); + } + } + + [Fact] + public void Should_Ignore_FontFamily() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var source = new Uri(NotoMono + "#Noto Mono", UriKind.Absolute); + + var ignorable = new FontFamily(new Uri(NotoMono, UriKind.Absolute), "Noto Mono"); + + var typeface = new Typeface(ignorable); + + var fontCollection = new CustomizableFontCollection(source, source, null, new[] { ignorable }); + + fontCollection.Initialize(new CustomFontManagerImpl()); + + Assert.False(fontCollection.TryCreateSyntheticGlyphTypeface( + typeface.GlyphTypeface, + FontStyle.Italic, + FontWeight.DemiBold, + FontStretch.Normal, + out var syntheticGlyphTypeface)); + } + } + + private class CustomizableFontCollection : EmbeddedFontCollection + { + private readonly IReadOnlyList? _fallbacks; + private readonly IReadOnlyList? _ignorables; + + public CustomizableFontCollection(Uri key, Uri source, IReadOnlyList? fallbacks = null, IReadOnlyList? ignorables = null) : base(key, source) + { + _fallbacks = fallbacks; + _ignorables = ignorables; + } + + public override bool TryMatchCharacter( + int codepoint, + FontStyle style, + FontWeight weight, + FontStretch stretch, + string? familyName, + CultureInfo? culture, + out Typeface match) + { + if(_fallbacks is not null) + { + foreach (var fallback in _fallbacks) + { + if (fallback.UnicodeRange.IsInRange(codepoint)) + { + match = new Typeface(fallback.FontFamily, style, weight, stretch); + + return true; + } + } + } + + return base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match); + } + + public override bool TryCreateSyntheticGlyphTypeface( + IGlyphTypeface glyphTypeface, + FontStyle style, + FontWeight weight, + FontStretch stretch, + [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface) + { + syntheticGlyphTypeface = null; + + if(_ignorables is not null) + { + foreach (var ignorable in _ignorables) + { + if (glyphTypeface.FamilyName == ignorable.Name || glyphTypeface is IGlyphTypeface2 glyphTypeface2 && glyphTypeface2.TypographicFamilyName == ignorable.Name) + { + return false; + } + } + } + + return base.TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out syntheticGlyphTypeface); + } + } } }