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 3 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;
using System.Collections.Generic;
using Avalonia.Media.Fonts; using Avalonia.Media.Fonts;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -34,19 +36,42 @@ namespace Avalonia.Media
throw new ArgumentNullException(nameof(name)); 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> /// <summary>
@ -88,44 +113,49 @@ namespace Avalonia.Media
return new FontFamily(s); return new FontFamily(s);
} }
private struct FontFamilyIdentifier private static FrugalStructList<FontSourceIdentifier> GetFontSourceIdentifier(string name)
{ {
public FontFamilyIdentifier(string name, Uri? source) var result = new FrugalStructList<FontSourceIdentifier>(1);
{
Name = name;
Source = source;
}
public string Name { get; }
public Uri? Source { get; }
}
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: var segment = segments[i];
{ var innerSegments = segment.Split('#');
return new FontFamilyIdentifier(segments[0], null);
}
case 2: FontSourceIdentifier identifier;
{
var source = segments[0].StartsWith("/", StringComparison.Ordinal)
? new Uri(segments[0], UriKind.Relative)
: new Uri(segments[0], UriKind.RelativeOrAbsolute);
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: result.Add(identifier);
{
return new FontFamilyIdentifier(name, null);
}
} }
return result;
} }
/// <summary> /// <summary>

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

@ -15,9 +15,11 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public sealed class FontManager 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 FontCollectionScheme = "fonts";
public const string SystemFontScheme = "systemfont";
public const string CompositeFontScheme = "compositefont";
private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>(); private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
private readonly IReadOnlyList<FontFallback>? _fontFallbacks; private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
@ -95,69 +97,86 @@ namespace Avalonia.Media
var fontFamily = typeface.FontFamily; 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); 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 (fontFamily.Key is CompositeFontFamilyKey compositeKey)
if (!source.IsAbsoluteUri)
{ {
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;
}
}
} }
else
if (!_fontCollections.TryGetValue(source, out var fontCollection) && (source.IsAbsoluteResm() || source.IsAvares()))
{ {
var embeddedFonts = new EmbeddedFontCollection(source, source); if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, fontFamily.FamilyNames.PrimaryFamilyName, out glyphTypeface))
embeddedFonts.Initialize(PlatformImpl);
if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
{ {
fontCollection = embeddedFonts; return true;
} }
}
if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, return false;
typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) }
}
else
{
if (SystemFonts.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{ {
return true; return true;
} }
}
if (!fontFamily.FamilyNames.HasFallbacks) if (typeface.FontFamily == DefaultFontFamily)
{ {
return false; 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) throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
{
return true;
}
} }
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 glyphTypeface = null;
return TryGetGlyphTypeface(new Typeface(FontFamily.DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
return false;
} }
/// <summary> /// <summary>
@ -230,18 +249,17 @@ namespace Avalonia.Media
} }
//Try to match against fallbacks first //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]; 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 //Try to find a match with the system font manager
return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface); 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; 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) private static string[] SplitNames(string names)
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
=> names.Split(',', StringSplitOptions.TrimEntries); => 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 _)) 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; return true;
} }
@ -45,9 +45,9 @@ namespace Avalonia.Media.Fonts
{ {
if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface)) 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; 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("Courier New", fontFamily.Name);
Assert.Equal(2, fontFamily.FamilyNames.Count()); Assert.Equal(2, fontFamily.FamilyNames.Count);
Assert.Equal("Times New Roman", fontFamily.FamilyNames.Last()); 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.Media;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -17,8 +16,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{ {
Direct2D1Platform.Initialize(); Direct2D1Platform.Initialize();
var glyphTypeface = var typeface = new Typeface(new FontFamily("A, B, Arial"));
new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface;
var glyphTypeface = typeface.GlyphTypeface;
Assert.Equal("Arial", glyphTypeface.FamilyName); Assert.Equal("Arial", glyphTypeface.FamilyName);
} }
@ -31,7 +31,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{ {
Direct2D1Platform.Initialize(); 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); 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