Browse Source

Allow multiple font sources per FontFamily and make sure combinations of system and embedded fonts can be used (#12871)

release/11.0.5-rc1
Benedikt Stebner 2 years ago
committed by Steven Kirk
parent
commit
49f30410f6
  1. 16
      src/Avalonia.Base/Media/CompositeFontFamilyKey.cs
  2. 104
      src/Avalonia.Base/Media/FontFamily.cs
  3. 126
      src/Avalonia.Base/Media/FontManager.cs
  4. 17
      src/Avalonia.Base/Media/FontSourceIdentifier.cs
  5. 14
      src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs
  6. 6
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  7. 2
      tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs
  8. 12
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  9. 78
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

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

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.Fonts;
namespace Avalonia.Media
{
internal class CompositeFontFamilyKey : FontFamilyKey
{
public CompositeFontFamilyKey(Uri source, FontFamilyKey[] keys) : base(source, null)
{
Keys = keys;
}
public IReadOnlyList<FontFamilyKey> Keys { get; }
}
}

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

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.Fonts;
using Avalonia.Utilities;
namespace Avalonia.Media
{
@ -34,19 +36,42 @@ namespace Avalonia.Media
throw new ArgumentNullException(nameof(name));
}
var fontFamilySegment = GetFontFamilyIdentifier(name);
var fontSources = GetFontSourceIdentifier(name);
if (fontFamilySegment.Source != null)
FamilyNames = new FamilyNameCollection(fontSources);
if (fontSources.Count == 1)
{
if (baseUri != null && !baseUri.IsAbsoluteUri)
if(fontSources[0].Source is Uri source)
{
throw new ArgumentException("Base uri must be an absolute uri.", nameof(baseUri));
}
if (baseUri != null && !baseUri.IsAbsoluteUri)
{
throw new ArgumentException("Base uri must be an absolute uri.", nameof(baseUri));
}
Key = new FontFamilyKey(fontFamilySegment.Source, baseUri);
Key = new FontFamilyKey(source, baseUri);
}
}
else
{
var keys = new FontFamilyKey[fontSources.Count];
for (int i = 0; i < fontSources.Count; i++)
{
var fontSource = fontSources[i];
FamilyNames = new FamilyNameCollection(fontFamilySegment.Name);
if(fontSource.Source is not null)
{
keys[i] = new FontFamilyKey(fontSource.Source, baseUri);
}
else
{
keys[i] = new FontFamilyKey(new Uri(FontManager.SystemFontScheme + ":" + fontSource.Name, UriKind.Absolute));
}
}
Key = new CompositeFontFamilyKey(new Uri(FontManager.CompositeFontScheme + ":" + name, UriKind.Absolute), keys);
}
}
/// <summary>
@ -88,44 +113,49 @@ namespace Avalonia.Media
return new FontFamily(s);
}
private struct FontFamilyIdentifier
private static FrugalStructList<FontSourceIdentifier> GetFontSourceIdentifier(string name)
{
public FontFamilyIdentifier(string name, Uri? source)
{
Name = name;
Source = source;
}
public string Name { get; }
public Uri? Source { get; }
}
var result = new FrugalStructList<FontSourceIdentifier>(1);
private static FontFamilyIdentifier GetFontFamilyIdentifier(string name)
{
var segments = name.Split('#');
var segments = name.Split(',');
switch (segments.Length)
for (int i = 0; i < segments.Length; i++)
{
case 1:
{
return new FontFamilyIdentifier(segments[0], null);
}
var segment = segments[i];
var innerSegments = segment.Split('#');
case 2:
{
var source = segments[0].StartsWith("/", StringComparison.Ordinal)
? new Uri(segments[0], UriKind.Relative)
: new Uri(segments[0], UriKind.RelativeOrAbsolute);
FontSourceIdentifier identifier;
return new FontFamilyIdentifier(segments[1], source);
}
switch (innerSegments.Length)
{
case 1:
{
identifier = new FontSourceIdentifier(innerSegments[0].Trim(), null);
break;
}
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);
break;
}
default:
{
identifier = new FontSourceIdentifier(name, null);
break;
}
}
default:
{
return new FontFamilyIdentifier(name, null);
}
result.Add(identifier);
}
return result;
}
/// <summary>

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

@ -15,9 +15,11 @@ namespace Avalonia.Media
/// </summary>
public sealed class FontManager
{
internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts");
internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts", UriKind.Absolute);
public const string FontCollectionScheme = "fonts";
public const string SystemFontScheme = "systemfont";
public const string CompositeFontScheme = "compositefont";
private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
@ -95,69 +97,86 @@ namespace Avalonia.Media
var fontFamily = typeface.FontFamily;
if(typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName)
if (typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName)
{
return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
}
if (fontFamily.Key is FontFamilyKey key)
if (fontFamily.Key is FontFamilyKey)
{
var source = key.Source;
if (!source.IsAbsoluteUri)
if (fontFamily.Key is CompositeFontFamilyKey compositeKey)
{
if (key.BaseUri == null)
for (int i = 0; i < compositeKey.Keys.Count; i++)
{
throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
}
var key = compositeKey.Keys[i];
source = new Uri(key.BaseUri, source);
var familyName = fontFamily.FamilyNames[i];
if (TryGetGlyphTypefaceByKeyAndName(typeface, key, familyName, out glyphTypeface) &&
glyphTypeface.FamilyName.Contains(familyName))
{
return true;
}
}
}
if (!_fontCollections.TryGetValue(source, out var fontCollection) && (source.IsAbsoluteResm() || source.IsAvares()))
else
{
var embeddedFonts = new EmbeddedFontCollection(source, source);
embeddedFonts.Initialize(PlatformImpl);
if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, fontFamily.FamilyNames.PrimaryFamilyName, out glyphTypeface))
{
fontCollection = embeddedFonts;
return true;
}
}
if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
return false;
}
}
else
{
if (SystemFonts.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return true;
}
}
if (!fontFamily.FamilyNames.HasFallbacks)
{
return false;
}
if (typeface.FontFamily == DefaultFontFamily)
{
return false;
}
for (var i = 0; i < fontFamily.FamilyNames.Count; i++)
//Nothing was found so use the default
return TryGetGlyphTypeface(new Typeface(FontFamily.DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
}
private bool TryGetGlyphTypefaceByKeyAndName(Typeface typeface, FontFamilyKey key, string familyName, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
var source = key.Source;
if (source.Scheme == SystemFontScheme)
{
var familyName = fontFamily.FamilyNames[i];
return SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface);
}
if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
if (!source.IsAbsoluteUri)
{
if (key.BaseUri == null)
{
if (!fontFamily.FamilyNames.HasFallbacks || glyphTypeface.FamilyName != DefaultFontFamily.Name)
{
return true;
}
throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
}
source = new Uri(key.BaseUri, source);
}
if(typeface.FontFamily == DefaultFontFamily)
if (TryGetFontCollection(source, out var fontCollection) &&
fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return false;
if (glyphTypeface.FamilyName.Contains(familyName))
{
return true;
}
}
//Nothing was found so use the default
return TryGetGlyphTypeface(new Typeface(FontFamily.DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
glyphTypeface = null;
return false;
}
/// <summary>
@ -230,18 +249,17 @@ namespace Avalonia.Media
}
//Try to match against fallbacks first
if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks)
if (fontFamily != null && fontFamily.Key is CompositeFontFamilyKey compositeKey)
{
for (int i = 1; i < fontFamily.FamilyNames.Count; i++)
for (int i = 0; i < compositeKey.Keys.Count; i++)
{
var key = compositeKey.Keys[i];
var familyName = fontFamily.FamilyNames[i];
foreach (var fontCollection in _fontCollections.Values)
if (TryGetFontCollection(key.Source, out var fontCollection) &&
fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{
if (fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{
return true;
};
return true;
}
}
}
@ -249,5 +267,27 @@ namespace Avalonia.Media
//Try to find a match with the system font manager
return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface);
}
private bool TryGetFontCollection(Uri source, [NotNullWhen(true)] out IFontCollection? fontCollection)
{
if(source.Scheme == SystemFontScheme)
{
source = SystemFontsKey;
}
if (!_fontCollections.TryGetValue(source, out fontCollection) && (source.IsAbsoluteResm() || source.IsAvares()))
{
var embeddedFonts = new EmbeddedFontCollection(source, source);
embeddedFonts.Initialize(PlatformImpl);
if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
{
fontCollection = embeddedFonts;
}
}
return fontCollection != null;
}
}
}

17
src/Avalonia.Base/Media/FontSourceIdentifier.cs

@ -0,0 +1,17 @@
using System;
namespace Avalonia.Media
{
internal readonly record struct FontSourceIdentifier
{
public FontSourceIdentifier(string name, Uri? source)
{
Name = name;
Source = source;
}
public string Name { get; init; }
public Uri? Source { get; init; }
}
}

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

@ -28,6 +28,20 @@ namespace Avalonia.Media.Fonts
HasFallbacks = _names.Length > 1;
}
internal FamilyNameCollection(FrugalStructList<FontSourceIdentifier> fontSources)
{
_names = new string[fontSources.Count];
for (int i = 0; i < fontSources.Count; i++)
{
_names[i] = fontSources[i].Name;
}
PrimaryFamilyName = _names[0];
HasFallbacks = _names.Length > 1;
}
private static string[] SplitNames(string names)
#if NET6_0_OR_GREATER
=> names.Split(',', StringSplitOptions.TrimEntries);

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

@ -34,7 +34,7 @@ namespace Avalonia.Media.Fonts
{
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
match = new Typeface(glyphTypeface.FamilyName, style, weight, stretch);
match = new Typeface(Key.AbsoluteUri + "#" + glyphTypeface.FamilyName, style, weight, stretch);
return true;
}
@ -45,9 +45,9 @@ namespace Avalonia.Media.Fonts
{
if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface))
{
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
if (glyphTypeface.FamilyName.Contains(familyName) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
match = new Typeface(familyName, style, weight, stretch);
match = new Typeface(Key.AbsoluteUri + "#" + familyName, style, weight, stretch);
return true;
}

2
tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs

@ -75,7 +75,7 @@ namespace Avalonia.Base.UnitTests.Media
Assert.Equal("Courier New", fontFamily.Name);
Assert.Equal(2, fontFamily.FamilyNames.Count());
Assert.Equal(2, fontFamily.FamilyNames.Count);
Assert.Equal("Times New Roman", fontFamily.FamilyNames.Last());
}

12
tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs

@ -1,5 +1,4 @@
using System;
using Avalonia.Direct2D1.Media;
using Avalonia.Direct2D1.Media;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
@ -17,8 +16,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var glyphTypeface =
new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface;
var typeface = new Typeface(new FontFamily("A, B, Arial"));
var glyphTypeface = typeface.GlyphTypeface;
Assert.Equal("Arial", glyphTypeface.FamilyName);
}
@ -31,7 +31,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
var typeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold);
var glyphTypeface = typeface.GlyphTypeface;
Assert.Equal("Arial", glyphTypeface.FamilyName);

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

@ -139,5 +139,83 @@ namespace Avalonia.Skia.UnitTests.Media
}
}
}
[Fact]
public void Should_Load_Embedded_Fallbacks()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
using (AvaloniaLocator.EnterScope())
{
var fontFamily = FontFamily.Parse("NotFound, " + s_fontUri);
var typeface = new Typeface(fontFamily);
var glyphTypeface = typeface.GlyphTypeface;
Assert.NotNull(glyphTypeface);
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
}
[Fact]
public void Should_Match_Chararcter_Width_Embedded_Fallbacks()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
using (AvaloniaLocator.EnterScope())
{
var fontFamily = FontFamily.Parse("NotFound, " + s_fontUri);
Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface));
var glyphTypeface = typeface.GlyphTypeface;
Assert.NotNull(glyphTypeface);
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
}
[Fact]
public void Should_Match_Chararcter_From_SystemFonts()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
using (AvaloniaLocator.EnterScope())
{
Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var typeface));
var glyphTypeface = typeface.GlyphTypeface;
Assert.NotNull(glyphTypeface);
Assert.Equal(FontManager.Current.DefaultFontFamily.Name, glyphTypeface.FamilyName);
}
}
}
[Fact]
public void Should_Match_Chararcter_Width_Fallbacks()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
using (AvaloniaLocator.EnterScope())
{
var fontFamily = FontFamily.Parse("NotFound, Unknown");
Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface));
var glyphTypeface = typeface.GlyphTypeface;
Assert.NotNull(glyphTypeface);
Assert.Equal(FontManager.Current.DefaultFontFamily.Name, glyphTypeface.FamilyName);
}
}
}
}
}

Loading…
Cancel
Save