diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index e55f003133..a86d311f4e 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/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() diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 6d624c9a07..6d759597b5 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Get("fontComboBox"); - fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); + fontComboBox.Items = FontManager.Current.SystemFonts; fontComboBox.SelectedIndex = 0; } } diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 2dabb29e76..27e25cf359 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/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 /// public sealed class FontManager { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - private readonly FontFamily _defaultFontFamily; + public const string FontCollectionScheme = "fonts"; + + private readonly SystemFontCollection _systemFonts; + private readonly ConcurrentDictionary _fontCollections = new ConcurrentDictionary(); private readonly IReadOnlyList? _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 } } - /// - /// - /// - public IFontManagerImpl PlatformImpl { get; } - /// /// Gets the system's default font family's name. /// @@ -71,41 +69,92 @@ namespace Avalonia.Media } /// - /// Get all installed font family names. + /// Get all system fonts. /// - /// If true the font collection is updated. - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); + public IFontCollection SystemFonts => _systemFonts; + + internal IFontManagerImpl PlatformImpl { get; } /// - /// Returns a new , or an existing one if a matching exists. + /// Tries to get a glyph typeface for specified typeface. /// /// The typeface. + /// The created glyphTypeface /// - /// The . + /// True, if the could create the glyph typeface, False otherwise. /// - 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 /// True, if the could match the character to specified parameters, False otherwise. /// 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; } } diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs new file mode 100644 index 0000000000..c242ed18db --- /dev/null +++ b/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> _glyphTypefaceCache = + new Dictionary>(); + + private readonly List _fontFamilies = new List(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(); + + 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(); + + _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 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 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 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 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; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs new file mode 100644 index 0000000000..0d0dc3016e --- /dev/null +++ b/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); +} diff --git a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs index 365fb6e412..39e80415fb 100644 --- a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs @@ -11,22 +11,22 @@ namespace Avalonia.Media.Fonts /// /// Loads all font assets that belong to the specified /// - /// + /// /// - public static IEnumerable LoadFontAssets(FontFamilyKey fontFamilyKey) => - IsFontTtfOrOtf(fontFamilyKey.Source) ? - GetFontAssetsByExpression(fontFamilyKey) : - GetFontAssetsBySource(fontFamilyKey); + public static IEnumerable LoadFontAssets(Uri source) => + IsFontTtfOrOtf(source) ? + GetFontAssetsByExpression(source) : + GetFontAssetsBySource(source); /// /// Searches for font assets at a given location and returns a quantity of found assets /// - /// + /// /// - private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey) + private static IEnumerable GetFontAssetsBySource(Uri source) { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - 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. /// File names can target multiple files with * wildcard. For example "FontFile*.ttf" /// - /// + /// /// - private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey) + private static IEnumerable 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(); - 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; } diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs new file mode 100644 index 0000000000..27b3378513 --- /dev/null +++ b/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, IDisposable + { + Uri Key { get; } + + void Initialize(IFontManagerImpl fontManager); + + bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + } +} diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs new file mode 100644 index 0000000000..ce0deb21e4 --- /dev/null +++ b/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> _glyphTypefaceCache = + new Dictionary>(); + + 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(); + + _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 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); + } + } +} diff --git a/src/Avalonia.Base/Media/IGlyphTypeface.cs b/src/Avalonia.Base/Media/IGlyphTypeface.cs index 9e1e52cb73..09740aac81 100644 --- a/src/Avalonia.Base/Media/IGlyphTypeface.cs +++ b/src/Avalonia.Base/Media/IGlyphTypeface.cs @@ -6,6 +6,26 @@ namespace Avalonia.Media [Unstable] public interface IGlyphTypeface : IDisposable { + /// + /// Gets the family name for the object. + /// + string FamilyName { get; } + + /// + /// Gets the designed weight of the font represented by the object. + /// + FontWeight Weight { get; } + + /// + /// Gets the style for the object. + /// + FontStyle Style { get; } + + /// + /// Gets the value for the object. + /// + FontStretch Stretch { get; } + /// /// Gets the number of glyphs held by this glyph typeface. /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index b4734d702b..253c7075fa 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/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 diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs index 1e744c30c8..e2729c9158 100644 --- a/src/Avalonia.Base/Media/Typeface.cs +++ b/src/Avalonia.Base/Media/Typeface.cs @@ -80,7 +80,18 @@ namespace Avalonia.Media /// /// The glyph typeface. /// - 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) { diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index cd6e64abaf..116f7cd6e2 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/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. /// If true the font collection is updated. /// - IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + string[] GetInstalledFontFamilyNames(bool checkForUpdates = false); /// /// 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); /// - /// Creates a glyph typeface. + /// Tries to get a glyph typeface for specified parameters. /// - /// The typeface. - /// 0 - /// The created glyph typeface. Can be Null if it was not possible to create a glyph typeface. + /// The family name. + /// The font style. + /// The font weiht. + /// The font stretch. + /// The created glyphTypeface + /// + /// True, if the could create the glyph typeface, False otherwise. + /// + bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); + + /// + /// Tries to create a glyph typeface from specified stream. + /// + /// A stream that holds the font's data. + /// The created glyphTypeface + /// + /// True, if the could create the glyph typeface, False otherwise. /// - IGlyphTypeface CreateGlyphTypeface(Typeface typeface); + bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); } } diff --git a/src/Avalonia.Base/Utilities/UriExtensions.cs b/src/Avalonia.Base/Utilities/UriExtensions.cs index c706f72a63..1f9c694eab 100644 --- a/src/Avalonia.Base/Utilities/UriExtensions.cs +++ b/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) diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index cf79fcd1a8..9e1222de6d 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/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? _appFactory; private IApplicationLifetime? _lifetime; - + /// /// Gets or sets the instance. /// @@ -31,12 +34,12 @@ namespace Avalonia /// Gets the instance being initialized. /// public Application? Instance { get; private set; } - + /// /// Gets the type of the Instance (even if it's not created yet) /// public Type? ApplicationType { get; private set; } - + /// /// Gets or sets a method to call the initialize the windowing subsystem. /// @@ -64,7 +67,7 @@ namespace Avalonia public Action AfterPlatformServicesSetupCallback { get; private set; } = builder => { }; - + /// /// Initializes a new instance of the class. /// @@ -73,7 +76,7 @@ namespace Avalonia builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly)) { } - + /// /// Initializes a new instance of the class. /// @@ -123,8 +126,8 @@ namespace Avalonia AfterSetupCallback = (Action)Delegate.Combine(AfterSetupCallback, callback); return Self; } - - + + public AppBuilder AfterPlatformServicesSetup(Action callback) { AfterPlatformServicesSetupCallback = (Action)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; } - + /// /// Specifies a windowing subsystem to use. /// @@ -195,7 +198,7 @@ namespace Avalonia _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); }; return Self; } - + /// /// Configures platform-specific options /// @@ -204,7 +207,28 @@ namespace Avalonia _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToFunc(options); }; return Self; } - + + /// + /// Registers a custom font collection with the font manager. + /// + /// The font collection. + /// An instance. + /// + 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); + }); + } + /// /// Sets up the platform-specific services for the . /// diff --git a/src/Avalonia.Fonts.Inter/InterFontCollection.cs b/src/Avalonia.Fonts.Inter/InterFontCollection.cs new file mode 100644 index 0000000000..0ed1779a03 --- /dev/null +++ b/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)) + { + } + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 46e3515d11..ee4cd5af98 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/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 GetInstalledFontFamilyNames(bool checkForUpdates = false) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface) { - return new List { "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, diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 82e48851b5..c19a4f5c09 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -3,7 +3,7 @@ xmlns:sys="using:System" xmlns:converters="using:Avalonia.Controls.Converters"> - avares://Avalonia.Fonts.Inter/Assets#Inter, $Default + fonts:Inter#Inter, $Default 14 diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 90a2f9169b..29e5687423 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/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 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; } } } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index 3093455bec..43e10e3e96 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/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; diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs deleted file mode 100644 index 9ee17a09d6..0000000000 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ /dev/null @@ -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 _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; - } - } -} diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs deleted file mode 100644 index d064f49ae4..0000000000 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ /dev/null @@ -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 s_cachedCollections; - - static SKTypefaceCollectionCache() - { - s_cachedCollections = new ConcurrentDictionary(); - } - - /// - /// Gets the or add typeface collection. - /// - /// The font family. - /// - public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily) - { - return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection); - } - - /// - /// Creates the custom font collection. - /// - /// The font family. - /// - 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(); - - 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; - } - } -} diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs index 4663a6561f..b60962a091 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs +++ b/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 /// /// The factory. /// - public DWriteResourceFontLoader(Factory factory, IEnumerable fontAssets) + public DWriteResourceFontLoader(Factory factory, Stream[] fontAssets) { var factory1 = factory; - var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - 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; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index 792bf2d0be..ad2ede3a91 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/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(); + + var fontAssets = assets.Select(x => assetLoader.Open(x)).ToArray(); + + var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, fontAssets); return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index b98ed3ffe6..ec2f6385da 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/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 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; } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs index e4988322e7..01add0f0cb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs +++ b/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(); @@ -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; } + /// public ushort GetGlyph(uint codepoint) { diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs index 11ecac0039..89e609eb10 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs +++ b/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); } } diff --git a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs b/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs index afc25ab88e..82dcd8f4fc 100644 --- a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs +++ b/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(); - 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); diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index c6ecc0a7e5..c50f31a834 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/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(() => + { + 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); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 5a6d7f2cdf..e18344580b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/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 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(); + + 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(); + + 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; + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs new file mode 100644 index 0000000000..8e7ed8b1d2 --- /dev/null +++ b/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); + } + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs index 649e1fbf3d..a9a0bd8faf 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs +++ b/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(() => + { + 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(() => - fontManager.CreateGlyphTypeface( - new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown"))); + Assert.Throws(() => new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown").GlyphTypeface); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs deleted file mode 100644 index 64050bd85e..0000000000 --- a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs +++ /dev/null @@ -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); - } - } - } -} diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs index 55ac16054d..a819cbd5e3 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs +++ b/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 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(); + 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; } } } diff --git a/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs index 5b11345f16..db517ba176 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs +++ b/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; } + + /// public ushort GetGlyph(uint codepoint) { diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs index e9b923a367..eda4544877 100644 --- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs +++ b/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 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; } } } diff --git a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs index bd9d8e5adf..5fcee7f515 100644 --- a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs +++ b/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;