Skip to content

Commit

Permalink
Fix SystemFontCollection nearest match lookup (#13365)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Gillibald authored Oct 26, 2023
1 parent 2cfdc09 commit 8da8100
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 20 deletions.
40 changes: 39 additions & 1 deletion src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class EmbeddedFontCollection : FontCollectionBase

private readonly Uri _source;

private IFontManagerImpl? _fontManager;

public EmbeddedFontCollection(Uri key, Uri source)
{
_key = key;
Expand All @@ -31,6 +33,8 @@ public EmbeddedFontCollection(Uri key, Uri source)

public override void Initialize(IFontManagerImpl fontManager)
{
_fontManager = fontManager;

var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();

var fontAssets = FontFamilyLoader.LoadFontAssets(_source);
Expand All @@ -39,7 +43,7 @@ public override void Initialize(IFontManagerImpl fontManager)
{
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))
{
Expand Down Expand Up @@ -69,8 +73,42 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon

if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
{
return true;
}

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;
}
}
Expand Down
46 changes: 37 additions & 9 deletions src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,46 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon

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

Expand Down Expand Up @@ -87,7 +115,7 @@ private void LoadGlyphTypefaces(IFontManagerImpl fontManager, Uri source)
{
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))
{
Expand All @@ -101,9 +129,9 @@ private void LoadGlyphTypefaces(IFontManagerImpl fontManager, Uri source)
}

var key = new FontCollectionKey(
glyphTypeface.Style,
glyphTypeface.Weight,
glyphTypeface.Stretch);
glyphTypeface.Style,
glyphTypeface.Weight,
glyphTypeface.Stretch);

glyphTypefaces.TryAdd(key, glyphTypeface);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Avalonia.Base/Platform/IFontManagerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weigh
/// Tries to create a glyph typeface from specified stream.
/// </summary>
/// <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>
/// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
}
}
6 changes: 4 additions & 2 deletions src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,12 @@ public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, F
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();

TryCreateGlyphTypefaceCount++;

return true;
}
}
Expand Down Expand Up @@ -301,7 +303,7 @@ public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, F
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();

Expand Down
4 changes: 2 additions & 2 deletions src/Skia/Avalonia.Skia/FontManagerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@ public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeigh
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);

if (skTypeface != null)
{
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);

return true;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
}

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,
Embolden = (FontSimulations & FontSimulations.Bold) != 0
Expand Down
2 changes: 1 addition & 1 deletion src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeigh
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 });

Expand Down
4 changes: 2 additions & 2 deletions tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeigh
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);

glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);

return true;
}
Expand Down
21 changes: 21 additions & 0 deletions tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,5 +239,26 @@ public void Should_Use_Custom_SystemFont()
}
}
}


[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 changes: 1 addition & 1 deletion tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fon
return false;
}

public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface)
{
glyphTypeface = new HarfBuzzGlyphTypefaceImpl(stream);

Expand Down

0 comments on commit 8da8100

Please sign in to comment.