Browse Source

Introduce font collections

pull/10455/head
Benedikt Stebner 3 years ago
parent
commit
d08083bbf3
  1. 2
      samples/ControlCatalog.NetCore/Program.cs
  2. 2
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  3. 109
      src/Avalonia.Base/Media/FontManager.cs
  4. 298
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  5. 4
      src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs
  6. 58
      src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
  7. 17
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  8. 100
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  9. 20
      src/Avalonia.Base/Media/IGlyphTypeface.cs
  10. 15
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  11. 13
      src/Avalonia.Base/Media/Typeface.cs
  12. 30
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  13. 5
      src/Avalonia.Base/Utilities/UriExtensions.cs
  14. 46
      src/Avalonia.Controls/AppBuilder.cs
  15. 14
      src/Avalonia.Fonts.Inter/InterFontCollection.cs
  16. 29
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  17. 2
      src/Avalonia.Themes.Fluent/Accents/Base.xaml
  18. 85
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  19. 14
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  20. 198
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  21. 73
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  22. 13
      src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs
  23. 13
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  24. 57
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  25. 20
      src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
  26. 6
      tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
  27. 16
      tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs
  28. 66
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  29. 116
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  30. 58
      tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs
  31. 79
      tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs
  32. 63
      tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs
  33. 37
      tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
  34. 9
      tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs
  35. 20
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  36. 8
      tests/Avalonia.UnitTests/MockGlyphTypeface.cs

2
samples/ControlCatalog.NetCore/Program.cs

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Fonts.Inter;
using Avalonia.Headless;
using Avalonia.LogicalTree;
using Avalonia.Threading;
@ -124,6 +125,7 @@ namespace ControlCatalog.NetCore
EnableIme = true
})
.UseSkia()
.WithFonts(new InterFontCollection())
.AfterSetup(builder =>
{
builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()

2
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Get<ComboBox>("fontComboBox");
fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
fontComboBox.Items = FontManager.Current.SystemFonts;
fontComboBox.SelectedIndex = 0;
}
}

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

@ -1,9 +1,11 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media
{
@ -13,9 +15,10 @@ namespace Avalonia.Media
/// </summary>
public sealed class FontManager
{
private readonly ConcurrentDictionary<Typeface, IGlyphTypeface> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, IGlyphTypeface>();
private readonly FontFamily _defaultFontFamily;
public const string FontCollectionScheme = "fonts";
private readonly SystemFontCollection _systemFonts;
private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
public FontManager(IFontManagerImpl platformImpl)
@ -33,7 +36,7 @@ namespace Avalonia.Media
throw new InvalidOperationException("Default font family name can't be null or empty.");
}
_defaultFontFamily = new FontFamily(DefaultFontFamilyName);
_systemFonts = new SystemFontCollection(this);
}
public static FontManager Current
@ -57,11 +60,6 @@ namespace Avalonia.Media
}
}
/// <summary>
///
/// </summary>
public IFontManagerImpl PlatformImpl { get; }
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
@ -71,41 +69,92 @@ namespace Avalonia.Media
}
/// <summary>
/// Get all installed font family names.
/// Get all system fonts.
/// </summary>
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
public IFontCollection SystemFonts => _systemFonts;
internal IFontManagerImpl PlatformImpl { get; }
/// <summary>
/// Returns a new <see cref="IGlyphTypeface"/>, or an existing one if a matching <see cref="IGlyphTypeface"/> exists.
/// Tries to get a glyph typeface for specified typeface.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns>
/// The <see cref="IGlyphTypeface"/>.
/// <c>True</c>, if the <see cref="FontManager"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
public IGlyphTypeface GetOrAddGlyphTypeface(Typeface typeface)
public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
while (true)
glyphTypeface = null;
var fontFamily = typeface.FontFamily;
if (fontFamily.Key is FontFamilyKey key)
{
if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface))
var source = key.Source;
if (!source.IsAbsoluteUri)
{
return glyphTypeface;
if (key.BaseUri == null)
{
throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
}
source = new Uri(key.BaseUri, source);
}
glyphTypeface = PlatformImpl.CreateGlyphTypeface(typeface);
if (!_fontCollections.TryGetValue(source, out var fontCollection))
{
var embeddedFonts = new EmbeddedFontCollection(source, source);
embeddedFonts.Initialize(PlatformImpl);
if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
{
fontCollection = embeddedFonts;
}
}
if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface))
if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return glyphTypeface;
return true;
}
}
if (typeface.FontFamily == _defaultFontFamily)
foreach (var familyName in fontFamily.FamilyNames)
{
if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
throw new InvalidOperationException($"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
return true;
}
}
return false;
}
public void AddFontCollection(IFontCollection fontCollection)
{
var key = fontCollection.Key;
if (!fontCollection.Key.IsFontCollection())
{
throw new ArgumentException(nameof(fontCollection), "Font collection Key should follow the fontCollection: scheme.");
}
typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight);
if (!_fontCollections.TryAdd(key, fontCollection))
{
throw new ArgumentException(nameof(fontCollection), "Font collection is already registered.");
}
fontCollection.Initialize(PlatformImpl);
}
public void RemoveFontCollection(Uri key)
{
if (_fontCollections.TryRemove(key, out var fontCollection))
{
fontCollection.Dispose();
}
}
@ -123,18 +172,16 @@ namespace Avalonia.Media
/// <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch,
FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
{
if(_fontFallbacks != null)
if (_fontFallbacks != null)
{
foreach (var fallback in _fontFallbacks)
{
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
var glyphTypeface = GetOrAddGlyphTypeface(typeface);
if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
return true;
}
}

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

@ -0,0 +1,298 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media.Fonts
{
public class EmbeddedFontCollection : IFontCollection
{
private readonly Dictionary<string, Dictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache =
new Dictionary<string, Dictionary<FontCollectionKey, IGlyphTypeface>>();
private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(1);
private readonly Uri _key;
private readonly Uri _source;
public EmbeddedFontCollection(Uri key, Uri source)
{
_key = key;
if(!source.IsAvares() && !source.IsAbsoluteResm())
{
throw new ArgumentOutOfRangeException(nameof(source), "Specified source uri does not follow the resm: or avares: scheme.");
}
_source = source;
}
public Uri Key => _key;
public FontFamily this[int index] => _fontFamilies[index];
public int Count => _fontFamilies.Count;
public void Initialize(IFontManagerImpl fontManager)
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontAssets = FontFamilyLoader.LoadFontAssets(_source);
foreach (var fontAsset in fontAssets)
{
var stream = assetLoader.Open(fontAsset);
if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface))
{
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
{
glyphTypefaces = new Dictionary<FontCollectionKey, IGlyphTypeface>();
_glyphTypefaceCache.Add(glyphTypeface.FamilyName, glyphTypefaces);
_fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName));
}
var key = new FontCollectionKey(
glyphTypeface.Style,
glyphTypeface.Weight,
glyphTypeface.Stretch);
if (!glyphTypefaces.ContainsKey(key))
{
glyphTypefaces.Add(key, glyphTypeface);
}
}
}
}
public void Dispose()
{
foreach (var fontFamily in _fontFamilies)
{
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces))
{
foreach (var glyphTypeface in glyphTypefaces.Values)
{
glyphTypeface.Dispose();
}
}
}
GC.SuppressFinalize(this);
}
public IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
}
//Try to find a partially matching font
for (var i = 0; i < Count; i++)
{
var fontFamily = _fontFamilies[i];
if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture)))
{
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
}
}
glyphTypeface = null;
return false;
}
private static bool TryGetNearestMatch(
Dictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
return true;
}
if (key.Style != FontStyle.Normal)
{
key = key with { Style = FontStyle.Normal };
}
if (key.Stretch != FontStretch.Normal)
{
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
if (key.Weight != FontWeight.Normal)
{
if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface))
{
return true;
}
}
key = key with { Stretch = FontStretch.Normal };
}
if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
//Take the first glyph typeface we can find.
foreach (var typeface in glyphTypefaces.Values)
{
glyphTypeface = typeface;
return true;
}
return false;
}
private static bool TryFindStretchFallback(
Dictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
var stretch = (int)key.Stretch;
if (stretch < 5)
{
for (var i = 0; stretch + i < 9; i++)
{
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface))
{
return true;
}
}
}
else
{
for (var i = 0; stretch - i > 1; i++)
{
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface))
{
return true;
}
}
}
return false;
}
private static bool TryFindWeightFallback(
Dictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? typeface)
{
typeface = null;
var weight = (int)key.Weight;
//If the target weight given is between 400 and 500 inclusive
if (weight >= 400 && weight <= 500)
{
//Look for available weights between the target and 500, in ascending order.
for (var i = 0; weight + i <= 500; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights greater than 500, in ascending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
}
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
if (weight < 400)
{
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
}
//If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
if (weight > 500)
{
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
}
return false;
}
}
}

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

@ -0,0 +1,4 @@
namespace Avalonia.Media.Fonts
{
public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch);
}

58
src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs

@ -11,22 +11,22 @@ namespace Avalonia.Media.Fonts
/// <summary>
/// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey) =>
IsFontTtfOrOtf(fontFamilyKey.Source) ?
GetFontAssetsByExpression(fontFamilyKey) :
GetFontAssetsBySource(fontFamilyKey);
public static IEnumerable<Uri> LoadFontAssets(Uri source) =>
IsFontTtfOrOtf(source) ?
GetFontAssetsByExpression(source) :
GetFontAssetsBySource(source);
/// <summary>
/// Searches for font assets at a given location and returns a quantity of found assets
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsBySource(FontFamilyKey fontFamilyKey)
private static IEnumerable<Uri> GetFontAssetsBySource(Uri source)
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
var availableAssets = assetLoader.GetAssets(source, null);
return availableAssets.Where(x => IsFontTtfOrOtf(x));
}
@ -34,60 +34,50 @@ namespace Avalonia.Media.Fonts
/// Searches for font assets at a given location and only accepts assets that fit to a given filename expression.
/// <para>File names can target multiple files with * wildcard. For example "FontFile*.ttf"</para>
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
private static IEnumerable<Uri> GetFontAssetsByExpression(Uri source)
{
var (fileNameWithoutExtension, extension) = GetFileName(fontFamilyKey, out var location);
var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension);
var (fileNameWithoutExtension, extension) = GetFileName(source, out var location);
var filePattern = CreateFilePattern(source, location, fileNameWithoutExtension);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
var availableResources = assetLoader.GetAssets(location, null);
return availableResources.Where(x => IsContainsFile(x, filePattern, extension));
}
private static (string fileNameWithoutExtension, string extension) GetFileName(
FontFamilyKey fontFamilyKey, out Uri location)
Uri source, out Uri location)
{
if (fontFamilyKey.Source.IsAbsoluteResm())
if (source.IsAbsoluteResm())
{
var fileName = GetFileNameAndExtension(fontFamilyKey.Source.GetUnescapeAbsolutePath(), '.');
var fileName = GetFileNameAndExtension(source.GetUnescapeAbsolutePath(), '.');
var uriLocation = fontFamilyKey.Source.GetUnescapeAbsoluteUri()
var uriLocation = source.GetUnescapeAbsoluteUri()
.Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty);
location = new Uri(uriLocation, UriKind.RelativeOrAbsolute);
return fileName;
}
var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString);
var filename = GetFileNameAndExtension(source.OriginalString);
var fullFilename = filename.fileNameWithoutExtension + filename.extension;
if (fontFamilyKey.BaseUri != null)
{
var relativePath = fontFamilyKey.Source.OriginalString
.Replace(fullFilename, string.Empty);
location = new Uri(fontFamilyKey.BaseUri, relativePath);
}
else
{
var uriString = fontFamilyKey.Source
.GetUnescapeAbsoluteUri()
.Replace(fullFilename, string.Empty);
location = new Uri(uriString);
}
var uriString = source
.GetUnescapeAbsoluteUri()
.Replace(fullFilename, string.Empty);
location = new Uri(uriString);
return filename;
}
private static string CreateFilePattern(
FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension)
Uri source, Uri location, string fileNameWithoutExtension)
{
var path = location.GetUnescapeAbsolutePath();
var file = GetSubString(fileNameWithoutExtension, '*');
return fontFamilyKey.Source.IsAbsoluteResm()
return source.IsAbsoluteResm()
? path + "." + file
: path + file;
}

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

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
public interface IFontCollection : IReadOnlyList<FontFamily>, IDisposable
{
Uri Key { get; }
void Initialize(IFontManagerImpl fontManager);
bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
}
}

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

@ -0,0 +1,100 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
internal class SystemFontCollection : IFontCollection
{
private readonly Dictionary<string, Dictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache =
new Dictionary<string, Dictionary<FontCollectionKey, IGlyphTypeface>>();
private readonly FontManager _fontManager;
private readonly string[] _familyNames;
public SystemFontCollection(FontManager fontManager)
{
_fontManager = fontManager;
_familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames();
}
public Uri Key => new Uri("fontCollection:SystemFonts");
public FontFamily this[int index]
{
get
{
var familyName = _familyNames[index];
return new FontFamily(familyName);
}
}
public int Count => _familyNames.Length;
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (familyName == FontFamily.DefaultFontFamilyName)
{
familyName = _fontManager.DefaultFontFamilyName;
}
var key = new FontCollectionKey(style, weight, stretch);
if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
glyphTypefaces = new Dictionary<FontCollectionKey, IGlyphTypeface>();
_glyphTypefaceCache.Add(familyName, glyphTypefaces);
}
if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
return true;
}
if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
{
glyphTypefaces.Add(key, glyphTypeface);
return true;
}
return false;
}
public void Initialize(IFontManagerImpl fontManager)
{
//We initialize the system font collection during construction.
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<FontFamily> GetEnumerator()
{
foreach (var familyName in _familyNames)
{
yield return new FontFamily(familyName);
}
}
void IDisposable.Dispose()
{
foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
{
foreach (var pair in glyphTypefaces)
{
pair.Value.Dispose();
}
}
GC.SuppressFinalize(this);
}
}
}

20
src/Avalonia.Base/Media/IGlyphTypeface.cs

@ -6,6 +6,26 @@ namespace Avalonia.Media
[Unstable]
public interface IGlyphTypeface : IDisposable
{
/// <summary>
/// Gets the family name for the <see cref="IGlyphTypeface"/> object.
/// </summary>
string FamilyName { get; }
/// <summary>
/// Gets the designed weight of the font represented by the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontWeight Weight { get; }
/// <summary>
/// Gets the style for the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontStyle Style { get; }
/// <summary>
/// Gets the <see cref="FontStretch"/> value for the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontStretch Stretch { get; }
/// <summary>
/// Gets the number of glyphs held by this glyph typeface.
/// </summary>

15
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -122,13 +122,14 @@ namespace Avalonia.Media.TextFormatting
if (matchFound)
{
// Fallback found
var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
{
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
biDiLevel);
}
if(fontManager.TryGetGlyphTypeface(fallbackTypeface, out var fallbackGlyphTypeface))
{
if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
{
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
biDiLevel);
}
}
}
// no fallback found

13
src/Avalonia.Base/Media/Typeface.cs

@ -80,7 +80,18 @@ namespace Avalonia.Media
/// <value>
/// The glyph typeface.
/// </value>
public IGlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this);
public IGlyphTypeface GlyphTypeface
{
get
{
if(FontManager.Current.TryGetGlyphTypeface(this, out var glyphTypeface))
{
return glyphTypeface;
}
throw new InvalidOperationException("Could not create glyphTypeface.");
}
}
public static bool operator !=(Typeface a, Typeface b)
{

30
src/Avalonia.Base/Platform/IFontManagerImpl.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Metadata;
@ -17,7 +18,7 @@ namespace Avalonia.Platform
/// Get all installed fonts in the system.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// </summary>
IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
string[] GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
@ -37,12 +38,27 @@ namespace Avalonia.Platform
FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface);
/// <summary>
/// Creates a glyph typeface.
/// Tries to get a glyph typeface for specified parameters.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>0
/// The created glyph typeface. Can be <c>Null</c> if it was not possible to create a glyph typeface.
/// <param name="familyName">The family name.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weiht.</param>
/// <param name="stretch">The font stretch.</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(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
/// <summary>
/// Tries to create a glyph typeface from specified stream.
/// </summary>
/// <param name="stream">A stream that holds the font's data.</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>
IGlyphTypeface CreateGlyphTypeface(Typeface typeface);
bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
}
}

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

@ -1,4 +1,5 @@
using System;
using Avalonia.Media;
namespace Avalonia.Utilities;
@ -10,7 +11,9 @@ internal static class UriExtensions
public static bool IsResm(this Uri uri) => uri.Scheme == "resm";
public static bool IsAvares(this Uri uri) => uri.Scheme == "avares";
public static bool IsFontCollection(this Uri uri) => uri.Scheme == FontManager.FontCollectionScheme;
public static Uri EnsureAbsolute(this Uri uri, Uri? baseUri)
{
if (uri.IsAbsoluteUri)

46
src/Avalonia.Controls/AppBuilder.cs

@ -4,6 +4,9 @@ using System.Reflection;
using System.Linq;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Media.Fonts;
using Avalonia.Media;
using System.Xml.Linq;
namespace Avalonia
{
@ -16,7 +19,7 @@ namespace Avalonia
private Action? _optionsInitializers;
private Func<Application>? _appFactory;
private IApplicationLifetime? _lifetime;
/// <summary>
/// Gets or sets the <see cref="IRuntimePlatform"/> instance.
/// </summary>
@ -31,12 +34,12 @@ namespace Avalonia
/// Gets the <see cref="Application"/> instance being initialized.
/// </summary>
public Application? Instance { get; private set; }
/// <summary>
/// Gets the type of the Instance (even if it's not created yet)
/// </summary>
public Type? ApplicationType { get; private set; }
/// <summary>
/// Gets or sets a method to call the initialize the windowing subsystem.
/// </summary>
@ -64,7 +67,7 @@ namespace Avalonia
public Action<AppBuilder> AfterPlatformServicesSetupCallback { get; private set; } = builder => { };
/// <summary>
/// Initializes a new instance of the <see cref="AppBuilder"/> class.
/// </summary>
@ -73,7 +76,7 @@ namespace Avalonia
builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AppBuilder"/> class.
/// </summary>
@ -123,8 +126,8 @@ namespace Avalonia
AfterSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterSetupCallback, callback);
return Self;
}
public AppBuilder AfterPlatformServicesSetup(Action<AppBuilder> callback)
{
AfterPlatformServicesSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterPlatformServicesSetupCallback, callback);
@ -132,7 +135,7 @@ namespace Avalonia
}
public delegate void AppMainDelegate(Application app, string[] args);
public void Start(AppMainDelegate main, string[] args)
{
Setup();
@ -160,7 +163,7 @@ namespace Avalonia
Setup();
return Self;
}
/// <summary>
/// Specifies a windowing subsystem to use.
/// </summary>
@ -195,7 +198,7 @@ namespace Avalonia
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToConstant(options); };
return Self;
}
/// <summary>
/// Configures platform-specific options
/// </summary>
@ -204,7 +207,28 @@ namespace Avalonia
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToFunc(options); };
return Self;
}
/// <summary>
/// Registers a custom font collection with the font manager.
/// </summary>
/// <param name="fontCollection">The font collection.</param>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
/// <exception cref="ArgumentNullException"></exception>
public AppBuilder WithFonts(IFontCollection fontCollection)
{
if(fontCollection == null)
{
throw new ArgumentNullException(nameof(fontCollection), "Font collection can't be null.");
}
return AfterSetup(appBuilder =>
{
var fontManager = FontManager.Current;
fontManager.AddFontCollection(fontCollection);
});
}
/// <summary>
/// Sets up the platform-specific services for the <see cref="Application"/>.
/// </summary>

14
src/Avalonia.Fonts.Inter/InterFontCollection.cs

@ -0,0 +1,14 @@
using System;
using Avalonia.Media.Fonts;
namespace Avalonia.Fonts.Inter
{
public sealed class InterFontCollection : EmbeddedFontCollection
{
public InterFontCollection() : base(
new Uri("fonts:Inter", UriKind.Absolute),
new Uri("avares://Avalonia.Fonts.Inter/Assets", UriKind.Absolute))
{
}
}
}

29
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -84,6 +84,14 @@ namespace Avalonia.Headless
public FontSimulations FontSimulations { get; }
public string FamilyName => "Arial";
public FontWeight Weight => FontWeight.Normal;
public FontStyle Style => FontStyle.Normal;
public FontStretch Stretch => FontStretch.Normal;
public void Dispose()
{
}
@ -147,19 +155,28 @@ namespace Avalonia.Headless
class HeadlessFontManagerStub : IFontManagerImpl
{
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public string GetDefaultFontFamilyName()
{
return new HeadlessGlyphTypefaceImpl();
return "Arial";
}
public string GetDefaultFontFamilyName()
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
return "Arial";
return new string[] { "Arial" };
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface)
{
return new List<string> { "Arial" };
glyphTypeface= new HeadlessGlyphTypefaceImpl();
return true;
}
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
glyphTypeface = new HeadlessGlyphTypefaceImpl();
return true;
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,

2
src/Avalonia.Themes.Fluent/Accents/Base.xaml

@ -3,7 +3,7 @@
xmlns:sys="using:System"
xmlns:converters="using:Avalonia.Controls.Converters">
<!-- https://docs.microsoft.com/en-us/previous-versions/windows/apps/dn518235(v=win.10)?redirectedfrom=MSDN -->
<FontFamily x:Key="ContentControlThemeFontFamily">avares://Avalonia.Fonts.Inter/Assets#Inter, $Default</FontFamily>
<FontFamily x:Key="ContentControlThemeFontFamily">fonts:Inter#Inter, $Default</FontFamily>
<sys:Double x:Key="ControlContentThemeFontSize">14</sys:Double>
<SolidColorBrush x:Key="SystemControlTransparentBrush" Color="Transparent" />

85
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
@ -16,14 +17,14 @@ namespace Avalonia.Skia
return SKTypeface.Default.FamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
if (checkForUpdates)
{
_skFontManager = SKFontManager.CreateDefault();
}
return _skFontManager.FontFamilies;
return _skFontManager.GetFontFamilies();
}
[ThreadStatic] private static string[]? t_languageTagBuffer;
@ -95,72 +96,58 @@ namespace Avalonia.Skia
return false;
}
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
SKTypeface? skTypeface = null;
glyphTypeface = null;
if(typeface.FontFamily.Key is not null)
{
var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
skTypeface = fontCollection.Get(typeface);
var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch,
(SKFontStyleSlant)style);
if (skTypeface is null && !typeface.FontFamily.FamilyNames.HasFallbacks)
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
}
var skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
if (skTypeface is null)
{
var defaultName = SKTypeface.Default.FamilyName;
var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, (SKFontStyleWidth)typeface.Stretch,
(SKFontStyleSlant)typeface.Style);
foreach (var familyName in typeface.FontFamily.FamilyNames)
{
if(familyName == FontFamily.DefaultFontFamilyName)
{
continue;
}
skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
if (skTypeface is null || defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal))
{
continue;
}
break;
}
// MatchTypeface can return "null" if matched typeface wasn't found for the style
// Fallback to the default typeface and styles instead.
skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle)
?? SKTypeface.Default;
return false;
}
if (skTypeface == null)
//MatchFamily can return a font other than we requested so we have to verify we got the expected.
if (!skTypeface.FamilyName.ToLower(CultureInfo.InvariantCulture).Equals(familyName.ToLower(CultureInfo.InvariantCulture), StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
return false;
}
var fontSimulations = FontSimulations.None;
if((int)typeface.Weight >= 600 && !skTypeface.IsBold)
if ((int)weight >= 600 && !skTypeface.IsBold)
{
fontSimulations |= FontSimulations.Bold;
}
if(typeface.Style == FontStyle.Italic && !skTypeface.IsItalic)
if (style == FontStyle.Italic && !skTypeface.IsItalic)
{
fontSimulations |= FontSimulations.Oblique;
}
return new GlyphTypefaceImpl(skTypeface, fontSimulations);
glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);
return true;
}
public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
var skTypeface = SKTypeface.FromStream(stream);
if (skTypeface != null)
{
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
return true;
}
glyphTypeface = null;
return false;
}
}
}

14
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -51,6 +51,12 @@ namespace Avalonia.Skia
GlyphCount = Typeface.GlyphCount;
FontSimulations = fontSimulations;
Weight = (FontWeight)Typeface.FontWeight;
Style = Typeface.FontSlant.ToAvalonia();
Stretch = (FontStretch)Typeface.FontStyle.Width;
}
public Face Face { get; }
@ -67,6 +73,14 @@ namespace Avalonia.Skia
public int GlyphCount { get; }
public string FamilyName => Typeface.FamilyName;
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = default;

198
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -1,198 +0,0 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class SKTypefaceCollection
{
private readonly ConcurrentDictionary<Typeface, SKTypeface> _typefaces = new();
public void AddTypeface(Typeface key, SKTypeface typeface)
{
_typefaces.TryAdd(key, typeface);
}
public SKTypeface? Get(Typeface typeface)
{
return GetNearestMatch(typeface);
}
private SKTypeface? GetNearestMatch(Typeface key)
{
if (_typefaces.Count == 0)
{
return null;
}
if (_typefaces.TryGetValue(key, out var typeface))
{
return typeface;
}
if(key.Style != FontStyle.Normal)
{
key = new Typeface(key.FontFamily, FontStyle.Normal, key.Weight, key.Stretch);
}
if(key.Stretch != FontStretch.Normal)
{
if(TryFindStretchFallback(key, out typeface))
{
return typeface;
}
if(key.Weight != FontWeight.Normal)
{
if (TryFindStretchFallback(new Typeface(key.FontFamily, key.Style, FontWeight.Normal, key.Stretch), out typeface))
{
return typeface;
}
}
key = new Typeface(key.FontFamily, key.Style, key.Weight, FontStretch.Normal);
}
if(TryFindWeightFallback(key, out typeface))
{
return typeface;
}
if (TryFindStretchFallback(key, out typeface))
{
return typeface;
}
//Nothing was found so we try some regular typeface.
if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface))
{
return typeface;
}
SKTypeface? skTypeface = null;
foreach(var pair in _typefaces)
{
skTypeface = pair.Value;
if (skTypeface.FamilyName.Contains(key.FontFamily.Name))
{
return skTypeface;
}
}
return skTypeface;
}
private bool TryFindStretchFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface)
{
typeface = null;
var stretch = (int)key.Stretch;
if (stretch < 5)
{
for (var i = 0; stretch + i < 9; i++)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch + i)), out typeface))
{
return true;
}
}
}
else
{
for (var i = 0; stretch - i > 1; i++)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch - i)), out typeface))
{
return true;
}
}
}
return false;
}
private bool TryFindWeightFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface)
{
typeface = null;
var weight = (int)key.Weight;
//If the target weight given is between 400 and 500 inclusive
if (weight >= 400 && weight <= 500)
{
//Look for available weights between the target and 500, in ascending order.
for (var i = 0; weight + i <= 500; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights greater than 500, in ascending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
}
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
if (weight < 400)
{
for (var i = 0; weight - i >= 100; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
}
//If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
if (weight > 500)
{
for (var i = 0; weight + i <= 900; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
{
return true;
}
}
}
return false;
}
}
}

73
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@ -1,73 +0,0 @@
using System;
using System.Collections.Concurrent;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
{
internal static class SKTypefaceCollectionCache
{
private static readonly ConcurrentDictionary<FontFamily, SKTypefaceCollection> s_cachedCollections;
static SKTypefaceCollectionCache()
{
s_cachedCollections = new ConcurrentDictionary<FontFamily, SKTypefaceCollection>();
}
/// <summary>
/// Gets the or add typeface collection.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <returns></returns>
public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily)
{
return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection);
}
/// <summary>
/// Creates the custom font collection.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <returns></returns>
private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily)
{
var typeFaceCollection = new SKTypefaceCollection();
if (fontFamily.Key is not { } fontFamilyKey)
{
return typeFaceCollection;
}
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamilyKey);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
foreach (var asset in fontAssets)
{
var assetStream = assetLoader.Open(asset);
if (assetStream == null)
throw new InvalidOperationException("Asset could not be loaded.");
var typeface = SKTypeface.FromStream(assetStream);
if (typeface == null)
throw new InvalidOperationException("Typeface could not be loaded.");
if (!typeface.FamilyName.Contains(fontFamily.Name))
{
continue;
}
var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(),
(FontWeight)typeface.FontWeight, (FontStretch)typeface.FontWidth);
typeFaceCollection.AddTypeface(key, typeface);
}
return typeFaceCollection;
}
}
}

13
src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs

@ -1,11 +1,10 @@
using System.Collections.Generic;
using Avalonia.Platform;
using SharpDX;
using SharpDX.DirectWrite;
namespace Avalonia.Direct2D1.Media
{
using System;
using System.IO;
internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader
{
@ -18,19 +17,15 @@ namespace Avalonia.Direct2D1.Media
/// </summary>
/// <param name="factory">The factory.</param>
/// <param name="fontAssets"></param>
public DWriteResourceFontLoader(Factory factory, IEnumerable<Uri> fontAssets)
public DWriteResourceFontLoader(Factory factory, Stream[] fontAssets)
{
var factory1 = factory;
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
foreach (var asset in fontAssets)
{
var assetStream = assetLoader.Open(asset);
var dataStream = new DataStream((int)assetStream.Length, true, true);
var dataStream = new DataStream((int)asset.Length, true, true);
assetStream.CopyTo(dataStream);
asset.CopyTo(dataStream);
dataStream.Position = 0;

13
src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs

@ -6,6 +6,9 @@ using FontFamily = Avalonia.Media.FontFamily;
using FontStyle = SharpDX.DirectWrite.FontStyle;
using FontWeight = SharpDX.DirectWrite.FontWeight;
using FontStretch = SharpDX.DirectWrite.FontStretch;
using Avalonia.Platform;
using System.Linq;
using System;
namespace Avalonia.Direct2D1.Media
{
@ -53,9 +56,15 @@ namespace Avalonia.Direct2D1.Media
private static FontCollection CreateFontCollection(FontFamilyKey key)
{
var assets = FontFamilyLoader.LoadFontAssets(key);
var source = key.BaseUri != null ? new Uri(key.BaseUri, key.Source) : key.Source;
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets);
var assets = FontFamilyLoader.LoadFontAssets(source);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontAssets = assets.Select(x => assetLoader.Open(x)).ToArray();
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, fontAssets);
return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
}

57
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
using FontStretch = Avalonia.Media.FontStretch;
using FontStyle = Avalonia.Media.FontStyle;
@ -18,7 +18,7 @@ namespace Avalonia.Direct2D1.Media
return "Segoe UI";
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@ -62,9 +62,56 @@ namespace Avalonia.Direct2D1.Media
return false;
}
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
return new GlyphTypefaceImpl(typeface);
var systemFonts = Direct2D1FontCollectionCache.InstalledFontCollection;
if (familyName == FontFamily.DefaultFontFamilyName)
{
familyName = "Segoe UI";
}
if (systemFonts.FindFamilyName(familyName, out var index))
{
var font = systemFonts.GetFontFamily(index).GetFirstMatchingFont(
(SharpDX.DirectWrite.FontWeight)weight,
(SharpDX.DirectWrite.FontStretch)stretch,
(SharpDX.DirectWrite.FontStyle)style);
glyphTypeface = new GlyphTypefaceImpl(font);
return true;
}
glyphTypeface = null;
return false;
}
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, new[] { stream });
var fontCollection = new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
if (fontCollection.FontFamilyCount > 0)
{
var fontFamily = fontCollection.GetFontFamily(0);
if (fontFamily.FontCount > 0)
{
var font = fontFamily.GetFont(0);
glyphTypeface = new GlyphTypefaceImpl(font);
return true;
}
}
glyphTypeface = null;
return false;
}
}
}

20
src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs

@ -12,9 +12,9 @@ namespace Avalonia.Direct2D1.Media
{
private bool _isDisposed;
public GlyphTypefaceImpl(Typeface typeface)
public GlyphTypefaceImpl(SharpDX.DirectWrite.Font font)
{
DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
DWFont = font;
FontFace = new FontFace(DWFont).QueryInterface<FontFace1>();
@ -48,6 +48,14 @@ namespace Avalonia.Direct2D1.Media
StrikethroughThickness = strikethroughThickness,
IsFixedPitch = FontFace.IsMonospacedFont
};
FamilyName = DWFont.FontFamily.FamilyNames.GetString(0);
Weight = (Avalonia.Media.FontWeight)DWFont.Weight;
Style = (Avalonia.Media.FontStyle)DWFont.Style;
Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch;
}
private Blob GetTable(Face face, Tag tag)
@ -83,6 +91,14 @@ namespace Avalonia.Direct2D1.Media
public FontSimulations FontSimulations => FontSimulations.None;
public string FamilyName { get; }
public Avalonia.Media.FontWeight Weight { get; }
public Avalonia.Media.FontStyle Style { get; }
public Avalonia.Media.FontStretch Stretch { get; }
/// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint)
{

6
tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs

@ -16,9 +16,11 @@ namespace Avalonia.Base.UnitTests.Media
var typeface = new Typeface(fontFamily);
var glyphTypeface = FontManager.Current.GetOrAddGlyphTypeface(typeface);
Assert.True(FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface));
Assert.Same(glyphTypeface, FontManager.Current.GetOrAddGlyphTypeface(typeface));
FontManager.Current.TryGetGlyphTypeface(typeface, out var other);
Assert.Same(glyphTypeface, other);
}
}

16
tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs

@ -46,9 +46,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Single_FontAsset()
{
var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
var fontAssets = FontFamilyLoader.LoadFontAssets(source);
Assert.Single(fontAssets);
}
@ -57,9 +56,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Single_FontAsset_Avares_Without_BaseUri()
{
var source = new Uri(AssetYourFontAvares);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
var fontAssets = FontFamilyLoader.LoadFontAssets(source);
Assert.Single(fontAssets);
}
@ -69,9 +67,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
{
var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute);
var baseUri = new Uri(AssetLocationAvares);
var key = new FontFamilyKey(source, baseUri);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
var fontAssets = FontFamilyLoader.LoadFontAssets(new Uri(baseUri, source));
Assert.Single(fontAssets);
}
@ -80,9 +77,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Matching_Assets()
{
var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray();
var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray();
foreach (var fontAsset in fontAssets)
{
@ -99,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontFamily = new FontFamily("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono");
var source = new Uri("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono", UriKind.RelativeOrAbsolute);
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key).ToArray();
var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray();
Assert.NotEmpty(fontAssets);

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

@ -1,4 +1,5 @@
using Avalonia.Direct2D1.Media;
using System;
using Avalonia.Direct2D1.Media;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
@ -16,18 +17,10 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, Arial")));
var font = glyphTypeface.DWFont;
Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
var glyphTypeface =
new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface;
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
Assert.Equal("Arial", glyphTypeface.FamilyName);
}
}
@ -38,42 +31,29 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold));
var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
var font = glyphTypeface.DWFont;
Assert.Equal("Arial", glyphTypeface.FamilyName);
Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(FontWeight.Bold, glyphTypeface.Weight);
Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
Assert.Equal(FontStyle.Normal, glyphTypeface.Style);
}
}
[Fact]
public void Should_Create_Typeface_For_Unknown_Font()
public void Should_Throw_InvalidOperationException_For_Unknown_Font()
{
using (AvaloniaLocator.EnterScope())
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("Unknown")));
var font = glyphTypeface.DWFont;
var defaultName = fontManager.GetDefaultFontFamilyName();
Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
var fontManager = FontManager.Current;
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
Assert.Throws<InvalidOperationException>(() =>
{
var glyphTypeface =new Typeface(new FontFamily("Unknown")).GlyphTypeface;
});
}
}
@ -86,12 +66,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri));
var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface;
var font = glyphTypeface.DWFont;
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
@ -102,14 +79,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
var font = glyphTypeface.DWFont;
var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface;
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
}

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

@ -1,10 +1,12 @@
using System.Collections.Generic;
using System;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SkiaSharp;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace Avalonia.Skia.UnitTests.Media
{
@ -35,9 +37,9 @@ namespace Avalonia.Skia.UnitTests.Media
return _defaultFamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
return _customTypefaces.Select(x => x.FontFamily.Name);
return _customTypefaces.Select(x => x.FontFamily.Name).ToArray();
}
private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
@ -70,48 +72,132 @@ namespace Avalonia.Skia.UnitTests.Media
{
SKTypeface skTypeface;
Uri source = null;
switch (typeface.FontFamily.Name)
{
case "Twitter Color Emoji":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
source = _emojiTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
source = _italicTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans Arabic":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_arabicTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
source = _arabicTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans Hebrew":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_hebrewTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
source = _hebrewTypeface.FontFamily.Key.Source;
break;
}
case FontFamily.DefaultFontFamilyName:
case "Noto Mono":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);
skTypeface = typefaceCollection.Get(_defaultTypeface);
source = _defaultTypeface.FontFamily.Key.Source;
break;
}
default:
{
skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
(SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
break;
}
}
if (source is null)
{
skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
(SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
}
else
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var assetUri = FontFamilyLoader.LoadFontAssets(source).First();
var stream = assetLoader.Open(assetUri);
skTypeface = SKTypeface.FromStream(stream);
}
return new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
}
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
SKTypeface skTypeface;
Uri source = null;
switch (familyName)
{
case "Twitter Color Emoji":
{
source = _emojiTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans":
{
source = _italicTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans Arabic":
{
source = _arabicTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans Hebrew":
{
source = _hebrewTypeface.FontFamily.Key.Source;
break;
}
case FontFamily.DefaultFontFamilyName:
case "Noto Mono":
{
source = _defaultTypeface.FontFamily.Key.Source;
break;
}
default:
{
break;
}
}
if (source is null)
{
skTypeface = SKTypeface.FromFamilyName(familyName,
(SKFontStyleWeight)weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)style);
}
else
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var assetUri = FontFamilyLoader.LoadFontAssets(source).First();
var stream = assetLoader.Open(assetUri);
skTypeface = SKTypeface.FromStream(stream);
}
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
return true;
}
public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
var skTypeface = SKTypeface.FromStream(stream);
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
return true;
}
}
}

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

@ -0,0 +1,58 @@
using System;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Skia.UnitTests.Media
{
public class EmbeddedFontCollectionTests
{
private const string s_notoMono =
"resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono";
[InlineData(FontWeight.SemiLight, FontStyle.Normal)]
[InlineData(FontWeight.Bold, FontStyle.Italic)]
[InlineData(FontWeight.Heavy, FontStyle.Oblique)]
[Theory]
public void Should_Get_Near_Matching_Typeface(FontWeight fontWeight, FontStyle fontStyle)
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
{
var fontCollection = new EmbeddedFontCollection(FontManager.Current, new Uri(s_notoMono));
Assert.True(fontCollection.TryGetGlyphTypeface("Noto Mono", fontStyle, fontWeight, FontStretch.Normal, out var glyphTypeface));
var actual = glyphTypeface?.FamilyName;
Assert.Equal("Noto Mono", actual);
}
}
[Fact]
public void Should_Not_Get_Typeface_For_Invalid_FamilyName()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
{
var fontCollection = new EmbeddedFontCollection(FontManager.Current, new Uri(s_notoMono));
Assert.False(fontCollection.TryGetGlyphTypeface("ABC", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface));
}
}
[Fact]
public void Should_Get_Typeface_For_Partial_FamilyName()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
{
var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T", UriKind.Absolute);
var fontCollection = new EmbeddedFontCollection(FontManager.Current, source);
Assert.True(fontCollection.TryGetGlyphTypeface("T", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface));
Assert.Equal("Twitter Color Emoji", glyphTypeface.FamilyName);
}
}
}
}

79
tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs

@ -14,92 +14,67 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact]
public void Should_Create_Typeface_From_Fallback()
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, " + fontManager.GetDefaultFontFamilyName())));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var fontManager = FontManager.Current;
Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
var glyphTypeface = new Typeface(new FontFamily("A, B, " + fontManager.DefaultFontFamilyName)).GlyphTypeface;
Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
Assert.Equal(SKTypeface.Default.FamilyName, glyphTypeface.FamilyName);
}
}
[Fact]
public void Should_Create_Typeface_From_Fallback_Bold()
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold));
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var glyphTypeface = new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
var skTypeface = glyphTypeface.Typeface;
Assert.True(skTypeface.FontWeight >= 600);
Assert.True((int)glyphTypeface.Weight >= 600);
}
}
[Fact]
public void Should_Create_Typeface_For_Unknown_Font()
public void Should_Throw_InvalidOperationException_For_Invalid_FamilyName()
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("Unknown")));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
Assert.Throws<InvalidOperationException>(() =>
{
var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface;
});
}
}
[Fact]
public void Should_Load_Typeface_From_Resource()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri));
var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface;
var skTypeface = glyphTypeface.Typeface;
Assert.Equal("Noto Mono", skTypeface.FamilyName);
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
[Fact]
public void Should_Load_Nearest_Matching_Font()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
var skTypeface = glyphTypeface.Typeface;
var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface;
Assert.Equal("Noto Mono", skTypeface.FamilyName);
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
[Fact]
public void Should_Throw_For_Invalid_Custom_Font()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var fontManager = new FontManagerImpl();
Assert.Throws<InvalidOperationException>(() =>
fontManager.CreateGlyphTypeface(
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown")));
Assert.Throws<InvalidOperationException>(() => new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown").GlyphTypeface);
}
}
}

63
tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs

@ -1,63 +0,0 @@
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Skia.UnitTests.Media
{
public class SKTypefaceCollectionCacheTests
{
private const string s_notoMono =
"resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono";
[InlineData(s_notoMono, FontWeight.SemiLight, FontStyle.Normal)]
[InlineData(s_notoMono, FontWeight.Bold, FontStyle.Italic)]
[InlineData(s_notoMono, FontWeight.Heavy, FontStyle.Oblique)]
[Theory]
public void Should_Get_Near_Matching_Typeface(string familyName, FontWeight fontWeight, FontStyle fontStyle)
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var fontFamily = new FontFamily(familyName);
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily);
var actual = typefaceCollection.Get(new Typeface(fontFamily, fontStyle, fontWeight))?.FamilyName;
Assert.Equal("Noto Mono", actual);
}
}
[Fact]
public void Should_Get_Typeface_For_Invalid_FamilyName()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var notoMono =
new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono);
var typeface = notoMonoCollection.Get(new Typeface("ABC"));
Assert.NotNull(typeface);
}
}
[Fact]
public void Should_Get_Typeface_For_Partial_FamilyName()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var fontFamily = new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T");
var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily);
var typeface = fontCollection.Get(new Typeface(fontFamily));
Assert.NotNull(typeface);
Assert.Equal("Twitter Color Emoji", typeface.FamilyName);
}
}
}
}

37
tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
namespace Avalonia.UnitTests
@ -31,9 +30,9 @@ namespace Avalonia.UnitTests
return _defaultFamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{
return _customTypefaces.Select(x => x.FontFamily!.Name);
return _customTypefaces.Select(x => x.FontFamily!.Name).ToArray();
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,
@ -58,29 +57,19 @@ namespace Avalonia.UnitTests
return false;
}
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
var fontFamily = typeface.FontFamily;
glyphTypeface = new HarfBuzzGlyphTypefaceImpl(stream);
if (fontFamily.IsDefault)
{
fontFamily = _defaultTypeface.FontFamily;
}
if (fontFamily!.Key == null)
{
return null;
}
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key);
return true;
}
var asset = fontAssets.First();
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
glyphTypeface = null;
var stream = assetLoader.Open(asset);
return new HarfBuzzGlyphTypefaceImpl(stream);
return false;
}
}
}

9
tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs

@ -57,6 +57,15 @@ namespace Avalonia.UnitTests
public FontSimulations FontSimulations { get; }
public string FamilyName => "$Default";
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
/// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint)
{

20
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
@ -19,12 +20,12 @@ namespace Avalonia.UnitTests
return _defaultFamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{
return new[] { _defaultFamilyName };
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch, FontFamily fontFamily,
CultureInfo culture, out Typeface fontKey)
{
@ -33,9 +34,18 @@ namespace Avalonia.UnitTests
return false;
}
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
return new MockGlyphTypeface();
glyphTypeface = new MockGlyphTypeface();
return true;
}
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
glyphTypeface = new MockGlyphTypeface();
return true;
}
}
}

8
tests/Avalonia.UnitTests/MockGlyphTypeface.cs

@ -17,6 +17,14 @@ namespace Avalonia.UnitTests
public FontSimulations FontSimulations => throw new NotImplementedException();
public string FamilyName => "$Default";
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
public ushort GetGlyph(uint codepoint)
{
return (ushort)codepoint;

Loading…
Cancel
Save