From 8da81002881485cea9b74086f128698033333505 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 26 Oct 2023 06:11:30 +0200 Subject: [PATCH] Fix SystemFontCollection nearest match lookup (#13365) * Try to find nearest matching glyphTypeface when no exact match could by found by SystemFontCollection * Fix glyphTypeface caching * Rework SystemFontCollection TryCreateGlyphTypeface * Make sure a failed glyphTypeface lookup is cached * Make sure to only try to get nearest match if we failed to load the font via font magager impl * Apply font simulations if possible * Enable font simulation for embedded fonts * Adjust simulated angle --- .../Media/Fonts/EmbeddedFontCollection.cs | 40 +++++++++++++++- .../Media/Fonts/SystemFontCollection.cs | 46 +++++++++++++++---- .../Platform/IFontManagerImpl.cs | 3 +- .../HeadlessPlatformStubs.cs | 6 ++- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 4 +- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 2 +- .../Media/FontManagerImpl.cs | 2 +- .../Media/CustomFontManagerImpl.cs | 4 +- .../Media/FontManagerTests.cs | 21 +++++++++ .../HarfBuzzFontManagerImpl.cs | 2 +- 10 files changed, 110 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index 4d4751db02..49cead719c 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -16,6 +16,8 @@ namespace Avalonia.Media.Fonts private readonly Uri _source; + private IFontManagerImpl? _fontManager; + public EmbeddedFontCollection(Uri key, Uri source) { _key = key; @@ -31,6 +33,8 @@ namespace Avalonia.Media.Fonts public override void Initialize(IFontManagerImpl fontManager) { + _fontManager = fontManager; + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); var fontAssets = FontFamilyLoader.LoadFontAssets(_source); @@ -39,7 +43,7 @@ namespace Avalonia.Media.Fonts { var stream = assetLoader.Open(fontAsset); - if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface)) + if (fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) { if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) { @@ -69,8 +73,42 @@ namespace Avalonia.Media.Fonts if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) { + 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) + { + if(_fontManager is not null && _fontManager.TryCreateGlyphTypeface(stream, fontSimulations, out glyphTypeface) && + glyphTypefaces.TryAdd(key, glyphTypeface)) + { + return true; + } + + return false; + } + } + } + return true; } } diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index a4a1fcf41d..c919257eee 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -47,18 +47,46 @@ namespace Avalonia.Media.Fonts var key = new FontCollectionKey(style, weight, stretch); - var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName, (key) => new ConcurrentDictionary()); + var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName, + (_) => new ConcurrentDictionary()); - if (!glyphTypefaces.TryGetValue(key, out glyphTypeface)) + if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) { - _fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface); + return glyphTypeface != null; + } + + if(!_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) || + !glyphTypeface.FamilyName.Contains(familyName)) + { + //Try to find nearest match if possible + TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface); + } - if (!glyphTypefaces.TryAdd(key, glyphTypeface)) + if(glyphTypeface is IGlyphTypeface2 glyphTypeface2) + { + var fontSimulations = FontSimulations.None; + + if(style != FontStyle.Normal && glyphTypeface2.Style != style) { - return false; + 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.PlatformImpl.TryCreateGlyphTypeface(stream, fontSimulations, out glyphTypeface); + } } } + glyphTypefaces.TryAdd(key, glyphTypeface); + return glyphTypeface != null; } @@ -87,7 +115,7 @@ namespace Avalonia.Media.Fonts { var stream = assetLoader.Open(fontAsset); - if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface)) + if (fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) { if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) { @@ -101,9 +129,9 @@ namespace Avalonia.Media.Fonts } var key = new FontCollectionKey( - glyphTypeface.Style, - glyphTypeface.Weight, - glyphTypeface.Stretch); + glyphTypeface.Style, + glyphTypeface.Weight, + glyphTypeface.Stretch); glyphTypefaces.TryAdd(key, glyphTypeface); } diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index 222e7196bb..f868bfaaed 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -53,10 +53,11 @@ namespace Avalonia.Platform /// Tries to create a glyph typeface from specified stream. /// /// A stream that holds the font's data. + /// Specifies algorithmic style simulations. /// The created glyphTypeface /// /// True, if the could create the glyph typeface, False otherwise. /// - bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); + bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); } } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 0311136d1c..2cf24ec66f 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -242,10 +242,12 @@ namespace Avalonia.Headless return true; } - public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) { glyphTypeface = new HeadlessGlyphTypefaceImpl(); + TryCreateGlyphTypefaceCount++; + return true; } } @@ -301,7 +303,7 @@ namespace Avalonia.Headless return true; } - public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) { glyphTypeface = new HeadlessGlyphTypefaceImpl(); diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index a97a198621..07caa09d52 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -105,13 +105,13 @@ namespace Avalonia.Skia return true; } - public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { var skTypeface = SKTypeface.FromStream(stream); if (skTypeface != null) { - glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations); return true; } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index 9878270d5d..c4045d9148 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -164,7 +164,7 @@ namespace Avalonia.Skia } public SKFont CreateSKFont(float size) - => new(_typeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0.0f) + => new(_typeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f) { LinearMetrics = true, Embolden = (FontSimulations & FontSimulations.Bold) != 0 diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 85bf2b6c4c..4c3e798d4b 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -88,7 +88,7 @@ namespace Avalonia.Direct2D1.Media return false; } - public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) { var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, new[] { stream }); diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 617ab952fa..22dd2dd8fe 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -86,11 +86,11 @@ namespace Avalonia.Skia.UnitTests.Media return true; } - public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) + public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) { var skTypeface = SKTypeface.FromStream(stream); - glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations); return true; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index ca7772f0c9..d70f770a11 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -239,5 +239,26 @@ namespace Avalonia.Skia.UnitTests.Media } } } + + + [Fact] + public void Should_Get_Nearest_Match_For_Custom_SystemFont() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + using (AvaloniaLocator.EnterScope()) + { + var systemFontCollection = FontManager.Current.SystemFonts as SystemFontCollection; + + Assert.NotNull(systemFontCollection); + + systemFontCollection.AddCustomFontSource(new Uri(s_fontUri, UriKind.Absolute)); + + Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Noto Mono", FontStyle.Italic), out var glyphTypeface)); + + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); + } + } + } } } diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs index 38897d28c5..3ec77307ef 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs @@ -57,7 +57,7 @@ namespace Avalonia.UnitTests return false; } - public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) { glyphTypeface = new HarfBuzzGlyphTypefaceImpl(stream);