Browse Source

Fix SystemFontCollection nearest match lookup (#13365)

* Try to find nearest matching glyphTypeface when no exact match could by found by SystemFontCollection

* Fix glyphTypeface caching

* Rework SystemFontCollection TryCreateGlyphTypeface

* Make sure a failed glyphTypeface lookup is cached

* Make sure to only try to get nearest match if we failed to load the font via font magager impl

* Apply font simulations if possible

* Enable font simulation for embedded fonts

* Adjust simulated angle
pull/13394/head
Benedikt Stebner 2 years ago
committed by GitHub
parent
commit
8da8100288
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 40
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  2. 46
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  3. 3
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  4. 6
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  5. 4
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  6. 2
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  7. 2
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  8. 4
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  9. 21
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
  10. 2
      tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

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

@ -16,6 +16,8 @@ namespace Avalonia.Media.Fonts
private readonly Uri _source; private readonly Uri _source;
private IFontManagerImpl? _fontManager;
public EmbeddedFontCollection(Uri key, Uri source) public EmbeddedFontCollection(Uri key, Uri source)
{ {
_key = key; _key = key;
@ -31,6 +33,8 @@ namespace Avalonia.Media.Fonts
public override void Initialize(IFontManagerImpl fontManager) public override void Initialize(IFontManagerImpl fontManager)
{ {
_fontManager = fontManager;
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontAssets = FontFamilyLoader.LoadFontAssets(_source); var fontAssets = FontFamilyLoader.LoadFontAssets(_source);
@ -39,7 +43,7 @@ namespace Avalonia.Media.Fonts
{ {
var stream = assetLoader.Open(fontAsset); var stream = assetLoader.Open(fontAsset);
if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface)) if (fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
{ {
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
{ {
@ -69,8 +73,42 @@ namespace Avalonia.Media.Fonts
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{ {
if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{ {
if (glyphTypeface is IGlyphTypeface2 glyphTypeface2)
{
var fontSimulations = FontSimulations.None;
if (style != FontStyle.Normal && glyphTypeface2.Style != style)
{
fontSimulations |= FontSimulations.Oblique;
}
if ((int)weight >= 600 && glyphTypeface2.Weight != weight)
{
fontSimulations |= FontSimulations.Bold;
}
if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream))
{
using (stream)
{
if(_fontManager is not null && _fontManager.TryCreateGlyphTypeface(stream, fontSimulations, out glyphTypeface) &&
glyphTypefaces.TryAdd(key, glyphTypeface))
{
return true;
}
return false;
}
}
}
return true; return true;
} }
} }

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

@ -47,18 +47,46 @@ namespace Avalonia.Media.Fonts
var key = new FontCollectionKey(style, weight, stretch); var key = new FontCollectionKey(style, weight, stretch);
var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName, (key) => new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>()); var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName,
(_) => new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>());
if (!glyphTypefaces.TryGetValue(key, out glyphTypeface)) if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
{ {
_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface); return glyphTypeface != null;
}
if(!_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) ||
!glyphTypeface.FamilyName.Contains(familyName))
{
//Try to find nearest match if possible
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface);
}
if (!glyphTypefaces.TryAdd(key, glyphTypeface)) if(glyphTypeface is IGlyphTypeface2 glyphTypeface2)
{
var fontSimulations = FontSimulations.None;
if(style != FontStyle.Normal && glyphTypeface2.Style != style)
{ {
return false; fontSimulations |= FontSimulations.Oblique;
}
if((int)weight >= 600 && glyphTypeface2.Weight != weight)
{
fontSimulations |= FontSimulations.Bold;
}
if(fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream))
{
using (stream)
{
_fontManager.PlatformImpl.TryCreateGlyphTypeface(stream, fontSimulations, out glyphTypeface);
}
} }
} }
glyphTypefaces.TryAdd(key, glyphTypeface);
return glyphTypeface != null; return glyphTypeface != null;
} }
@ -87,7 +115,7 @@ namespace Avalonia.Media.Fonts
{ {
var stream = assetLoader.Open(fontAsset); var stream = assetLoader.Open(fontAsset);
if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface)) if (fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
{ {
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
{ {
@ -101,9 +129,9 @@ namespace Avalonia.Media.Fonts
} }
var key = new FontCollectionKey( var key = new FontCollectionKey(
glyphTypeface.Style, glyphTypeface.Style,
glyphTypeface.Weight, glyphTypeface.Weight,
glyphTypeface.Stretch); glyphTypeface.Stretch);
glyphTypefaces.TryAdd(key, glyphTypeface); glyphTypefaces.TryAdd(key, glyphTypeface);
} }

3
src/Avalonia.Base/Platform/IFontManagerImpl.cs

@ -53,10 +53,11 @@ namespace Avalonia.Platform
/// Tries to create a glyph typeface from specified stream. /// Tries to create a glyph typeface from specified stream.
/// </summary> /// </summary>
/// <param name="stream">A stream that holds the font's data.</param> /// <param name="stream">A stream that holds the font's data.</param>
/// <param name="fontSimulations">Specifies algorithmic style simulations.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param> /// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns> /// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise. /// <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns> /// </returns>
bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
} }
} }

6
src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -242,10 +242,12 @@ namespace Avalonia.Headless
return true; return true;
} }
public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface)
{ {
glyphTypeface = new HeadlessGlyphTypefaceImpl(); glyphTypeface = new HeadlessGlyphTypefaceImpl();
TryCreateGlyphTypefaceCount++;
return true; return true;
} }
} }
@ -301,7 +303,7 @@ namespace Avalonia.Headless
return true; return true;
} }
public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface)
{ {
glyphTypeface = new HeadlessGlyphTypefaceImpl(); glyphTypeface = new HeadlessGlyphTypefaceImpl();

4
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -105,13 +105,13 @@ namespace Avalonia.Skia
return true; return true;
} }
public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{ {
var skTypeface = SKTypeface.FromStream(stream); var skTypeface = SKTypeface.FromStream(stream);
if (skTypeface != null) if (skTypeface != null)
{ {
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);
return true; return true;
} }

2
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -164,7 +164,7 @@ namespace Avalonia.Skia
} }
public SKFont CreateSKFont(float size) public SKFont CreateSKFont(float size)
=> new(_typeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0.0f) => new(_typeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f)
{ {
LinearMetrics = true, LinearMetrics = true,
Embolden = (FontSimulations & FontSimulations.Bold) != 0 Embolden = (FontSimulations & FontSimulations.Bold) != 0

2
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@ -88,7 +88,7 @@ namespace Avalonia.Direct2D1.Media
return false; return false;
} }
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface)
{ {
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, new[] { stream }); var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, new[] { stream });

4
tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs

@ -86,11 +86,11 @@ namespace Avalonia.Skia.UnitTests.Media
return true; return true;
} }
public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{ {
var skTypeface = SKTypeface.FromStream(stream); var skTypeface = SKTypeface.FromStream(stream);
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);
return true; return true;
} }

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

@ -239,5 +239,26 @@ namespace Avalonia.Skia.UnitTests.Media
} }
} }
} }
[Fact]
public void Should_Get_Nearest_Match_For_Custom_SystemFont()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
using (AvaloniaLocator.EnterScope())
{
var systemFontCollection = FontManager.Current.SystemFonts as SystemFontCollection;
Assert.NotNull(systemFontCollection);
systemFontCollection.AddCustomFontSource(new Uri(s_fontUri, UriKind.Absolute));
Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Noto Mono", FontStyle.Italic), out var glyphTypeface));
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
}
} }
} }

2
tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

@ -57,7 +57,7 @@ namespace Avalonia.UnitTests
return false; return false;
} }
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface)
{ {
glyphTypeface = new HarfBuzzGlyphTypefaceImpl(stream); glyphTypeface = new HarfBuzzGlyphTypefaceImpl(stream);

Loading…
Cancel
Save