Browse Source

Make typeface matching and synthetic typeface creation customizable (#18890)

* Make typeface matching and synthetic typeface creation customizable

* Rename test collection

* Revert breaking change

* Directly use the DefaultFontFamily name when the alias is being used
release/11.3.5
Benedikt Stebner 9 months ago
parent
commit
b7c2881655
  1. 66
      src/Avalonia.Base/Media/FontManager.cs
  2. 14
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  3. 84
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  4. 11
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  5. 6
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  6. 6
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  7. 34
      tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs
  8. 111
      tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs

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

@ -109,9 +109,9 @@ namespace Avalonia.Media
var familyName = fontFamily.FamilyNames[i];
if(_fontFamilyMappings != null && _fontFamilyMappings.TryGetValue(familyName, out var mappedFontFamily))
if (_fontFamilyMappings != null && _fontFamilyMappings.TryGetValue(familyName, out var mappedFontFamily))
{
if(mappedFontFamily.Key != null)
if (mappedFontFamily.Key != null)
{
key = mappedFontFamily.Key;
}
@ -123,6 +123,11 @@ namespace Avalonia.Media
familyName = mappedFontFamily.FamilyNames.PrimaryFamilyName;
}
if (familyName == FontFamily.DefaultFontFamilyName)
{
return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
}
if (TryGetGlyphTypefaceByKeyAndName(typeface, key, familyName, out glyphTypeface) &&
glyphTypeface.FamilyName.Contains(familyName))
{
@ -274,6 +279,11 @@ namespace Avalonia.Media
var familyName = fontFamily.FamilyNames[i];
var source = key.Source.EnsureAbsolute(key.BaseUri);
if(familyName == FontFamily.DefaultFontFamilyName)
{
familyName = DefaultFontFamily.Name;
}
if (TryGetFontCollection(source, out var fontCollection) &&
fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{
@ -286,58 +296,6 @@ namespace Avalonia.Media
return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface);
}
/// <summary>
/// Tries to create a synthetic glyph typefacefor specified source glyph typeface and font properties.
/// </summary>
/// <param name="fontManager">The font manager implementation.</param>
/// <param name="glyphTypeface">The source glyph typeface.</param>
/// <param name="style">The requested font style.</param>
/// <param name="weight">The requested font weight.</param>
/// <param name="syntheticGlyphTypeface">The created synthetic glyph typeface.</param>
/// <returns>
/// <c>True</c>, if the <see cref="FontManager"/> could create a synthetic glyph typeface, <c>False</c> otherwise.
/// </returns>
internal static bool TryCreateSyntheticGlyphTypeface(IFontManagerImpl fontManager, IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight,
[NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
{
if (fontManager == null)
{
syntheticGlyphTypeface = null;
return false;
}
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)
{
fontManager.TryCreateGlyphTypeface(stream, fontSimulations,
out syntheticGlyphTypeface);
return syntheticGlyphTypeface != null;
}
}
}
syntheticGlyphTypeface = null;
return false;
}
internal IReadOnlyList<Typeface> GetFamilyTypefaces(FontFamily fontFamily)
{
var key = fontFamily.Key;

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

@ -71,14 +71,16 @@ namespace Avalonia.Media.Fonts
if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
if(_fontManager != null && FontManager.TryCreateSyntheticGlyphTypeface(_fontManager, glyphTypeface, style, weight, out var syntheticGlyphTypeface))
var matchedKey = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
if(matchedKey != key)
{
glyphTypeface = syntheticGlyphTypeface;
if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface))
{
glyphTypeface = syntheticGlyphTypeface;
}
}
//Make sure we cache the found match
glyphTypefaces.TryAdd(key, glyphTypeface);
return true;
}
}
@ -143,7 +145,7 @@ namespace Avalonia.Media.Fonts
}
}
bool IFontCollection2.TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
familyTypefaces = null;

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

@ -23,7 +23,7 @@ namespace Avalonia.Media.Fonts
public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch,
public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch,
string? familyName, CultureInfo? culture, out Typeface match)
{
match = default;
@ -59,6 +59,88 @@ namespace Avalonia.Media.Fonts
return false;
}
public virtual bool TryCreateSyntheticGlyphTypeface(
IGlyphTypeface glyphTypeface,
FontStyle style,
FontWeight weight,
FontStretch stretch,
[NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
{
syntheticGlyphTypeface = null;
//Source family should be present in the cache.
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
{
return false;
}
var fontManager = FontManager.Current.PlatformImpl;
var key = new FontCollectionKey(style, weight, stretch);
var currentKey =
new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
if (currentKey == key)
{
return false;
}
if (glyphTypeface is not IGlyphTypeface2 glyphTypeface2)
{
return false;
}
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.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface))
{
//Add the TypographicFamilyName to the cache
if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
{
AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, syntheticGlyphTypeface);
}
foreach (var kvp in glyphTypeface2.FamilyNames)
{
AddGlyphTypefaceByFamilyName(kvp.Value, syntheticGlyphTypeface);
}
return true;
}
return false;
}
}
return false;
void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface)
{
var typefaces = _glyphTypefaceCache.GetOrAdd(familyName,
x =>
{
return new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
});
typefaces.TryAdd(key, glyphTypeface);
}
}
public abstract void Initialize(IFontManagerImpl fontManager);
public abstract IEnumerator<FontFamily> GetEnumerator();

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

@ -59,5 +59,16 @@ namespace Avalonia.Media.Fonts
/// <c>True</c>, if the <see cref="IFontCollection2"/> could get the list of typefaces, <c>False</c> otherwise.
/// </returns>
bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces);
/// <summary>
/// Try to get a synthetic glyph typeface for given parameters.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface we try to synthesize.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param>
/// <param name="stretch">The font stretch.</param>
/// <param name="syntheticGlyphTypeface"></param>
/// <returns>Returns <c>true</c> if a synthetic glyph typface can be created; otherwise, <c>false</c></returns>
bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface);
}
}

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

@ -91,9 +91,11 @@ namespace Avalonia.Media.Fonts
}
//Try to create a synthetic glyph typeface
if (FontManager.TryCreateSyntheticGlyphTypeface(_fontManager.PlatformImpl, glyphTypeface, style, weight, out var syntheticGlyphTypeface))
if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface))
{
glyphTypeface = syntheticGlyphTypeface;
return true;
}
}
@ -159,7 +161,7 @@ namespace Avalonia.Media.Fonts
}
}
bool IFontCollection2.TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
familyTypefaces = null;

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

@ -245,7 +245,11 @@ namespace Avalonia.Headless
public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface)
{
glyphTypeface = new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal);
glyphTypeface = new HeadlessGlyphTypefaceImpl(
FontFamily.DefaultFontFamilyName,
fontSimulations.HasFlag(FontSimulations.Oblique) ? FontStyle.Italic : FontStyle.Normal,
fontSimulations.HasFlag(FontSimulations.Bold) ? FontWeight.Bold : FontWeight.Normal,
FontStretch.Normal);
TryCreateGlyphTypefaceCount++;

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

@ -4,6 +4,7 @@ using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.UnitTests;
@ -18,6 +19,7 @@ namespace Avalonia.Skia.UnitTests.Media
private const string s_manrope = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope";
[InlineData(FontWeight.SemiLight, FontStyle.Normal)]
[InlineData(FontWeight.Bold, FontStyle.Italic)]
[InlineData(FontWeight.Heavy, FontStyle.Oblique)]
@ -28,7 +30,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var source = new Uri(s_notoMono, UriKind.Absolute);
var fontCollection = new EmbeddedFontCollection(source, source);
var fontCollection = new TestEmbeddedFontCollection(source, source);
fontCollection.Initialize(new CustomFontManagerImpl());
@ -47,7 +49,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var source = new Uri(s_notoMono, UriKind.Absolute);
var fontCollection = new EmbeddedFontCollection(source, source);
var fontCollection = new TestEmbeddedFontCollection(source, source);
fontCollection.Initialize(new CustomFontManagerImpl());
@ -62,7 +64,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T", UriKind.Absolute);
var fontCollection = new EmbeddedFontCollection(source, source);
var fontCollection = new TestEmbeddedFontCollection(source, source);
fontCollection.Initialize(new CustomFontManagerImpl());
@ -79,7 +81,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var source = new Uri(s_manrope, UriKind.Absolute);
var fontCollection = new EmbeddedFontCollection(source, source);
var fontCollection = new TestEmbeddedFontCollection(source, source);
fontCollection.Initialize(new CustomFontManagerImpl());
@ -102,7 +104,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var source = new Uri(s_manrope, UriKind.Absolute);
var fontCollection = new TestEmbeddedFontCollection(source, source);
var fontCollection = new TestEmbeddedFontCollection(source, source, true);
fontCollection.Initialize(new CustomFontManagerImpl());
@ -120,11 +122,31 @@ namespace Avalonia.Skia.UnitTests.Media
private class TestEmbeddedFontCollection : EmbeddedFontCollection
{
public TestEmbeddedFontCollection(Uri key, Uri source) : base(key, source)
private bool _createSyntheticTypefaces;
public TestEmbeddedFontCollection(Uri key, Uri source, bool createSyntheticTypefaces = false) : base(key, source)
{
_createSyntheticTypefaces = createSyntheticTypefaces;
}
public IDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> GlyphTypefaceCache => _glyphTypefaceCache;
public override bool TryCreateSyntheticGlyphTypeface(
IGlyphTypeface glyphTypeface,
FontStyle style,
FontWeight weight,
FontStretch stretch,
[NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
{
if (!_createSyntheticTypefaces)
{
syntheticGlyphTypeface = null;
return false;
}
return base.TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out syntheticGlyphTypeface);
}
}
}
}

111
tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs

@ -1,7 +1,10 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.UnitTests;
@ -11,6 +14,9 @@ namespace Avalonia.Skia.UnitTests.Media
{
public class FontCollectionTests
{
private const string NotoMono =
"resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests";
[InlineData("Hello World 6", "Hello World 6", FontStyle.Normal, FontWeight.Normal)]
[InlineData("Hello World Italic", "Hello World", FontStyle.Italic, FontWeight.Normal)]
[InlineData("Hello World Italic Bold", "Hello World", FontStyle.Italic, FontWeight.Bold)]
@ -41,8 +47,6 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.True(fontCollection.TryGetGlyphTypeface("Arial", FontStyle.Normal, FontWeight.ExtraBlack, FontStretch.Normal, out var glyphTypeface));
Assert.True(glyphTypeface.FontSimulations == FontSimulations.Bold);
Assert.True(fontCollection.GlyphTypefaceCache.TryGetValue("Arial", out var glyphTypefaces));
Assert.Equal(2, glyphTypefaces.Count);
@ -64,5 +68,108 @@ namespace Avalonia.Skia.UnitTests.Media
public IDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> GlyphTypefaceCache => _glyphTypefaceCache;
}
[Fact]
public void Should_Use_Fallback()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var source = new Uri(NotoMono, UriKind.Absolute);
var fallback = new FontFallback { FontFamily = new FontFamily("Arial"), UnicodeRange = new UnicodeRange('A', 'A') };
var fontCollection = new CustomizableFontCollection(source, source, new[] { fallback });
fontCollection.Initialize(new CustomFontManagerImpl());
Assert.True(fontCollection.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match));
Assert.Equal("Arial", match.FontFamily.Name);
}
}
[Fact]
public void Should_Ignore_FontFamily()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var source = new Uri(NotoMono + "#Noto Mono", UriKind.Absolute);
var ignorable = new FontFamily(new Uri(NotoMono, UriKind.Absolute), "Noto Mono");
var typeface = new Typeface(ignorable);
var fontCollection = new CustomizableFontCollection(source, source, null, new[] { ignorable });
fontCollection.Initialize(new CustomFontManagerImpl());
Assert.False(fontCollection.TryCreateSyntheticGlyphTypeface(
typeface.GlyphTypeface,
FontStyle.Italic,
FontWeight.DemiBold,
FontStretch.Normal,
out var syntheticGlyphTypeface));
}
}
private class CustomizableFontCollection : EmbeddedFontCollection
{
private readonly IReadOnlyList<FontFallback>? _fallbacks;
private readonly IReadOnlyList<FontFamily>? _ignorables;
public CustomizableFontCollection(Uri key, Uri source, IReadOnlyList<FontFallback>? fallbacks = null, IReadOnlyList<FontFamily>? ignorables = null) : base(key, source)
{
_fallbacks = fallbacks;
_ignorables = ignorables;
}
public override bool TryMatchCharacter(
int codepoint,
FontStyle style,
FontWeight weight,
FontStretch stretch,
string? familyName,
CultureInfo? culture,
out Typeface match)
{
if(_fallbacks is not null)
{
foreach (var fallback in _fallbacks)
{
if (fallback.UnicodeRange.IsInRange(codepoint))
{
match = new Typeface(fallback.FontFamily, style, weight, stretch);
return true;
}
}
}
return base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match);
}
public override bool TryCreateSyntheticGlyphTypeface(
IGlyphTypeface glyphTypeface,
FontStyle style,
FontWeight weight,
FontStretch stretch,
[NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
{
syntheticGlyphTypeface = null;
if(_ignorables is not null)
{
foreach (var ignorable in _ignorables)
{
if (glyphTypeface.FamilyName == ignorable.Name || glyphTypeface is IGlyphTypeface2 glyphTypeface2 && glyphTypeface2.TypographicFamilyName == ignorable.Name)
{
return false;
}
}
}
return base.TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out syntheticGlyphTypeface);
}
}
}
}

Loading…
Cancel
Save