Browse Source

FontManager adjustments (#15819)

* Remove Regex usage from font family name normalization
Fix font loading issues

* Move implicit typeface resolution to the font collection
pull/15846/head
Benedikt Stebner 2 years ago
committed by GitHub
parent
commit
22231f5b91
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 14
      src/Avalonia.Base/Media/FontManager.cs
  2. 11
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  3. 78
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  4. 53
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  5. 24
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  6. 4
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  7. 17
      tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs
  8. 28
      tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs
  9. 22
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

14
src/Avalonia.Base/Media/FontManager.cs

@ -93,18 +93,16 @@ namespace Avalonia.Media
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 @@ namespace Avalonia.Media
}
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 @@ namespace Avalonia.Media
}
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))
{

11
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@ -69,6 +69,14 @@ namespace Avalonia.Media.Fonts
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 @@ namespace Avalonia.Media.Fonts
}
}
//Replace known typographic names
familyName = NormalizeFamilyName(familyName);
//Try to find a partially matching font
for (var i = 0; i < Count; i++)
{

78
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@ -4,7 +4,6 @@ using System.Collections.Concurrent;
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 @@ namespace Avalonia.Media.Fonts
return false;
}
private static readonly List<string> 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 @@ namespace Avalonia.Media.Fonts
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 @@ namespace Avalonia.Media.Fonts
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 @@ namespace Avalonia.Media.Fonts
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 @@ namespace Avalonia.Media.Fonts
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;

53
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@ -45,6 +45,14 @@ namespace Avalonia.Media.Fonts
{
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 @@ namespace Avalonia.Media.Fonts
glyphTypefaces ??= _glyphTypefaceCache.GetOrAdd(familyName,
(_) => new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>());
//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,

24
src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -83,6 +83,14 @@ namespace Avalonia.Headless
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 @@ namespace Avalonia.Headless
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 @@ namespace Avalonia.Headless
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 @@ namespace Avalonia.Headless
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;
}

4
tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs

@ -180,7 +180,9 @@ namespace Avalonia.Base.UnitTests.Media
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()

17
tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs

@ -64,22 +64,5 @@ namespace Avalonia.Skia.UnitTests.Media
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);
}
}
}
}

28
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);
}
}
}

22
tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

@ -276,5 +276,27 @@ namespace Avalonia.Skia.UnitTests.Media
}
}
}
[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);
}
}
}
}
}

Loading…
Cancel
Save