Skip to content

Commit c06c943

Browse files
committed
Introduce Typeface.Normalize
Introduce CustomFontCollection
1 parent 62054db commit c06c943

File tree

6 files changed

+257
-78
lines changed

6 files changed

+257
-78
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Globalization;
5+
using System.IO;
6+
using Avalonia.Platform;
7+
8+
namespace Avalonia.Media.Fonts
9+
{
10+
internal class CustomFontCollection : FontCollectionBase
11+
{
12+
protected List<FontFamily> _fontFamilies { get; } = new List<FontFamily>();
13+
14+
public CustomFontCollection(Uri key)
15+
{
16+
Key = key;
17+
}
18+
19+
public override FontFamily this[int index] => throw new NotImplementedException();
20+
21+
public override Uri Key { get; }
22+
23+
public override int Count => _fontFamilies.Count;
24+
25+
public override void Initialize(IFontManagerImpl fontManager)
26+
{
27+
// Nothing to do
28+
}
29+
30+
public override IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
31+
32+
public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
33+
{
34+
var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
35+
36+
style = typeface.Style;
37+
38+
weight = typeface.Weight;
39+
40+
stretch = typeface.Stretch;
41+
42+
var key = new FontCollectionKey(style, weight, stretch);
43+
44+
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
45+
{
46+
if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
47+
{
48+
return true;
49+
}
50+
51+
if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
52+
{
53+
var matchedKey = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
54+
55+
if (matchedKey != key)
56+
{
57+
if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface))
58+
{
59+
glyphTypeface = syntheticGlyphTypeface;
60+
}
61+
}
62+
63+
return true;
64+
}
65+
}
66+
67+
//Try to find a partially matching font
68+
for (var i = 0; i < Count; i++)
69+
{
70+
var fontFamily = _fontFamilies[i];
71+
72+
if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture)))
73+
{
74+
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
75+
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
76+
{
77+
return true;
78+
}
79+
}
80+
}
81+
82+
glyphTypeface = null;
83+
84+
return false;
85+
}
86+
87+
public bool TryAddGlyphTypeface(IGlyphTypeface glyphTypeface)
88+
{
89+
if (glyphTypeface == null || string.IsNullOrEmpty(glyphTypeface.FamilyName))
90+
{
91+
return false;
92+
}
93+
94+
var familyName = glyphTypeface.FamilyName;
95+
96+
if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
97+
{
98+
glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName,
99+
(_) => new System.Collections.Concurrent.ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>());
100+
101+
_fontFamilies.Add(new FontFamily(familyName));
102+
}
103+
var key = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
104+
105+
return glyphTypefaces.TryAdd(key, glyphTypeface);
106+
}
107+
108+
public bool TryAddGlyphTypeface(Stream stream)
109+
{
110+
var fontManager = FontManager.Current?.PlatformImpl;
111+
112+
if(fontManager == null)
113+
{
114+
return false;
115+
}
116+
117+
if(!fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
118+
{
119+
return false;
120+
}
121+
122+
return TryAddGlyphTypeface(glyphTypeface);
123+
}
124+
}
125+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public override void Initialize(IFontManagerImpl fontManager)
4848
public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
4949
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
5050
{
51-
var typeface = GetImplicitTypeface(new Typeface(familyName, style, weight, stretch), out familyName);
51+
var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
5252

5353
style = typeface.Style;
5454

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

Lines changed: 34 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
using System.Collections.Generic;
55
using System.Diagnostics.CodeAnalysis;
66
using System.Globalization;
7-
using System.Text;
87
using Avalonia.Platform;
9-
using Avalonia.Utilities;
108

119
namespace Avalonia.Media.Fonts
1210
{
@@ -170,12 +168,38 @@ IEnumerator IEnumerable.GetEnumerator()
170168
{
171169
return GetEnumerator();
172170
}
171+
172+
/// <summary>
173+
/// Attempts to retrieve the glyph typeface that most closely matches the specified font family name, style,
174+
/// weight, and stretch.
175+
/// </summary>
176+
/// <remarks>This method searches for a glyph typeface in the font collection cache that matches
177+
/// the specified parameters. If an exact match is not found, fallback mechanisms are applied to find the
178+
/// closest available match based on the specified style, weight, and stretch. If no suitable match is found,
179+
/// the method returns <see langword="false"/> and <paramref name="glyphTypeface"/> is set to <see
180+
/// langword="null"/>.</remarks>
181+
/// <param name="familyName">The name of the font family to search for. This parameter cannot be <see langword="null"/> or empty.</param>
182+
/// <param name="style">The desired font style.</param>
183+
/// <param name="weight">The desired font weight.</param>
184+
/// <param name="stretch">The desired font stretch.</param>
185+
/// <param name="glyphTypeface">When this method returns, contains the <see cref="IGlyphTypeface"/> that most closely matches the specified
186+
/// parameters, if a match is found; otherwise, <see langword="null"/>. This parameter is passed uninitialized.</param>
187+
/// <returns><see langword="true"/> if a matching glyph typeface is found; otherwise, <see langword="false"/>.</returns>
188+
public bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
189+
{
190+
if(!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
191+
{
192+
glyphTypeface = null;
173193

174-
internal static bool TryGetNearestMatch(
175-
ConcurrentDictionary<FontCollectionKey,
176-
IGlyphTypeface?> glyphTypefaces,
177-
FontCollectionKey key,
178-
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
194+
return false;
195+
}
196+
197+
var key = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch };
198+
199+
return TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface);
200+
}
201+
202+
protected bool TryGetNearestMatch(IDictionary<FontCollectionKey, IGlyphTypeface?> glyphTypefaces, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
179203
{
180204
if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
181205
{
@@ -218,7 +242,7 @@ internal static bool TryGetNearestMatch(
218242
//Take the first glyph typeface we can find.
219243
foreach (var typeface in glyphTypefaces.Values)
220244
{
221-
if(typeface != null)
245+
if (typeface != null)
222246
{
223247
glyphTypeface = typeface;
224248

@@ -230,8 +254,7 @@ internal static bool TryGetNearestMatch(
230254
}
231255

232256
internal static bool TryFindStretchFallback(
233-
ConcurrentDictionary<FontCollectionKey,
234-
IGlyphTypeface?> glyphTypefaces,
257+
IDictionary<FontCollectionKey, IGlyphTypeface?> glyphTypefaces,
235258
FontCollectionKey key,
236259
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
237260
{
@@ -264,8 +287,7 @@ internal static bool TryFindStretchFallback(
264287
}
265288

266289
internal static bool TryFindWeightFallback(
267-
ConcurrentDictionary<FontCollectionKey,
268-
IGlyphTypeface?> glyphTypefaces,
290+
IDictionary<FontCollectionKey, IGlyphTypeface?> glyphTypefaces,
269291
FontCollectionKey key,
270292
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
271293
{
@@ -347,69 +369,5 @@ internal static bool TryFindWeightFallback(
347369

348370
return false;
349371
}
350-
351-
internal static Typeface GetImplicitTypeface(Typeface typeface, out string normalizedFamilyName)
352-
{
353-
normalizedFamilyName = typeface.FontFamily.FamilyNames.PrimaryFamilyName;
354-
355-
//Return early if no separator is present.
356-
if (!normalizedFamilyName.Contains(' '))
357-
{
358-
return typeface;
359-
}
360-
361-
var style = typeface.Style;
362-
var weight = typeface.Weight;
363-
var stretch = typeface.Stretch;
364-
365-
StringBuilder? normalizedFamilyNameBuilder = null;
366-
var totalCharsRemoved = 0;
367-
368-
var tokenizer = new SpanStringTokenizer(normalizedFamilyName, ' ');
369-
370-
// Skip initial family name.
371-
tokenizer.ReadSpan();
372-
373-
while (tokenizer.TryReadSpan(out var token))
374-
{
375-
// Don't try to match numbers.
376-
if (new SpanStringTokenizer(token).TryReadInt32(out _))
377-
{
378-
continue;
379-
}
380-
381-
// Try match with font style, weight or stretch and update accordingly.
382-
var match = false;
383-
if (EnumHelper.TryParse<FontStyle>(token, true, out var newStyle))
384-
{
385-
style = newStyle;
386-
match = true;
387-
}
388-
else if (EnumHelper.TryParse<FontWeight>(token, true, out var newWeight))
389-
{
390-
weight = newWeight;
391-
match = true;
392-
}
393-
else if (EnumHelper.TryParse<FontStretch>(token, true, out var newStretch))
394-
{
395-
stretch = newStretch;
396-
match = true;
397-
}
398-
399-
if (match)
400-
{
401-
// Carve out matched word from the normalized name.
402-
normalizedFamilyNameBuilder ??= new StringBuilder(normalizedFamilyName);
403-
normalizedFamilyNameBuilder.Remove(tokenizer.CurrentTokenIndex - totalCharsRemoved, token.Length);
404-
totalCharsRemoved += token.Length;
405-
}
406-
}
407-
408-
// Get rid of any trailing spaces.
409-
normalizedFamilyName = (normalizedFamilyNameBuilder?.ToString() ?? normalizedFamilyName).TrimEnd();
410-
411-
//Preserve old font source
412-
return new Typeface(typeface.FontFamily, style, weight, stretch);
413-
}
414372
}
415373
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,23 @@ internal interface IFontCollection2 : IFontCollection
7070
/// <param name="syntheticGlyphTypeface"></param>
7171
/// <returns>Returns <c>true</c> if a synthetic glyph typface can be created; otherwise, <c>false</c></returns>
7272
bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface);
73+
74+
/// <summary>
75+
/// Attempts to retrieve the glyph typeface that most closely matches the specified font family name, style,
76+
/// weight, and stretch.
77+
/// </summary>
78+
/// <remarks>This method searches for a glyph typeface in the font collection cache that matches
79+
/// the specified parameters. If an exact match is not found, fallback mechanisms are applied to find the
80+
/// closest available match based on the specified style, weight, and stretch. If no suitable match is found,
81+
/// the method returns <see langword="false"/> and <paramref name="glyphTypeface"/> is set to <see
82+
/// langword="null"/>.</remarks>
83+
/// <param name="familyName">The name of the font family to search for. This parameter cannot be <see langword="null"/> or empty.</param>
84+
/// <param name="style">The desired font style.</param>
85+
/// <param name="weight">The desired font weight.</param>
86+
/// <param name="stretch">The desired font stretch.</param>
87+
/// <param name="glyphTypeface">When this method returns, contains the <see cref="IGlyphTypeface"/> that most closely matches the specified
88+
/// parameters, if a match is found; otherwise, <see langword="null"/>. This parameter is passed uninitialized.</param>
89+
/// <returns><see langword="true"/> if a matching glyph typeface is found; otherwise, <see langword="false"/>.</returns>
90+
bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
7391
}
7492
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon
4545
{
4646
glyphTypeface = null;
4747

48-
var typeface = GetImplicitTypeface(new Typeface(familyName, style, weight, stretch), out familyName);
48+
var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
4949

5050
style = typeface.Style;
5151

0 commit comments

Comments
 (0)