Browse Source

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
pull/15810/head
Benedikt Stebner 2 years ago
committed by GitHub
parent
commit
112187984b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 27
      src/Avalonia.Base/Media/FontManager.cs
  2. 16
      src/Avalonia.Base/Media/FontWeight.cs
  3. 3
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  4. 110
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  5. 64
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  6. 6
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  7. 17
      tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs
  8. 32
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

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

@ -4,6 +4,8 @@ using System.Collections.Generic;
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 @@ 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);
@ -115,7 +119,10 @@ namespace Avalonia.Media
}
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 @@ namespace Avalonia.Media
}
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 @@ namespace Avalonia.Media
{
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;

16
src/Avalonia.Base/Media/FontWeight.cs

@ -1,3 +1,4 @@
#pragma warning disable CA1069
namespace Avalonia.Media
{
/// <summary>
@ -22,7 +23,7 @@ namespace Avalonia.Media
/// <summary>
/// Specifies an "ultra light" font weight.
/// </summary>
UltraLight = 200,
UltraLight = ExtraLight,
/// <summary>
/// Specifies a "light" font weight.
@ -42,7 +43,7 @@ namespace Avalonia.Media
/// <summary>
/// Specifies a "regular" font weight.
/// </summary>
Regular = 400,
Regular = Normal,
/// <summary>
/// Specifies a "medium" font weight.
@ -52,7 +53,7 @@ namespace Avalonia.Media
/// <summary>
/// Specifies a "demi-bold" font weight.
/// </summary>
DemiBold = 600,
DemiBold = SemiBold,
/// <summary>
/// Specifies a "semi-bold" font weight.
@ -72,7 +73,7 @@ namespace Avalonia.Media
/// <summary>
/// Specifies an "ultra bold" font weight.
/// </summary>
UltraBold = 800,
UltraBold = ExtraBold,
/// <summary>
/// Specifies a "black" font weight.
@ -82,7 +83,12 @@ namespace Avalonia.Media
/// <summary>
/// Specifies a "heavy" font weight.
/// </summary>
Heavy = 900,
Heavy = Black,
/// <summary>
/// Specifies a "solid" font weight.
/// </summary>
Solid = Black,
/// <summary>
/// Specifies an "extra black" font weight.

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

@ -113,6 +113,9 @@ 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++)
{

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

@ -4,7 +4,9 @@ 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;
namespace Avalonia.Media.Fonts
{
@ -255,5 +257,113 @@ 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)
{
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;
}
}
}

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

@ -15,7 +15,7 @@ namespace Avalonia.Media.Fonts
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 @@ namespace Avalonia.Media.Fonts
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<FontCollectionKey, IGlyphTypeface?>());
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)

6
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -51,9 +51,11 @@ namespace Avalonia.Skia
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;
}

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

@ -64,5 +64,22 @@ 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);
}
}
}
}

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

@ -229,11 +229,8 @@ namespace Avalonia.Skia.UnitTests.Media
{
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 @@ namespace Avalonia.Skia.UnitTests.Media
{
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);
}
}
}

Loading…
Cancel
Save