diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 65ade1a4131..9aec42ae457 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -4,6 +4,8 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq; +using Avalonia.Logging; using Avalonia.Media.Fonts; using Avalonia.Platform; using Avalonia.Utilities; @@ -91,6 +93,8 @@ public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyp var fontFamily = typeface.FontFamily; + typeface = FontCollectionBase.GetImplicitTypeface(typeface); + if (typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName) { return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); @@ -115,7 +119,10 @@ public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyp } else { - if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, fontFamily.FamilyNames.PrimaryFamilyName, out glyphTypeface)) + //Replace known typographic names + var familyName = FontCollectionBase.NormalizeFamilyName(fontFamily.FamilyNames.PrimaryFamilyName); + + if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, familyName, out glyphTypeface)) { return true; } @@ -125,7 +132,10 @@ public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyp } else { - if (SystemFonts.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) + //Replace known typographic names + var familyName = FontCollectionBase.NormalizeFamilyName(fontFamily.FamilyNames.PrimaryFamilyName); + + if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { return true; } @@ -144,13 +154,20 @@ private bool TryGetGlyphTypefaceByKeyAndName(Typeface typeface, FontFamilyKey ke { var source = key.Source.EnsureAbsolute(key.BaseUri); - if (TryGetFontCollection(source, out var fontCollection) && - fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) + if (TryGetFontCollection(source, out var fontCollection)) { - if (glyphTypeface.FamilyName.Contains(familyName)) + if (fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, + out glyphTypeface)) { return true; } + + var logger = Logger.TryGet(LogEventLevel.Debug, "FontManager"); + + logger?.Log(this, + $"Font family '{familyName}' could not be found. Present font families: [{string.Join(",", fontCollection)}]"); + + return false; } glyphTypeface = null; diff --git a/src/Avalonia.Base/Media/FontWeight.cs b/src/Avalonia.Base/Media/FontWeight.cs index 5a4a4963a5e..5776e5e2854 100644 --- a/src/Avalonia.Base/Media/FontWeight.cs +++ b/src/Avalonia.Base/Media/FontWeight.cs @@ -1,3 +1,4 @@ +#pragma warning disable CA1069 namespace Avalonia.Media { /// @@ -22,7 +23,7 @@ public enum FontWeight /// /// Specifies an "ultra light" font weight. /// - UltraLight = 200, + UltraLight = ExtraLight, /// /// Specifies a "light" font weight. @@ -42,7 +43,7 @@ public enum FontWeight /// /// Specifies a "regular" font weight. /// - Regular = 400, + Regular = Normal, /// /// Specifies a "medium" font weight. @@ -52,7 +53,7 @@ public enum FontWeight /// /// Specifies a "demi-bold" font weight. /// - DemiBold = 600, + DemiBold = SemiBold, /// /// Specifies a "semi-bold" font weight. @@ -72,7 +73,7 @@ public enum FontWeight /// /// Specifies an "ultra bold" font weight. /// - UltraBold = 800, + UltraBold = ExtraBold, /// /// Specifies a "black" font weight. @@ -82,7 +83,12 @@ public enum FontWeight /// /// Specifies a "heavy" font weight. /// - Heavy = 900, + Heavy = Black, + + /// + /// Specifies a "solid" font weight. + /// + Solid = Black, /// /// Specifies an "extra black" font weight. diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index 49cead719c1..1ad5d17ee81 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -113,6 +113,9 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon } } + //Replace known typographic names + familyName = NormalizeFamilyName(familyName); + //Try to find a partially matching font for (var i = 0; i < Count; i++) { diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index 3daa19c788b..b7f72d4c17a 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Text.RegularExpressions; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media.Fonts { @@ -255,5 +257,113 @@ internal static bool TryFindWeightFallback( return false; } + + private static readonly List s_knownNames = ["Solid", "Regular", "Bold", "Black", "Normal", "Thin", "Italic"]; + + internal static string NormalizeFamilyName(string familyName) + { + //Return early if no separator is present. + if (!familyName.Contains(' ')) + { + return familyName; + } + + foreach (var name in s_knownNames) + { + familyName = Regex.Replace(familyName, name, "", RegexOptions.IgnoreCase); + } + + return familyName.Trim(); + } + + internal static Typeface GetImplicitTypeface(Typeface typeface) + { + var familyName = typeface.FontFamily.FamilyNames.PrimaryFamilyName; + + //Return early if no separator is present. + if (!familyName.Contains(' ')) + { + return typeface; + } + + var style = typeface.Style; + var weight = typeface.Weight; + var stretch = typeface.Stretch; + + if(TryGetStyle(familyName, out var foundStyle)) + { + style = foundStyle; + } + + if(TryGetWeight(familyName, out var foundWeight)) + { + weight = foundWeight; + } + + if(TryGetStretch(familyName, out var foundStretch)) + { + stretch = foundStretch; + } + + return new Typeface(typeface.FontFamily, style, weight, stretch); + + } + + internal static bool TryGetWeight(string familyName, out FontWeight weight) + { + weight = FontWeight.Normal; + + var tokenizer = new StringTokenizer(familyName, ' '); + + tokenizer.ReadString(); + + while (tokenizer.TryReadString(out var weightString)) + { + if (Enum.TryParse(weightString, true, out weight)) + { + return true; + } + } + + return false; + } + + internal static bool TryGetStyle(string familyName, out FontStyle style) + { + style = FontStyle.Normal; + + var tokenizer = new StringTokenizer(familyName, ' '); + + tokenizer.ReadString(); + + while (tokenizer.TryReadString(out var styleString)) + { + if (Enum.TryParse(styleString, true, out style)) + { + return true; + } + } + + return false; + } + + internal static bool TryGetStretch(string familyName, out FontStretch stretch) + { + stretch = FontStretch.Normal; + + var tokenizer = new StringTokenizer(familyName, ' '); + + tokenizer.ReadString(); + + while (tokenizer.TryReadString(out var stretchString)) + { + if (Enum.TryParse(stretchString, true, out stretch)) + { + return true; + } + } + + return false; + } } } diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index c919257eee9..2e11410307d 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -15,7 +15,7 @@ internal class SystemFontCollection : FontCollectionBase public SystemFontCollection(FontManager fontManager) { _fontManager = fontManager; - _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().ToList(); + _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().Where(x=> !string.IsNullOrEmpty(x)).ToList(); } public override Uri Key => FontManager.SystemFontsKey; @@ -47,47 +47,83 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon var key = new FontCollectionKey(style, weight, stretch); - var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName, + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) + { + return glyphTypeface != null; + } + } + + glyphTypefaces ??= _glyphTypefaceCache.GetOrAdd(familyName, (_) => new ConcurrentDictionary()); - if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) + //Try top create the font via system font manager + if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) { + glyphTypefaces.TryAdd(key, glyphTypeface); + + return true; + } + + //Try to find nearest match if possible + if (!TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + { + if (TryGetGlyphTypeface(_fontManager.DefaultFontFamily.Name, style, weight, stretch, out glyphTypeface)) + { + glyphTypefaces.TryAdd(key, glyphTypeface); + } + return glyphTypeface != null; } - if(!_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) || - !glyphTypeface.FamilyName.Contains(familyName)) + if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, out var syntheticGlyphTypeface)) + { + glyphTypefaces.TryAdd(key, syntheticGlyphTypeface); + + glyphTypeface = syntheticGlyphTypeface; + } + else { - //Try to find nearest match if possible - TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface); + glyphTypefaces.TryAdd(key, glyphTypeface); } - if(glyphTypeface is IGlyphTypeface2 glyphTypeface2) + return true; + + } + + private bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, + [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface) + { + if (glyphTypeface is IGlyphTypeface2 glyphTypeface2) { var fontSimulations = FontSimulations.None; - if(style != FontStyle.Normal && glyphTypeface2.Style != style) + if (style != FontStyle.Normal && glyphTypeface2.Style != style) { fontSimulations |= FontSimulations.Oblique; } - if((int)weight >= 600 && glyphTypeface2.Weight != weight) + if ((int)weight >= 600 && glyphTypeface2.Weight != weight) { fontSimulations |= FontSimulations.Bold; } - if(fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream)) + if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream)) { using (stream) { - _fontManager.PlatformImpl.TryCreateGlyphTypeface(stream, fontSimulations, out glyphTypeface); + _fontManager.PlatformImpl.TryCreateGlyphTypeface(stream, fontSimulations, + out syntheticGlyphTypeface); + + return syntheticGlyphTypeface != null; } } } - glyphTypefaces.TryAdd(key, glyphTypeface); + syntheticGlyphTypeface = null; - return glyphTypeface != null; + return false; } public override void Initialize(IFontManagerImpl fontManager) diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index c4045d9148c..f6bbd72244a 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -51,9 +51,11 @@ public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations) FontSimulations = fontSimulations; - Weight = (FontWeight)typeface.FontWeight; + Weight = (fontSimulations & FontSimulations.Bold) != 0 ? FontWeight.Bold : (FontWeight)typeface.FontWeight; - Style = typeface.FontSlant.ToAvalonia(); + Style = (fontSimulations & FontSimulations.Oblique) != 0 ? + FontStyle.Italic : + typeface.FontSlant.ToAvalonia(); Stretch = (FontStretch)typeface.FontStyle.Width; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs index 006abe9278e..2cd5cb855bd 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs @@ -64,5 +64,22 @@ public void Should_Get_Typeface_For_Partial_FamilyName() Assert.Equal("Twitter Color Emoji", glyphTypeface.FamilyName); } } + + [Fact] + public void Should_Get_Typeface_For_Known_Typographic_Name() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests", UriKind.Absolute); + + var fontCollection = new EmbeddedFontCollection(source, source); + + fontCollection.Initialize(new CustomFontManagerImpl()); + + Assert.True(fontCollection.TryGetGlyphTypeface("Twitter Regular", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface)); + + Assert.Equal("Twitter Color Emoji", glyphTypeface.FamilyName); + } + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index 322645a914e..f7c02df5349 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -229,11 +229,8 @@ public void Should_Use_Custom_SystemFont() { using (AvaloniaLocator.EnterScope()) { - var systemFontCollection = FontManager.Current.SystemFonts as SystemFontCollection; - - Assert.NotNull(systemFontCollection); - - systemFontCollection.AddCustomFontSource(new Uri(s_fontUri, UriKind.Absolute)); + FontManager.Current.AddFontCollection(new EmbeddedFontCollection(FontManager.SystemFontsKey, + new Uri(s_fontUri, UriKind.Absolute))); Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Noto Mono"), out var glyphTypeface)); @@ -250,15 +247,32 @@ public void Should_Get_Nearest_Match_For_Custom_SystemFont() { using (AvaloniaLocator.EnterScope()) { - var systemFontCollection = FontManager.Current.SystemFonts as SystemFontCollection; + FontManager.Current.AddFontCollection(new EmbeddedFontCollection(FontManager.SystemFontsKey, + new Uri(s_fontUri, UriKind.Absolute))); - Assert.NotNull(systemFontCollection); + Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Noto Mono", FontStyle.Italic), out var glyphTypeface)); - systemFontCollection.AddCustomFontSource(new Uri(s_fontUri, UriKind.Absolute)); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); + } + } + } - Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Noto Mono", FontStyle.Italic), out var glyphTypeface)); + [Fact] + public void Should_Get_Implicit_Typeface() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + using (AvaloniaLocator.EnterScope()) + { + FontManager.Current.AddFontCollection(new EmbeddedFontCollection(FontManager.SystemFontsKey, + new Uri(s_fontUri, UriKind.Absolute))); + + Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Noto Mono Italic"), + out var glyphTypeface)); Assert.Equal("Noto Mono", glyphTypeface.FamilyName); + + Assert.Equal(FontStyle.Italic, glyphTypeface.Style); } } }