diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 9aec42ae457..88e332f3346 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -93,18 +93,16 @@ 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); } - if (fontFamily.Key is FontFamilyKey) + if (fontFamily.Key != null) { if (fontFamily.Key is CompositeFontFamilyKey compositeKey) { - for (int i = 0; i < compositeKey.Keys.Count; i++) + for (var i = 0; i < compositeKey.Keys.Count; i++) { var key = compositeKey.Keys[i]; @@ -119,9 +117,8 @@ public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyp } else { - //Replace known typographic names - var familyName = FontCollectionBase.NormalizeFamilyName(fontFamily.FamilyNames.PrimaryFamilyName); - + var familyName = fontFamily.FamilyNames.PrimaryFamilyName; + if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, familyName, out glyphTypeface)) { return true; @@ -132,8 +129,7 @@ public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyp } else { - //Replace known typographic names - var familyName = FontCollectionBase.NormalizeFamilyName(fontFamily.FamilyNames.PrimaryFamilyName); + var familyName = fontFamily.FamilyNames.PrimaryFamilyName; if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index 1ad5d17ee81..8a958621c3d 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -69,6 +69,14 @@ public override void Initialize(IFontManagerImpl fontManager) public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { + var typeface = GetImplicitTypeface(new Typeface(familyName, style, weight, stretch), out familyName); + + style = typeface.Style; + + weight = typeface.Weight; + + stretch = typeface.Stretch; + var key = new FontCollectionKey(style, weight, stretch); if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) @@ -113,9 +121,6 @@ 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 b7f72d4c17a..bbf10fdca82 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Text.RegularExpressions; using Avalonia.Platform; using Avalonia.Utilities; @@ -258,30 +257,12 @@ 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) + internal static Typeface GetImplicitTypeface(Typeface typeface, out string normalizedFamilyName) { - var familyName = typeface.FontFamily.FamilyNames.PrimaryFamilyName; + normalizedFamilyName = typeface.FontFamily.FamilyNames.PrimaryFamilyName; //Return early if no separator is present. - if (!familyName.Contains(' ')) + if (!normalizedFamilyName.Contains(' ')) { return typeface; } @@ -290,26 +271,27 @@ internal static Typeface GetImplicitTypeface(Typeface typeface) var weight = typeface.Weight; var stretch = typeface.Stretch; - if(TryGetStyle(familyName, out var foundStyle)) + if(TryGetStyle(ref normalizedFamilyName, out var foundStyle)) { style = foundStyle; } - if(TryGetWeight(familyName, out var foundWeight)) + if(TryGetWeight(ref normalizedFamilyName, out var foundWeight)) { weight = foundWeight; } - if(TryGetStretch(familyName, out var foundStretch)) + if(TryGetStretch(ref normalizedFamilyName, out var foundStretch)) { stretch = foundStretch; } + //Preserve old font source return new Typeface(typeface.FontFamily, style, weight, stretch); } - internal static bool TryGetWeight(string familyName, out FontWeight weight) + internal static bool TryGetWeight(ref string familyName, out FontWeight weight) { weight = FontWeight.Normal; @@ -319,16 +301,25 @@ internal static bool TryGetWeight(string familyName, out FontWeight weight) while (tokenizer.TryReadString(out var weightString)) { - if (Enum.TryParse(weightString, true, out weight)) + if (new StringTokenizer(weightString).TryReadInt32(out _)) { - return true; + continue; } + + if (!Enum.TryParse(weightString, true, out weight)) + { + continue; + } + + familyName = familyName.Replace(" " + weightString, "").TrimEnd(); + + return true; } return false; } - internal static bool TryGetStyle(string familyName, out FontStyle style) + internal static bool TryGetStyle(ref string familyName, out FontStyle style) { style = FontStyle.Normal; @@ -338,16 +329,26 @@ internal static bool TryGetStyle(string familyName, out FontStyle style) while (tokenizer.TryReadString(out var styleString)) { - if (Enum.TryParse(styleString, true, out style)) + //Do not try to parse an integer + if (new StringTokenizer(styleString).TryReadInt32(out _)) { - return true; + continue; + } + + if (!Enum.TryParse(styleString, true, out style)) + { + continue; } + + familyName = familyName.Replace(" " + styleString, "").TrimEnd(); + + return true; } return false; } - internal static bool TryGetStretch(string familyName, out FontStretch stretch) + internal static bool TryGetStretch(ref string familyName, out FontStretch stretch) { stretch = FontStretch.Normal; @@ -357,10 +358,19 @@ internal static bool TryGetStretch(string familyName, out FontStretch stretch) while (tokenizer.TryReadString(out var stretchString)) { - if (Enum.TryParse(stretchString, true, out stretch)) + if (new StringTokenizer(stretchString).TryReadInt32(out _)) { - return true; + continue; + } + + if (!Enum.TryParse(stretchString, true, out stretch)) + { + continue; } + + familyName = familyName.Replace(" " + stretchString, "").TrimEnd(); + + return true; } return false; diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index 2e11410307d..90d4a446999 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -45,6 +45,14 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon { glyphTypeface = null; + var typeface = GetImplicitTypeface(new Typeface(familyName, style, weight, stretch), out familyName); + + style = typeface.Style; + + weight = typeface.Weight; + + stretch = typeface.Stretch; + var key = new FontCollectionKey(style, weight, stretch); if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) @@ -58,38 +66,39 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon glyphTypefaces ??= _glyphTypefaceCache.GetOrAdd(familyName, (_) => new ConcurrentDictionary()); - //Try top create the font via system font manager - if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + //Try to create the glyph typeface via system font manager + if (!_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, + out glyphTypeface)) { - glyphTypefaces.TryAdd(key, glyphTypeface); + glyphTypefaces.TryAdd(key, null); - return true; + return false; } - //Try to find nearest match if possible - if (!TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + var createdKey = + new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); + + //No exact match + if (createdKey != key) { - if (TryGetGlyphTypeface(_fontManager.DefaultFontFamily.Name, style, weight, stretch, out glyphTypeface)) + //Try to find nearest match if possible + if (!TryGetNearestMatch(glyphTypefaces, key, out var nearestMatch)) { - glyphTypefaces.TryAdd(key, glyphTypeface); + glyphTypeface = nearestMatch; + } + else + { + //Try to create a synthetic glyph typeface + if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, out var syntheticGlyphTypeface)) + { + glyphTypeface = syntheticGlyphTypeface; + } } - - return glyphTypeface != null; - } - - if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, out var syntheticGlyphTypeface)) - { - glyphTypefaces.TryAdd(key, syntheticGlyphTypeface); - - glyphTypeface = syntheticGlyphTypeface; - } - else - { - glyphTypefaces.TryAdd(key, glyphTypeface); } - return true; + glyphTypefaces.TryAdd(key, glyphTypeface); + return glyphTypeface != null; } private bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 2cf24ec66f6..fe2f8d42e4b 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -83,6 +83,14 @@ public void Dispose() { } internal class HeadlessGlyphTypefaceImpl : IGlyphTypeface { + public HeadlessGlyphTypefaceImpl(string familyName, FontStyle style, FontWeight weight, FontStretch stretch) + { + FamilyName = familyName; + Style = style; + Weight = weight; + Stretch = stretch; + } + public FontMetrics Metrics => new FontMetrics { DesignEmHeight = 10, @@ -100,13 +108,13 @@ internal class HeadlessGlyphTypefaceImpl : IGlyphTypeface public FontSimulations FontSimulations => FontSimulations.None; - public string FamilyName => "$Default"; + public string FamilyName { get; } - public FontWeight Weight => FontWeight.Normal; + public FontWeight Weight { get; } - public FontStyle Style => FontStyle.Normal; + public FontStyle Style { get; } - public FontStretch Stretch => FontStretch.Normal; + public FontStretch Stretch { get; } public void Dispose() { @@ -237,14 +245,14 @@ public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, F return false; } - glyphTypeface = new HeadlessGlyphTypefaceImpl(); + glyphTypeface = new HeadlessGlyphTypefaceImpl(familyName, style, weight, stretch); return true; } public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) { - glyphTypeface = new HeadlessGlyphTypefaceImpl(); + glyphTypeface = new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal); TryCreateGlyphTypefaceCount++; @@ -298,14 +306,14 @@ public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, F return false; } - glyphTypeface = new HeadlessGlyphTypefaceImpl(); + glyphTypeface = new HeadlessGlyphTypefaceImpl(familyName, style, weight, stretch); return true; } public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) { - glyphTypeface = new HeadlessGlyphTypefaceImpl(); + glyphTypeface = new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal); return true; } diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index 69d7fc49167..229b63ae1a4 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -180,7 +180,9 @@ private static GlyphRun CreateGlyphRun(double[] glyphAdvances, int[] glyphCluste glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]); } - return new GlyphRun(new HeadlessGlyphTypefaceImpl(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel); + return new GlyphRun( + new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, + FontStretch.Normal), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel); } private static IDisposable Start() diff --git a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs index 2cd5cb855bd..006abe9278e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs @@ -64,22 +64,5 @@ 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/FontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs new file mode 100644 index 00000000000..182024dc88d --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs @@ -0,0 +1,28 @@ +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Xunit; + +namespace Avalonia.Skia.UnitTests.Media +{ + public class FontCollectionTests + { + [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)] + [InlineData("FontAwesome 6 Free Regular", "FontAwesome 6 Free", FontStyle.Normal, FontWeight.Normal)] + [InlineData("FontAwesome 6 Free Solid", "FontAwesome 6 Free", FontStyle.Normal, FontWeight.Solid)] + [InlineData("FontAwesome 6 Brands", "FontAwesome 6 Brands", FontStyle.Normal, FontWeight.Normal)] + [Theory] + public void Should_Get_Implicit_Typeface(string input, string familyName, FontStyle style, FontWeight weight) + { + var typeface = new Typeface(input); + + var result = FontCollectionBase.GetImplicitTypeface(typeface, out var normalizedFamilyName); + + Assert.Equal(familyName, normalizedFamilyName); + Assert.Equal(style, result.Style); + Assert.Equal(weight, result.Weight); + Assert.Equal(FontStretch.Normal, result.Stretch); + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index f7c02df5349..85bec100beb 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -276,5 +276,27 @@ public void Should_Get_Implicit_Typeface() } } } + + [Fact] + public void Should_Create_Synthetic_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", FontStyle.Italic, FontWeight.Bold), + out var glyphTypeface)); + + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); + + Assert.Equal(FontWeight.Bold, glyphTypeface.Weight); + + Assert.Equal(FontStyle.Italic, glyphTypeface.Style); + } + } + } } }