Browse Source

Fix FontCollection MatchCharacter (#19494)

* Fix FontCollection.MatchCharacter usage inside the FontManager implementation
Fix default MatchCharacter implementation
Add a unit test

* Reuse existing font collection key

* Fix resm FontFamilyIdentifier

* Fix FontFamily definition

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/19532/head
Benedikt Stebner 6 months ago
committed by GitHub
parent
commit
51443cb089
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 31
      src/Avalonia.Base/Media/FontFamily.cs
  2. 34
      src/Avalonia.Base/Media/FontManager.cs
  3. 4
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  4. 34
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  5. 3
      src/Avalonia.Base/Utilities/UriExtensions.cs
  6. 4
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  7. 25
      tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs
  8. 29
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
  9. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  10. 11
      tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

31
src/Avalonia.Base/Media/FontFamily.cs

@ -42,7 +42,9 @@ namespace Avalonia.Media
if (fontSources.Count == 1)
{
if(fontSources[0].Source is Uri source)
var singleSource = fontSources[0];
if (singleSource.Source is Uri source)
{
if (baseUri != null && !baseUri.IsAbsoluteUri)
{
@ -51,6 +53,13 @@ namespace Avalonia.Media
Key = new FontFamilyKey(source, baseUri);
}
else
{
if(baseUri != null && baseUri.IsAbsoluteUri)
{
Key = new FontFamilyKey(baseUri);
}
}
}
else
{
@ -141,11 +150,21 @@ namespace Avalonia.Media
case 2:
{
var source = innerSegments[0].StartsWith("/", StringComparison.Ordinal)
? new Uri(innerSegments[0], UriKind.Relative)
: new Uri(innerSegments[0], UriKind.RelativeOrAbsolute);
identifier = new FontSourceIdentifier(innerSegments[1].Trim(), source);
var path = innerSegments[0].Trim();
var innerName = innerSegments[1].Trim();
if (string.IsNullOrEmpty(path))
{
identifier = new FontSourceIdentifier(innerName, null);
}
else
{
var source = path.StartsWith("/", StringComparison.Ordinal)
? new Uri(path, UriKind.Relative)
: new Uri(path, UriKind.RelativeOrAbsolute);
identifier = new FontSourceIdentifier(innerName, source);
}
break;
}

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

@ -271,21 +271,35 @@ namespace Avalonia.Media
}
//Try to match against fallbacks first
if (fontFamily != null && fontFamily.Key is CompositeFontFamilyKey compositeKey)
if (fontFamily?.Key != null)
{
for (int i = 0; i < compositeKey.Keys.Count; i++)
{
var key = compositeKey.Keys[i];
var familyName = fontFamily.FamilyNames[i];
var source = key.Source.EnsureAbsolute(key.BaseUri);
var fontUri = fontFamily.Key.Source.EnsureAbsolute(fontFamily.Key.BaseUri);
if(familyName == FontFamily.DefaultFontFamilyName)
if (fontFamily.Key is CompositeFontFamilyKey compositeKey)
{
for (int i = 0; i < compositeKey.Keys.Count; i++)
{
familyName = DefaultFontFamily.Name;
var key = compositeKey.Keys[i];
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))
{
return true;
}
}
}
if (TryGetFontCollection(source, out var fontCollection) &&
fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
if (fontUri.IsFontCollection())
{
if (TryGetFontCollection(fontUri, out var fontCollection) &&
fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily.Name, culture, out typeface))
{
return true;
}

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

@ -15,8 +15,6 @@ namespace Avalonia.Media.Fonts
private readonly Uri _source;
private IFontManagerImpl? _fontManager;
public EmbeddedFontCollection(Uri key, Uri source)
{
_key = key;
@ -32,8 +30,6 @@ namespace Avalonia.Media.Fonts
public override void Initialize(IFontManagerImpl fontManager)
{
_fontManager = fontManager;
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontAssets = FontFamilyLoader.LoadFontAssets(_source);

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

@ -27,29 +27,37 @@ namespace Avalonia.Media.Fonts
string? familyName, CultureInfo? culture, out Typeface match)
{
match = default;
if (string.IsNullOrEmpty(familyName))
//If a font family is defined we try to find a match inside that family first
if (familyName != null && _glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
foreach (var typefaces in _glyphTypefaceCache.Values)
if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
{
if (TryGetNearestMatch(typefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
match = new Typeface(Key.AbsoluteUri + "#" + glyphTypeface.FamilyName, style, weight, stretch);
match = new Typeface(new FontFamily(Key, "#" + glyphTypeface.FamilyName), style, weight, stretch);
return true;
}
return true;
}
}
}
else
//Try to find a match in any font family
foreach (var pair in _glyphTypefaceCache)
{
if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface))
if(pair.Key == familyName)
{
//We already tried this before
continue;
}
glyphTypefaces = pair.Value;
if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
{
if (glyphTypeface.FamilyName.Contains(familyName) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
match = new Typeface(Key.AbsoluteUri + "#" + familyName, style, weight, stretch);
match = new Typeface(new FontFamily(Key, "#" + glyphTypeface.FamilyName) , style, weight, stretch);
return true;
}

3
src/Avalonia.Base/Utilities/UriExtensions.cs

@ -22,9 +22,6 @@ internal static class UriExtensions
throw new ArgumentException($"Relative uri {uri} without base url");
if (!baseUri.IsAbsoluteUri)
throw new ArgumentException($"Base uri {baseUri} is relative");
if (baseUri.IsResm())
throw new ArgumentException(
$"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm");
return new Uri(baseUri, uri);
}

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

@ -18,10 +18,10 @@ namespace Avalonia.Skia.UnitTests.Media
public CustomFontManagerImpl()
{
_defaultFamilyName = "Noto Mono";
var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests");
_defaultFamilyName = source.AbsoluteUri + "#Noto Mono";
_customFonts = new EmbeddedFontCollection(source, source);
}

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

@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Xunit;
@ -72,7 +73,7 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact]
public void Should_Use_Fallback()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
{
var source = new Uri(NotoMono, UriKind.Absolute);
@ -80,7 +81,7 @@ namespace Avalonia.Skia.UnitTests.Media
var fontCollection = new CustomizableFontCollection(source, source, new[] { fallback });
fontCollection.Initialize(new CustomFontManagerImpl());
fontCollection.Initialize(FontManager.Current.PlatformImpl);
Assert.True(fontCollection.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match));
@ -91,23 +92,25 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact]
public void Should_Ignore_FontFamily()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
{
var source = new Uri(NotoMono + "#Noto Mono", UriKind.Absolute);
var key = new Uri(NotoMono, UriKind.Absolute);
var ignorable = new FontFamily(new Uri(NotoMono, UriKind.Absolute), "Noto Mono");
var typeface = new Typeface(ignorable);
var fontCollection = new CustomizableFontCollection(key, key, null, new[] { ignorable });
fontCollection.Initialize(FontManager.Current.PlatformImpl);
var fontCollection = new CustomizableFontCollection(source, source, null, new[] { ignorable });
var typeface = new Typeface(ignorable);
fontCollection.Initialize(new CustomFontManagerImpl());
var glyphTypeface = typeface.GlyphTypeface;
Assert.False(fontCollection.TryCreateSyntheticGlyphTypeface(
typeface.GlyphTypeface,
FontStyle.Italic,
FontWeight.DemiBold,
FontStretch.Normal,
typeface.GlyphTypeface,
FontStyle.Italic,
FontWeight.DemiBold,
FontStretch.Normal,
out var syntheticGlyphTypeface));
}
}

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

@ -6,6 +6,7 @@ using Avalonia.Fonts.Inter;
using Avalonia.Headless;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.UnitTests;
using SkiaSharp;
using Xunit;
@ -380,5 +381,33 @@ namespace Avalonia.Skia.UnitTests.Media
}
}
}
[Fact]
public void Should_Use_FontCollection_MatchCharacter()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
using (AvaloniaLocator.EnterScope())
{
FontManager.Current.AddFontCollection(
new EmbeddedFontCollection(
new Uri("fonts:MyCollection"), //key
new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"))); //source
var fontFamily = new FontFamily("fonts:MyCollection#Noto Mono");
var character = "א";
var codepoint = Codepoint.ReadAt(character, 0, out _);
Assert.True(FontManager.Current.TryMatchCharacter(codepoint, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface));
//Typeface should come from the font collection
Assert.NotNull(typeface.FontFamily.Key);
Assert.Equal("Noto Sans Hebrew", typeface.GlyphTypeface.FamilyName);
}
}
}
}
}

2
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -1095,7 +1095,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var text = "𖾇";
var typeface = new Typeface(new FontFamily(new Uri("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests"), "Noto Mono"));
var typeface = new Typeface(new FontFamily(new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"), "Noto Mono"));
var defaultRunProperties = new GenericTextRunProperties(typeface);
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrapping: TextWrapping.Wrap);
var textLine = TextFormatter.Current.FormatLine(new SimpleTextSource(text, defaultRunProperties), 0, 120, paragraphProperties);

11
tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@ -13,13 +14,13 @@ namespace Avalonia.UnitTests
private readonly string _defaultFamilyName;
private static readonly Typeface _defaultTypeface =
new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono");
new Typeface(new FontFamily(new Uri("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests", UriKind.Absolute), "Noto Mono"));
private static readonly Typeface _italicTypeface =
new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Sans");
new Typeface(new FontFamily(new Uri("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests", UriKind.Absolute), "Noto Sans"));
private static readonly Typeface _emojiTypeface =
new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Twitter Color Emoji");
new Typeface(new FontFamily(new Uri("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests"), "Twitter Color Emoji"));
public HarfBuzzFontManagerImpl(string defaultFamilyName = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono")
public HarfBuzzFontManagerImpl(string defaultFamilyName = "Noto Mono")
{
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
_defaultFamilyName = defaultFamilyName;

Loading…
Cancel
Save