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

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

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

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

@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using Avalonia.Media.Fonts; using Avalonia.Media.Fonts;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -13,9 +15,10 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public sealed class FontManager public sealed class FontManager
{ {
private readonly ConcurrentDictionary<Typeface, IGlyphTypeface> _glyphTypefaceCache = public const string FontCollectionScheme = "fonts";
new ConcurrentDictionary<Typeface, IGlyphTypeface>();
private readonly FontFamily _defaultFontFamily; private readonly SystemFontCollection _systemFonts;
private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
private readonly IReadOnlyList<FontFallback>? _fontFallbacks; private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
public FontManager(IFontManagerImpl platformImpl) public FontManager(IFontManagerImpl platformImpl)
@ -33,7 +36,7 @@ namespace Avalonia.Media
throw new InvalidOperationException("Default font family name can't be null or empty."); 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 public static FontManager Current
@ -57,11 +60,6 @@ namespace Avalonia.Media
} }
} }
/// <summary>
///
/// </summary>
public IFontManagerImpl PlatformImpl { get; }
/// <summary> /// <summary>
/// Gets the system's default font family's name. /// Gets the system's default font family's name.
/// </summary> /// </summary>
@ -71,41 +69,92 @@ namespace Avalonia.Media
} }
/// <summary> /// <summary>
/// Get all installed font family names. /// Get all system fonts.
/// </summary> /// </summary>
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param> public IFontCollection SystemFonts => _systemFonts;
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); internal IFontManagerImpl PlatformImpl { get; }
/// <summary> /// <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> /// </summary>
/// <param name="typeface">The typeface.</param> /// <param name="typeface">The typeface.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns> /// <returns>
/// The <see cref="IGlyphTypeface"/>. /// <c>True</c>, if the <see cref="FontManager"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns> /// </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. /// <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns> /// </returns>
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch, FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
{ {
if(_fontFallbacks != null) if (_fontFallbacks != null)
{ {
foreach (var fallback in _fontFallbacks) foreach (var fallback in _fontFallbacks)
{ {
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
var glyphTypeface = GetOrAddGlyphTypeface(typeface); if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
return true; 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> /// <summary>
/// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/> /// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
/// </summary> /// </summary>
/// <param name="fontFamilyKey"></param> /// <param name="source"></param>
/// <returns></returns> /// <returns></returns>
public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey) => public static IEnumerable<Uri> LoadFontAssets(Uri source) =>
IsFontTtfOrOtf(fontFamilyKey.Source) ? IsFontTtfOrOtf(source) ?
GetFontAssetsByExpression(fontFamilyKey) : GetFontAssetsByExpression(source) :
GetFontAssetsBySource(fontFamilyKey); GetFontAssetsBySource(source);
/// <summary> /// <summary>
/// Searches for font assets at a given location and returns a quantity of found assets /// Searches for font assets at a given location and returns a quantity of found assets
/// </summary> /// </summary>
/// <param name="fontFamilyKey"></param> /// <param name="source"></param>
/// <returns></returns> /// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsBySource(FontFamilyKey fontFamilyKey) private static IEnumerable<Uri> GetFontAssetsBySource(Uri source)
{ {
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); 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)); 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. /// 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> /// <para>File names can target multiple files with * wildcard. For example "FontFile*.ttf"</para>
/// </summary> /// </summary>
/// <param name="fontFamilyKey"></param> /// <param name="source"></param>
/// <returns></returns> /// <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 (fileNameWithoutExtension, extension) = GetFileName(source, out var location);
var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension); var filePattern = CreateFilePattern(source, location, fileNameWithoutExtension);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); 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)); return availableResources.Where(x => IsContainsFile(x, filePattern, extension));
} }
private static (string fileNameWithoutExtension, string extension) GetFileName( 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); .Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty);
location = new Uri(uriLocation, UriKind.RelativeOrAbsolute); location = new Uri(uriLocation, UriKind.RelativeOrAbsolute);
return fileName; return fileName;
} }
var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString); var filename = GetFileNameAndExtension(source.OriginalString);
var fullFilename = filename.fileNameWithoutExtension + filename.extension; var fullFilename = filename.fileNameWithoutExtension + filename.extension;
if (fontFamilyKey.BaseUri != null) var uriString = source
{ .GetUnescapeAbsoluteUri()
var relativePath = fontFamilyKey.Source.OriginalString .Replace(fullFilename, string.Empty);
.Replace(fullFilename, string.Empty); location = new Uri(uriString);
location = new Uri(fontFamilyKey.BaseUri, relativePath);
}
else
{
var uriString = fontFamilyKey.Source
.GetUnescapeAbsoluteUri()
.Replace(fullFilename, string.Empty);
location = new Uri(uriString);
}
return filename; return filename;
} }
private static string CreateFilePattern( private static string CreateFilePattern(
FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension) Uri source, Uri location, string fileNameWithoutExtension)
{ {
var path = location.GetUnescapeAbsolutePath(); var path = location.GetUnescapeAbsolutePath();
var file = GetSubString(fileNameWithoutExtension, '*'); var file = GetSubString(fileNameWithoutExtension, '*');
return fontFamilyKey.Source.IsAbsoluteResm() return source.IsAbsoluteResm()
? path + "." + file ? path + "." + file
: 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] [Unstable]
public interface IGlyphTypeface : IDisposable 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> /// <summary>
/// Gets the number of glyphs held by this glyph typeface. /// Gets the number of glyphs held by this glyph typeface.
/// </summary> /// </summary>

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

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

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

@ -80,7 +80,18 @@ namespace Avalonia.Media
/// <value> /// <value>
/// The glyph typeface. /// The glyph typeface.
/// </value> /// </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) 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.Globalization;
using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -17,7 +18,7 @@ namespace Avalonia.Platform
/// Get all installed fonts in the system. /// Get all installed fonts in the system.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param> /// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// </summary> /// </summary>
IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false); string[] GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary> /// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties. /// 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); FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface);
/// <summary> /// <summary>
/// Creates a glyph typeface. /// Tries to get a glyph typeface for specified parameters.
/// </summary> /// </summary>
/// <param name="typeface">The typeface.</param> /// <param name="familyName">The family name.</param>
/// <returns>0 /// <param name="style">The font style.</param>
/// The created glyph typeface. Can be <c>Null</c> if it was not possible to create a glyph typeface. /// <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> /// </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 System;
using Avalonia.Media;
namespace Avalonia.Utilities; namespace Avalonia.Utilities;
@ -10,7 +11,9 @@ internal static class UriExtensions
public static bool IsResm(this Uri uri) => uri.Scheme == "resm"; public static bool IsResm(this Uri uri) => uri.Scheme == "resm";
public static bool IsAvares(this Uri uri) => uri.Scheme == "avares"; 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) public static Uri EnsureAbsolute(this Uri uri, Uri? baseUri)
{ {
if (uri.IsAbsoluteUri) if (uri.IsAbsoluteUri)

46
src/Avalonia.Controls/AppBuilder.cs

@ -4,6 +4,9 @@ using System.Reflection;
using System.Linq; using System.Linq;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Media.Fonts;
using Avalonia.Media;
using System.Xml.Linq;
namespace Avalonia namespace Avalonia
{ {
@ -16,7 +19,7 @@ namespace Avalonia
private Action? _optionsInitializers; private Action? _optionsInitializers;
private Func<Application>? _appFactory; private Func<Application>? _appFactory;
private IApplicationLifetime? _lifetime; private IApplicationLifetime? _lifetime;
/// <summary> /// <summary>
/// Gets or sets the <see cref="IRuntimePlatform"/> instance. /// Gets or sets the <see cref="IRuntimePlatform"/> instance.
/// </summary> /// </summary>
@ -31,12 +34,12 @@ namespace Avalonia
/// Gets the <see cref="Application"/> instance being initialized. /// Gets the <see cref="Application"/> instance being initialized.
/// </summary> /// </summary>
public Application? Instance { get; private set; } public Application? Instance { get; private set; }
/// <summary> /// <summary>
/// Gets the type of the Instance (even if it's not created yet) /// Gets the type of the Instance (even if it's not created yet)
/// </summary> /// </summary>
public Type? ApplicationType { get; private set; } public Type? ApplicationType { get; private set; }
/// <summary> /// <summary>
/// Gets or sets a method to call the initialize the windowing subsystem. /// Gets or sets a method to call the initialize the windowing subsystem.
/// </summary> /// </summary>
@ -64,7 +67,7 @@ namespace Avalonia
public Action<AppBuilder> AfterPlatformServicesSetupCallback { get; private set; } = builder => { }; public Action<AppBuilder> AfterPlatformServicesSetupCallback { get; private set; } = builder => { };
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AppBuilder"/> class. /// Initializes a new instance of the <see cref="AppBuilder"/> class.
/// </summary> /// </summary>
@ -73,7 +76,7 @@ namespace Avalonia
builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly)) builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly))
{ {
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AppBuilder"/> class. /// Initializes a new instance of the <see cref="AppBuilder"/> class.
/// </summary> /// </summary>
@ -123,8 +126,8 @@ namespace Avalonia
AfterSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterSetupCallback, callback); AfterSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterSetupCallback, callback);
return Self; return Self;
} }
public AppBuilder AfterPlatformServicesSetup(Action<AppBuilder> callback) public AppBuilder AfterPlatformServicesSetup(Action<AppBuilder> callback)
{ {
AfterPlatformServicesSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterPlatformServicesSetupCallback, callback); AfterPlatformServicesSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterPlatformServicesSetupCallback, callback);
@ -132,7 +135,7 @@ namespace Avalonia
} }
public delegate void AppMainDelegate(Application app, string[] args); public delegate void AppMainDelegate(Application app, string[] args);
public void Start(AppMainDelegate main, string[] args) public void Start(AppMainDelegate main, string[] args)
{ {
Setup(); Setup();
@ -160,7 +163,7 @@ namespace Avalonia
Setup(); Setup();
return Self; return Self;
} }
/// <summary> /// <summary>
/// Specifies a windowing subsystem to use. /// Specifies a windowing subsystem to use.
/// </summary> /// </summary>
@ -195,7 +198,7 @@ namespace Avalonia
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToConstant(options); }; _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToConstant(options); };
return Self; return Self;
} }
/// <summary> /// <summary>
/// Configures platform-specific options /// Configures platform-specific options
/// </summary> /// </summary>
@ -204,7 +207,28 @@ namespace Avalonia
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToFunc(options); }; _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToFunc(options); };
return Self; 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> /// <summary>
/// Sets up the platform-specific services for the <see cref="Application"/>. /// Sets up the platform-specific services for the <see cref="Application"/>.
/// </summary> /// </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 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() public void Dispose()
{ {
} }
@ -147,19 +155,28 @@ namespace Avalonia.Headless
class HeadlessFontManagerStub : IFontManagerImpl 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, 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:sys="using:System"
xmlns:converters="using:Avalonia.Controls.Converters"> xmlns:converters="using:Avalonia.Controls.Converters">
<!-- https://docs.microsoft.com/en-us/previous-versions/windows/apps/dn518235(v=win.10)?redirectedfrom=MSDN --> <!-- 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> <sys:Double x:Key="ControlContentThemeFontSize">14</sys:Double>
<SolidColorBrush x:Key="SystemControlTransparentBrush" Color="Transparent" /> <SolidColorBrush x:Key="SystemControlTransparentBrush" Color="Transparent" />

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

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using SkiaSharp; using SkiaSharp;
@ -16,14 +17,14 @@ namespace Avalonia.Skia
return SKTypeface.Default.FamilyName; return SKTypeface.Default.FamilyName;
} }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{ {
if (checkForUpdates) if (checkForUpdates)
{ {
_skFontManager = SKFontManager.CreateDefault(); _skFontManager = SKFontManager.CreateDefault();
} }
return _skFontManager.FontFamilies; return _skFontManager.GetFontFamilies();
} }
[ThreadStatic] private static string[]? t_languageTagBuffer; [ThreadStatic] private static string[]? t_languageTagBuffer;
@ -95,72 +96,58 @@ namespace Avalonia.Skia
return false; 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 fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch,
{ (SKFontStyleSlant)style);
var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
skTypeface = fontCollection.Get(typeface);
if (skTypeface is null && !typeface.FontFamily.FamilyNames.HasFallbacks) var skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
}
if (skTypeface is null) if (skTypeface is null)
{ {
var defaultName = SKTypeface.Default.FamilyName; return false;
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;
} }
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( return false;
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
} }
var fontSimulations = FontSimulations.None; var fontSimulations = FontSimulations.None;
if((int)typeface.Weight >= 600 && !skTypeface.IsBold) if ((int)weight >= 600 && !skTypeface.IsBold)
{ {
fontSimulations |= FontSimulations.Bold; fontSimulations |= FontSimulations.Bold;
} }
if(typeface.Style == FontStyle.Italic && !skTypeface.IsItalic) if (style == FontStyle.Italic && !skTypeface.IsItalic)
{ {
fontSimulations |= FontSimulations.Oblique; 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; GlyphCount = Typeface.GlyphCount;
FontSimulations = fontSimulations; FontSimulations = fontSimulations;
Weight = (FontWeight)Typeface.FontWeight;
Style = Typeface.FontSlant.ToAvalonia();
Stretch = (FontStretch)Typeface.FontStyle.Width;
} }
public Face Face { get; } public Face Face { get; }
@ -67,6 +73,14 @@ namespace Avalonia.Skia
public int GlyphCount { get; } 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) public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{ {
metrics = default; 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 System.Collections.Generic;
using Avalonia.Platform;
using SharpDX; using SharpDX;
using SharpDX.DirectWrite; using SharpDX.DirectWrite;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
using System; using System.IO;
internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader
{ {
@ -18,19 +17,15 @@ namespace Avalonia.Direct2D1.Media
/// </summary> /// </summary>
/// <param name="factory">The factory.</param> /// <param name="factory">The factory.</param>
/// <param name="fontAssets"></param> /// <param name="fontAssets"></param>
public DWriteResourceFontLoader(Factory factory, IEnumerable<Uri> fontAssets) public DWriteResourceFontLoader(Factory factory, Stream[] fontAssets)
{ {
var factory1 = factory; var factory1 = factory;
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
foreach (var asset in fontAssets) foreach (var asset in fontAssets)
{ {
var assetStream = assetLoader.Open(asset); var dataStream = new DataStream((int)asset.Length, true, true);
var dataStream = new DataStream((int)assetStream.Length, true, true);
assetStream.CopyTo(dataStream); asset.CopyTo(dataStream);
dataStream.Position = 0; 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 FontStyle = SharpDX.DirectWrite.FontStyle;
using FontWeight = SharpDX.DirectWrite.FontWeight; using FontWeight = SharpDX.DirectWrite.FontWeight;
using FontStretch = SharpDX.DirectWrite.FontStretch; using FontStretch = SharpDX.DirectWrite.FontStretch;
using Avalonia.Platform;
using System.Linq;
using System;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
@ -53,9 +56,15 @@ namespace Avalonia.Direct2D1.Media
private static FontCollection CreateFontCollection(FontFamilyKey key) 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); 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.Globalization;
using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily; using FontFamily = Avalonia.Media.FontFamily;
using FontStretch = Avalonia.Media.FontStretch; using FontStretch = Avalonia.Media.FontStretch;
using FontStyle = Avalonia.Media.FontStyle; using FontStyle = Avalonia.Media.FontStyle;
@ -18,7 +18,7 @@ namespace Avalonia.Direct2D1.Media
return "Segoe UI"; return "Segoe UI";
} }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{ {
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@ -62,9 +62,56 @@ namespace Avalonia.Direct2D1.Media
return false; 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; 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>(); FontFace = new FontFace(DWFont).QueryInterface<FontFace1>();
@ -48,6 +48,14 @@ namespace Avalonia.Direct2D1.Media
StrikethroughThickness = strikethroughThickness, StrikethroughThickness = strikethroughThickness,
IsFixedPitch = FontFace.IsMonospacedFont 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) private Blob GetTable(Face face, Tag tag)
@ -83,6 +91,14 @@ namespace Avalonia.Direct2D1.Media
public FontSimulations FontSimulations => FontSimulations.None; 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"/> /// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint) 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 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() public void Should_Load_Single_FontAsset()
{ {
var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute); 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); Assert.Single(fontAssets);
} }
@ -57,9 +56,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Single_FontAsset_Avares_Without_BaseUri() public void Should_Load_Single_FontAsset_Avares_Without_BaseUri()
{ {
var source = new Uri(AssetYourFontAvares); var source = new Uri(AssetYourFontAvares);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key); var fontAssets = FontFamilyLoader.LoadFontAssets(source);
Assert.Single(fontAssets); Assert.Single(fontAssets);
} }
@ -69,9 +67,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
{ {
var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute); var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute);
var baseUri = new Uri(AssetLocationAvares); 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); Assert.Single(fontAssets);
} }
@ -80,9 +77,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Matching_Assets() public void Should_Load_Matching_Assets()
{ {
var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute); 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) foreach (var fontAsset in fontAssets)
{ {
@ -99,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
{ {
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); 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); 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.Media;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -16,18 +17,10 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{ {
Direct2D1Platform.Initialize(); Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl(); var glyphTypeface =
new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface;
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);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); Assert.Equal("Arial", glyphTypeface.FamilyName);
} }
} }
@ -38,42 +31,29 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{ {
Direct2D1Platform.Initialize(); Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl(); var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold));
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(FontStyle.Normal, glyphTypeface.Style);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
} }
} }
[Fact] [Fact]
public void Should_Create_Typeface_For_Unknown_Font() public void Should_Throw_InvalidOperationException_For_Unknown_Font()
{ {
using (AvaloniaLocator.EnterScope()) using (AvaloniaLocator.EnterScope())
{ {
Direct2D1Platform.Initialize(); Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl(); var fontManager = FontManager.Current;
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);
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 fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface;
new Typeface(s_fontUri));
var font = glyphTypeface.DWFont; Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
} }
} }
@ -102,14 +79,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{ {
Direct2D1Platform.Initialize(); Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl(); var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface;
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
var font = glyphTypeface.DWFont;
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.Globalization;
using System.Linq; using System.Linq;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts; using Avalonia.Media.Fonts;
using Avalonia.Platform; using Avalonia.Platform;
using SkiaSharp; using SkiaSharp;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace Avalonia.Skia.UnitTests.Media namespace Avalonia.Skia.UnitTests.Media
{ {
@ -35,9 +37,9 @@ namespace Avalonia.Skia.UnitTests.Media
return _defaultFamilyName; 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 }; private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
@ -70,48 +72,132 @@ namespace Avalonia.Skia.UnitTests.Media
{ {
SKTypeface skTypeface; SKTypeface skTypeface;
Uri source = null;
switch (typeface.FontFamily.Name) switch (typeface.FontFamily.Name)
{ {
case "Twitter Color Emoji": case "Twitter Color Emoji":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily); source = _emojiTypeface.FontFamily.Key.Source;
skTypeface = typefaceCollection.Get(typeface);
break; break;
} }
case "Noto Sans": case "Noto Sans":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); source = _italicTypeface.FontFamily.Key.Source;
skTypeface = typefaceCollection.Get(typeface);
break; break;
} }
case "Noto Sans Arabic": case "Noto Sans Arabic":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_arabicTypeface.FontFamily); source = _arabicTypeface.FontFamily.Key.Source;
skTypeface = typefaceCollection.Get(typeface);
break; break;
} }
case "Noto Sans Hebrew": case "Noto Sans Hebrew":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_hebrewTypeface.FontFamily); source = _hebrewTypeface.FontFamily.Key.Source;
skTypeface = typefaceCollection.Get(typeface);
break; break;
} }
case FontFamily.DefaultFontFamilyName: case FontFamily.DefaultFontFamilyName:
case "Noto Mono": case "Noto Mono":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); source = _defaultTypeface.FontFamily.Key.Source;
skTypeface = typefaceCollection.Get(_defaultTypeface);
break; break;
} }
default: default:
{ {
skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
(SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
break; 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); 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] [Fact]
public void Should_Create_Typeface_From_Fallback() public void Should_Create_Typeface_From_Fallback()
{ {
var fontManager = new FontManagerImpl(); using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( var fontManager = FontManager.Current;
new Typeface(new FontFamily("A, B, " + fontManager.GetDefaultFontFamilyName())));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
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] [Fact]
public void Should_Create_Typeface_From_Fallback_Bold() public void Should_Create_Typeface_From_Fallback_Bold()
{ {
var fontManager = new FontManagerImpl(); using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( var glyphTypeface = new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold));
var skTypeface = glyphTypeface.Typeface; Assert.True((int)glyphTypeface.Weight >= 600);
}
Assert.True(skTypeface.FontWeight >= 600);
} }
[Fact] [Fact]
public void Should_Create_Typeface_For_Unknown_Font() public void Should_Throw_InvalidOperationException_For_Invalid_FamilyName()
{ {
var fontManager = new FontManagerImpl(); using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( Assert.Throws<InvalidOperationException>(() =>
new Typeface(new FontFamily("Unknown"))); {
var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface;
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);
} }
[Fact] [Fact]
public void Should_Load_Typeface_From_Resource() 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 = new Typeface(s_fontUri).GlyphTypeface;
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri));
var skTypeface = glyphTypeface.Typeface; Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
Assert.Equal("Noto Mono", skTypeface.FamilyName);
} }
} }
[Fact] [Fact]
public void Should_Load_Nearest_Matching_Font() 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 = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface;
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal("Noto Mono", skTypeface.FamilyName); Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
} }
} }
[Fact] [Fact]
public void Should_Throw_For_Invalid_Custom_Font() 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>(() => new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown").GlyphTypeface);
Assert.Throws<InvalidOperationException>(() =>
fontManager.CreateGlyphTypeface(
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown")));
} }
} }
} }

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

9
tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs

@ -57,6 +57,15 @@ namespace Avalonia.UnitTests
public FontSimulations FontSimulations { get; } public FontSimulations FontSimulations { get; }
public string FamilyName => "$Default";
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
/// <inheritdoc cref="IGlyphTypeface"/> /// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint) 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.Globalization;
using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
@ -19,12 +20,12 @@ namespace Avalonia.UnitTests
return _defaultFamilyName; return _defaultFamilyName;
} }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{ {
return new[] { _defaultFamilyName }; 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, FontStretch fontStretch, FontFamily fontFamily,
CultureInfo culture, out Typeface fontKey) CultureInfo culture, out Typeface fontKey)
{ {
@ -33,9 +34,18 @@ namespace Avalonia.UnitTests
return false; 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 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) public ushort GetGlyph(uint codepoint)
{ {
return (ushort)codepoint; return (ushort)codepoint;

Loading…
Cancel
Save