Skip to content

Commit 1121879

Browse files
authored
Try to normalize family names for embedded fonts (#15703)
* Try to normalize family names in case some known name is included in the requested family name * Implicit Typeface loading * Avoid null family names * Fix system font collection * Fix unit tests on macOS
1 parent b30894c commit 1121879

8 files changed

Lines changed: 240 additions & 35 deletions

File tree

src/Avalonia.Base/Media/FontManager.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Diagnostics;
55
using System.Diagnostics.CodeAnalysis;
66
using System.Globalization;
7+
using System.Linq;
8+
using Avalonia.Logging;
79
using Avalonia.Media.Fonts;
810
using Avalonia.Platform;
911
using Avalonia.Utilities;
@@ -91,6 +93,8 @@ public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyp
9193

9294
var fontFamily = typeface.FontFamily;
9395

96+
typeface = FontCollectionBase.GetImplicitTypeface(typeface);
97+
9498
if (typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName)
9599
{
96100
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
115119
}
116120
else
117121
{
118-
if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, fontFamily.FamilyNames.PrimaryFamilyName, out glyphTypeface))
122+
//Replace known typographic names
123+
var familyName = FontCollectionBase.NormalizeFamilyName(fontFamily.FamilyNames.PrimaryFamilyName);
124+
125+
if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, familyName, out glyphTypeface))
119126
{
120127
return true;
121128
}
@@ -125,7 +132,10 @@ public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyp
125132
}
126133
else
127134
{
128-
if (SystemFonts.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
135+
//Replace known typographic names
136+
var familyName = FontCollectionBase.NormalizeFamilyName(fontFamily.FamilyNames.PrimaryFamilyName);
137+
138+
if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
129139
{
130140
return true;
131141
}
@@ -144,13 +154,20 @@ private bool TryGetGlyphTypefaceByKeyAndName(Typeface typeface, FontFamilyKey ke
144154
{
145155
var source = key.Source.EnsureAbsolute(key.BaseUri);
146156

147-
if (TryGetFontCollection(source, out var fontCollection) &&
148-
fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
157+
if (TryGetFontCollection(source, out var fontCollection))
149158
{
150-
if (glyphTypeface.FamilyName.Contains(familyName))
159+
if (fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch,
160+
out glyphTypeface))
151161
{
152162
return true;
153163
}
164+
165+
var logger = Logger.TryGet(LogEventLevel.Debug, "FontManager");
166+
167+
logger?.Log(this,
168+
$"Font family '{familyName}' could not be found. Present font families: [{string.Join(",", fontCollection)}]");
169+
170+
return false;
154171
}
155172

156173
glyphTypeface = null;

src/Avalonia.Base/Media/FontWeight.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#pragma warning disable CA1069
12
namespace Avalonia.Media
23
{
34
/// <summary>
@@ -22,7 +23,7 @@ public enum FontWeight
2223
/// <summary>
2324
/// Specifies an "ultra light" font weight.
2425
/// </summary>
25-
UltraLight = 200,
26+
UltraLight = ExtraLight,
2627

2728
/// <summary>
2829
/// Specifies a "light" font weight.
@@ -42,7 +43,7 @@ public enum FontWeight
4243
/// <summary>
4344
/// Specifies a "regular" font weight.
4445
/// </summary>
45-
Regular = 400,
46+
Regular = Normal,
4647

4748
/// <summary>
4849
/// Specifies a "medium" font weight.
@@ -52,7 +53,7 @@ public enum FontWeight
5253
/// <summary>
5354
/// Specifies a "demi-bold" font weight.
5455
/// </summary>
55-
DemiBold = 600,
56+
DemiBold = SemiBold,
5657

5758
/// <summary>
5859
/// Specifies a "semi-bold" font weight.
@@ -72,7 +73,7 @@ public enum FontWeight
7273
/// <summary>
7374
/// Specifies an "ultra bold" font weight.
7475
/// </summary>
75-
UltraBold = 800,
76+
UltraBold = ExtraBold,
7677

7778
/// <summary>
7879
/// Specifies a "black" font weight.
@@ -82,7 +83,12 @@ public enum FontWeight
8283
/// <summary>
8384
/// Specifies a "heavy" font weight.
8485
/// </summary>
85-
Heavy = 900,
86+
Heavy = Black,
87+
88+
/// <summary>
89+
/// Specifies a "solid" font weight.
90+
/// </summary>
91+
Solid = Black,
8692

8793
/// <summary>
8894
/// Specifies an "extra black" font weight.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon
113113
}
114114
}
115115

116+
//Replace known typographic names
117+
familyName = NormalizeFamilyName(familyName);
118+
116119
//Try to find a partially matching font
117120
for (var i = 0; i < Count; i++)
118121
{

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

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
using System.Collections.Generic;
55
using System.Diagnostics.CodeAnalysis;
66
using System.Globalization;
7+
using System.Text.RegularExpressions;
78
using Avalonia.Platform;
9+
using Avalonia.Utilities;
810

911
namespace Avalonia.Media.Fonts
1012
{
@@ -255,5 +257,113 @@ internal static bool TryFindWeightFallback(
255257

256258
return false;
257259
}
260+
261+
private static readonly List<string> s_knownNames = ["Solid", "Regular", "Bold", "Black", "Normal", "Thin", "Italic"];
262+
263+
internal static string NormalizeFamilyName(string familyName)
264+
{
265+
//Return early if no separator is present.
266+
if (!familyName.Contains(' '))
267+
{
268+
return familyName;
269+
}
270+
271+
foreach (var name in s_knownNames)
272+
{
273+
familyName = Regex.Replace(familyName, name, "", RegexOptions.IgnoreCase);
274+
}
275+
276+
return familyName.Trim();
277+
}
278+
279+
internal static Typeface GetImplicitTypeface(Typeface typeface)
280+
{
281+
var familyName = typeface.FontFamily.FamilyNames.PrimaryFamilyName;
282+
283+
//Return early if no separator is present.
284+
if (!familyName.Contains(' '))
285+
{
286+
return typeface;
287+
}
288+
289+
var style = typeface.Style;
290+
var weight = typeface.Weight;
291+
var stretch = typeface.Stretch;
292+
293+
if(TryGetStyle(familyName, out var foundStyle))
294+
{
295+
style = foundStyle;
296+
}
297+
298+
if(TryGetWeight(familyName, out var foundWeight))
299+
{
300+
weight = foundWeight;
301+
}
302+
303+
if(TryGetStretch(familyName, out var foundStretch))
304+
{
305+
stretch = foundStretch;
306+
}
307+
308+
return new Typeface(typeface.FontFamily, style, weight, stretch);
309+
310+
}
311+
312+
internal static bool TryGetWeight(string familyName, out FontWeight weight)
313+
{
314+
weight = FontWeight.Normal;
315+
316+
var tokenizer = new StringTokenizer(familyName, ' ');
317+
318+
tokenizer.ReadString();
319+
320+
while (tokenizer.TryReadString(out var weightString))
321+
{
322+
if (Enum.TryParse(weightString, true, out weight))
323+
{
324+
return true;
325+
}
326+
}
327+
328+
return false;
329+
}
330+
331+
internal static bool TryGetStyle(string familyName, out FontStyle style)
332+
{
333+
style = FontStyle.Normal;
334+
335+
var tokenizer = new StringTokenizer(familyName, ' ');
336+
337+
tokenizer.ReadString();
338+
339+
while (tokenizer.TryReadString(out var styleString))
340+
{
341+
if (Enum.TryParse(styleString, true, out style))
342+
{
343+
return true;
344+
}
345+
}
346+
347+
return false;
348+
}
349+
350+
internal static bool TryGetStretch(string familyName, out FontStretch stretch)
351+
{
352+
stretch = FontStretch.Normal;
353+
354+
var tokenizer = new StringTokenizer(familyName, ' ');
355+
356+
tokenizer.ReadString();
357+
358+
while (tokenizer.TryReadString(out var stretchString))
359+
{
360+
if (Enum.TryParse(stretchString, true, out stretch))
361+
{
362+
return true;
363+
}
364+
}
365+
366+
return false;
367+
}
258368
}
259369
}

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

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ internal class SystemFontCollection : FontCollectionBase
1515
public SystemFontCollection(FontManager fontManager)
1616
{
1717
_fontManager = fontManager;
18-
_familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().ToList();
18+
_familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().Where(x=> !string.IsNullOrEmpty(x)).ToList();
1919
}
2020

2121
public override Uri Key => FontManager.SystemFontsKey;
@@ -47,47 +47,83 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon
4747

4848
var key = new FontCollectionKey(style, weight, stretch);
4949

50-
var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName,
50+
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
51+
{
52+
if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
53+
{
54+
return glyphTypeface != null;
55+
}
56+
}
57+
58+
glyphTypefaces ??= _glyphTypefaceCache.GetOrAdd(familyName,
5159
(_) => new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>());
5260

53-
if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
61+
//Try top create the font via system font manager
62+
if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
5463
{
64+
glyphTypefaces.TryAdd(key, glyphTypeface);
65+
66+
return true;
67+
}
68+
69+
//Try to find nearest match if possible
70+
if (!TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
71+
{
72+
if (TryGetGlyphTypeface(_fontManager.DefaultFontFamily.Name, style, weight, stretch, out glyphTypeface))
73+
{
74+
glyphTypefaces.TryAdd(key, glyphTypeface);
75+
}
76+
5577
return glyphTypeface != null;
5678
}
5779

58-
if(!_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) ||
59-
!glyphTypeface.FamilyName.Contains(familyName))
80+
if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, out var syntheticGlyphTypeface))
81+
{
82+
glyphTypefaces.TryAdd(key, syntheticGlyphTypeface);
83+
84+
glyphTypeface = syntheticGlyphTypeface;
85+
}
86+
else
6087
{
61-
//Try to find nearest match if possible
62-
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface);
88+
glyphTypefaces.TryAdd(key, glyphTypeface);
6389
}
6490

65-
if(glyphTypeface is IGlyphTypeface2 glyphTypeface2)
91+
return true;
92+
93+
}
94+
95+
private bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight,
96+
[NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
97+
{
98+
if (glyphTypeface is IGlyphTypeface2 glyphTypeface2)
6699
{
67100
var fontSimulations = FontSimulations.None;
68101

69-
if(style != FontStyle.Normal && glyphTypeface2.Style != style)
102+
if (style != FontStyle.Normal && glyphTypeface2.Style != style)
70103
{
71104
fontSimulations |= FontSimulations.Oblique;
72105
}
73106

74-
if((int)weight >= 600 && glyphTypeface2.Weight != weight)
107+
if ((int)weight >= 600 && glyphTypeface2.Weight != weight)
75108
{
76109
fontSimulations |= FontSimulations.Bold;
77110
}
78111

79-
if(fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream))
112+
if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream))
80113
{
81114
using (stream)
82115
{
83-
_fontManager.PlatformImpl.TryCreateGlyphTypeface(stream, fontSimulations, out glyphTypeface);
116+
_fontManager.PlatformImpl.TryCreateGlyphTypeface(stream, fontSimulations,
117+
out syntheticGlyphTypeface);
118+
119+
return syntheticGlyphTypeface != null;
84120
}
85121
}
86122
}
87123

88-
glyphTypefaces.TryAdd(key, glyphTypeface);
124+
syntheticGlyphTypeface = null;
89125

90-
return glyphTypeface != null;
126+
return false;
91127
}
92128

93129
public override void Initialize(IFontManagerImpl fontManager)

src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations)
5151

5252
FontSimulations = fontSimulations;
5353

54-
Weight = (FontWeight)typeface.FontWeight;
54+
Weight = (fontSimulations & FontSimulations.Bold) != 0 ? FontWeight.Bold : (FontWeight)typeface.FontWeight;
5555

56-
Style = typeface.FontSlant.ToAvalonia();
56+
Style = (fontSimulations & FontSimulations.Oblique) != 0 ?
57+
FontStyle.Italic :
58+
typeface.FontSlant.ToAvalonia();
5759

5860
Stretch = (FontStretch)typeface.FontStyle.Width;
5961
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,22 @@ public void Should_Get_Typeface_For_Partial_FamilyName()
6464
Assert.Equal("Twitter Color Emoji", glyphTypeface.FamilyName);
6565
}
6666
}
67+
68+
[Fact]
69+
public void Should_Get_Typeface_For_Known_Typographic_Name()
70+
{
71+
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
72+
{
73+
var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests", UriKind.Absolute);
74+
75+
var fontCollection = new EmbeddedFontCollection(source, source);
76+
77+
fontCollection.Initialize(new CustomFontManagerImpl());
78+
79+
Assert.True(fontCollection.TryGetGlyphTypeface("Twitter Regular", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface));
80+
81+
Assert.Equal("Twitter Color Emoji", glyphTypeface.FamilyName);
82+
}
83+
}
6784
}
6885
}

0 commit comments

Comments
 (0)