From 3e62621aaf167a3e5ac47b0f1d8d1e28a67ada7f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 24 Nov 2025 17:23:32 +0100 Subject: [PATCH] Improve FontCollection customization (#19756) * Improve FontCollection user story * Make adjustments after review * Refactor IsFontFile * Make FontFamilyLoader internal Make tests happy again * Update baseline * Adjust modifier --------- Co-authored-by: Julien Lebosquain --- api/Avalonia.Win32.Interoperability.nupkg.xml | 40 ++ api/Avalonia.nupkg.xml | 264 ++++++++ src/Avalonia.Base/Media/FontManager.cs | 49 +- .../Media/Fonts/EmbeddedFontCollection.cs | 156 +---- .../Media/Fonts/EmptySystemFontCollection.cs | 9 + .../Media/Fonts/FontCollectionBase.cs | 639 +++++++++++++++--- .../Media/Fonts/FontFamilyLoader.cs | 42 +- .../Media/Fonts/IFontCollection.cs | 25 +- .../Media/Fonts/SystemFontCollection.cs | 174 ++--- src/Avalonia.Base/Media/Typeface.cs | 78 +++ .../Platform/IFontManagerImpl.cs | 7 +- .../SystemFontAppBuilderExtension.cs | 2 +- .../HeadlessPlatformStubs.cs | 6 +- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 11 +- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 38 +- .../Media/TypefaceTests.cs | 19 + .../ItemsControlTests.cs | 4 +- .../MaskedTextBoxTests.cs | 2 +- .../SelectingItemsControlTests_Multiple.cs | 4 +- .../TextBoxTests.cs | 5 +- .../TransitioningContentControlTests.cs | 4 +- .../TreeViewTests.cs | 4 +- .../Avalonia.Skia.UnitTests.csproj | 5 + .../Media/CustomFontCollectionTests.cs | 188 ++++++ .../Media/CustomFontManagerImpl.cs | 81 ++- .../Media/EmbeddedFontCollectionTests.cs | 61 +- .../Media/FontCollectionTests.cs | 30 +- .../TextFormatting/TextFormatterTests.cs | 12 +- .../HarfBuzzFontManagerImpl.cs | 2 +- 29 files changed, 1404 insertions(+), 557 deletions(-) create mode 100644 api/Avalonia.Win32.Interoperability.nupkg.xml create mode 100644 src/Avalonia.Base/Media/Fonts/EmptySystemFontCollection.cs create mode 100644 tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs diff --git a/api/Avalonia.Win32.Interoperability.nupkg.xml b/api/Avalonia.Win32.Interoperability.nupkg.xml new file mode 100644 index 0000000000..3672bb9b99 --- /dev/null +++ b/api/Avalonia.Win32.Interoperability.nupkg.xml @@ -0,0 +1,40 @@ + + + + + CP0002 + M:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost.PreFilterMessage(System.Windows.Forms.Message@) + baseline/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll + current/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll + + + CP0002 + M:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost.PreFilterMessage(System.Windows.Forms.Message@) + baseline/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll + current/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll + + + CP0002 + M:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost.PreFilterMessage(System.Windows.Forms.Message@) + baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll + current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll + + + CP0008 + T:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost + baseline/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll + current/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll + + + CP0008 + T:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost + baseline/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll + current/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll + + + CP0008 + T:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost + baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll + current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll + + diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index f5864e85cf..af46d2fb2e 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1,24 +1,150 @@ + + CP0001 + T:Avalonia.Media.Fonts.FontFamilyLoader + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Media.Fonts.FontFamilyLoader + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Media.Fonts.FontFamilyLoader + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Media.Fonts.FontFamilyLoader + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + CP0002 M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType) baseline/Avalonia/lib/net6.0/Avalonia.Dialogs.dll current/Avalonia/lib/net6.0/Avalonia.Dialogs.dll + + CP0002 + F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType) baseline/Avalonia/lib/net8.0/Avalonia.Dialogs.dll current/Avalonia/lib/net8.0/Avalonia.Dialogs.dll + + CP0002 + F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + CP0002 M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType) baseline/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll current/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer) @@ -43,6 +169,12 @@ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll current/Avalonia/lib/net6.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64) @@ -97,6 +229,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64) @@ -151,6 +289,12 @@ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64) @@ -181,4 +325,124 @@ baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.get_Count + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0012 + P:Avalonia.Media.Fonts.FontCollectionBase.Count + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0012 + P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.get_Count + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0012 + P:Avalonia.Media.Fonts.FontCollectionBase.Count + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0012 + P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.get_Count + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0012 + P:Avalonia.Media.Fonts.FontCollectionBase.Count + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0012 + P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.get_Count + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0012 + M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0012 + P:Avalonia.Media.Fonts.FontCollectionBase.Count + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0012 + P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 5a49511a5a..595a926f0c 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -31,8 +31,6 @@ namespace Avalonia.Media { PlatformImpl = platformImpl; - AddFontCollection(new SystemFontCollection(this)); - var options = AvaloniaLocator.Current.GetService(); _fontFallbacks = options?.FontFallbacks; _fontFamilyMappings = options?.FontFamilyMappings; @@ -76,7 +74,19 @@ namespace Avalonia.Media /// /// Get all system fonts. /// - public IFontCollection SystemFonts => _fontCollections[SystemFontsKey]; + public IFontCollection SystemFonts + { + get + { + if (TryGetFontCollection(SystemFontsKey, out var fontCollection)) + { + return fontCollection; + } + + // Fallback to an empty system font collection + return new EmptySystemFontCollection(); + } + } internal IFontManagerImpl PlatformImpl { get; } @@ -93,12 +103,13 @@ namespace Avalonia.Media glyphTypeface = null; var fontFamily = GetMappedFontFamily(typeface.FontFamily); - + if (typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName) { return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); } + if (fontFamily.Key != null) { if (fontFamily.Key is CompositeFontFamilyKey compositeKey) @@ -167,7 +178,7 @@ namespace Avalonia.Media FontFamily GetMappedFontFamily(FontFamily fontFamily) { - if (_fontFamilyMappings == null ||!_fontFamilyMappings.TryGetValue(fontFamily.FamilyNames.PrimaryFamilyName, out var mappedFontFamily)) + if (_fontFamilyMappings == null || !_fontFamilyMappings.TryGetValue(fontFamily.FamilyNames.PrimaryFamilyName, out var mappedFontFamily)) { return fontFamily; } @@ -222,8 +233,6 @@ namespace Avalonia.Media return fontCollection; }); - - fontCollection.Initialize(PlatformImpl); } /// @@ -288,7 +297,7 @@ namespace Avalonia.Media if (TryGetFontCollection(source, out var fontCollection) && // With composite fonts we need to first check if the font collection contains the family if not we skip it - fontCollection.TryGetGlyphTypeface(familyName, fontStyle, fontWeight, fontStretch, out _) && + fontCollection.TryGetGlyphTypeface(familyName, fontStyle, fontWeight, fontStretch, out _) && fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) { return true; @@ -319,7 +328,7 @@ namespace Avalonia.Media if (key == null) { - if(SystemFonts is IFontCollection2 fontCollection2) + if (SystemFonts is IFontCollection2 fontCollection2) { if (fontCollection2.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces)) { @@ -352,15 +361,23 @@ namespace Avalonia.Media source = SystemFontsKey; } - if (!_fontCollections.TryGetValue(source, out fontCollection) && (source.IsAbsoluteResm() || source.IsAvares())) + if (!_fontCollections.TryGetValue(source, out fontCollection)) { - var embeddedFonts = new EmbeddedFontCollection(source, source); - - embeddedFonts.Initialize(PlatformImpl); - - if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts)) + if (source == SystemFontsKey) + { + fontCollection = new SystemFontCollection(PlatformImpl); + } + else + { + if (source.IsAbsoluteResm() || source.IsAvares()) + { + fontCollection = new EmbeddedFontCollection(source, source); + } + } + + if (fontCollection != null) { - fontCollection = embeddedFonts; + return _fontCollections.TryAdd(fontCollection.Key, fontCollection); } } diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index 9efee47a5c..02701513b9 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -1,18 +1,10 @@ using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using Avalonia.Platform; namespace Avalonia.Media.Fonts { - public class EmbeddedFontCollection : FontCollectionBase, IFontCollection2 + public class EmbeddedFontCollection : FontCollectionBase { - private readonly List _fontFamilies = new List(1); - private readonly Uri _key; - private readonly Uri _source; public EmbeddedFontCollection(Uri key, Uri source) @@ -20,152 +12,10 @@ namespace Avalonia.Media.Fonts _key = key; _source = source; - } - - public override Uri Key => _key; - - public override FontFamily this[int index] => _fontFamilies[index]; - - public override int Count => _fontFamilies.Count; - - public override 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, FontSimulations.None, out var glyphTypeface)) - { - AddGlyphTypeface(glyphTypeface); - } - } + TryAddFontSource(_source); } - public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) - { - var typeface = GetImplicitTypeface(new Typeface(familyName, style, weight, stretch), out familyName); - - style = typeface.Style; - - weight = typeface.Weight; - - stretch = typeface.Stretch; - - var key = new FontCollectionKey(style, weight, stretch); - - if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) - { - if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) - { - return true; - } - - if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) - { - var matchedKey = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); - - if(matchedKey != key) - { - //Create a synthetic glyph typeface. The successfull result will be cached. - if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface)) - { - glyphTypeface = syntheticGlyphTypeface; - } - else - { - //Add the matched glyph typeface to the cache - glyphTypefaces.TryAdd(key, 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; - } - - public override IEnumerator GetEnumerator() => _fontFamilies.GetEnumerator(); - - private void AddGlyphTypeface(IGlyphTypeface glyphTypeface) - { - if (glyphTypeface is IGlyphTypeface2 glyphTypeface2) - { - //Add the TypographicFamilyName to the cache - if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) - { - AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, glyphTypeface); - } - - foreach (var kvp in glyphTypeface2.FamilyNames) - { - AddGlyphTypefaceByFamilyName(kvp.Value, glyphTypeface); - } - } - else - { - AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface); - } - - return; - - void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface) - { - var typefaces = _glyphTypefaceCache.GetOrAdd(familyName, - x => - { - _fontFamilies.Add(new FontFamily(_key, familyName)); - - return new ConcurrentDictionary(); - }); - - typefaces.TryAdd( - new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch), - glyphTypeface); - } - } - - public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) - { - familyTypefaces = null; - - if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) - { - var typefaces = new List(glyphTypefaces.Count); - - foreach (var key in glyphTypefaces.Keys) - { - typefaces.Add(new Typeface(new FontFamily(_key, familyName), key.Style, key.Weight, key.Stretch)); - } - - familyTypefaces = typefaces; - - return true; - } - - return false; - } + public override Uri Key => _key; } } diff --git a/src/Avalonia.Base/Media/Fonts/EmptySystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmptySystemFontCollection.cs new file mode 100644 index 0000000000..cf9717c81a --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/EmptySystemFontCollection.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Media.Fonts +{ + internal class EmptySystemFontCollection : FontCollectionBase + { + public override Uri Key => FontManager.SystemFontsKey; + } +} diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index 222a514ed1..8f2b43ff8d 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -4,30 +4,41 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Text; +using System.IO; using Avalonia.Platform; -using Avalonia.Utilities; namespace Avalonia.Media.Fonts { - public abstract class FontCollectionBase : IFontCollection + public abstract class FontCollectionBase : IFontCollection2 { - protected readonly ConcurrentDictionary> _glyphTypefaceCache = new(); + private static readonly Comparer FontFamilyNameComparer = + Comparer.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); - public abstract Uri Key { get; } + // Make this internal for testing purposes + internal readonly ConcurrentDictionary> _glyphTypefaceCache = new(); + + private readonly object _fontFamiliesLock = new(); + private volatile FontFamily[] _fontFamilies = Array.Empty(); + private readonly IFontManagerImpl _fontManagerImpl; + private readonly IAssetLoader _assetLoader; + + protected FontCollectionBase() + { + _fontManagerImpl = AvaloniaLocator.Current.GetRequiredService(); + _assetLoader = AvaloniaLocator.Current.GetRequiredService(); + } - public abstract int Count { get; } + public abstract Uri Key { get; } - public abstract FontFamily this[int index] { get; } + public int Count => _fontFamilies.Length; - public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, - [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + public FontFamily this[int index] => _fontFamilies[index]; public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName, CultureInfo? culture, out Typeface match) { match = default; - + //If a font family is defined we try to find a match inside that family first if (familyName != null && _glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) { @@ -45,7 +56,7 @@ namespace Avalonia.Media.Fonts //Try to find a match in any font family foreach (var pair in _glyphTypefaceCache) { - if(pair.Key == familyName) + if (pair.Key == familyName) { //We already tried this before continue; @@ -57,7 +68,11 @@ namespace Avalonia.Media.Fonts { if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) { - match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), style, weight, stretch); + // Found a match + match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), + glyphTypeface.Style, + glyphTypeface.Weight, + glyphTypeface.Stretch); return true; } @@ -69,9 +84,9 @@ namespace Avalonia.Media.Fonts public virtual bool TryCreateSyntheticGlyphTypeface( IGlyphTypeface glyphTypeface, - FontStyle style, - FontWeight weight, - FontStretch stretch, + FontStyle style, + FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface) { syntheticGlyphTypeface = null; @@ -82,8 +97,6 @@ namespace Avalonia.Media.Fonts return false; } - var fontManager = FontManager.Current.PlatformImpl; - var key = new FontCollectionKey(style, weight, stretch); var currentKey = @@ -115,17 +128,17 @@ namespace Avalonia.Media.Fonts { using (stream) { - if (fontManager.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface)) + if (_fontManagerImpl.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface)) { //Add the TypographicFamilyName to the cache if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) { - AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, syntheticGlyphTypeface); + TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, syntheticGlyphTypeface); } foreach (var kvp in glyphTypeface2.FamilyNames) { - AddGlyphTypefaceByFamilyName(kvp.Value, syntheticGlyphTypeface); + TryAddGlyphTypeface(kvp.Value, key, syntheticGlyphTypeface); } return true; @@ -136,46 +149,420 @@ namespace Avalonia.Media.Fonts } return false; + } + + public IEnumerator GetEnumerator() => ((IEnumerable)_fontFamilies).GetEnumerator(); + + public virtual bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName); + + style = typeface.Style; + + weight = typeface.Weight; - void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface) + stretch = typeface.Stretch; + + var key = new FontCollectionKey(style, weight, stretch); + + return TryGetGlyphTypeface(familyName, key, out glyphTypeface); + } + + public virtual bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + { + familyTypefaces = null; + + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) { - var typefaces = _glyphTypefaceCache.GetOrAdd(familyName, - x => - { - return new ConcurrentDictionary(); - }); + // Take a snapshot of the entries to avoid issues with concurrent modifications + var entries = glyphTypefaces.ToArray(); - typefaces.TryAdd(key, glyphTypeface); + var typefaces = new Typeface[entries.Length]; + + for (var i = 0; i < entries.Length; i++) + { + var key = entries[i].Key; + + typefaces[i] = new Typeface(new FontFamily(Key + "#" + familyName), key.Style, key.Weight, key.Stretch); + } + + familyTypefaces = typefaces; + + return true; } + + return false; } - public abstract void Initialize(IFontManagerImpl fontManager); + public bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + { + glyphTypeface = null; + + return false; + } - public abstract IEnumerator GetEnumerator(); + var key = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }; - void IDisposable.Dispose() + return TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface); + } + + /// + /// Attempts to add the specified to the font collection. + /// + /// This method checks the and, if applicable, + /// the typographic family name and other family names provided by the interface. + /// If any of these names can be associated with the glyph typeface, the typeface is added to the collection. + /// The method ensures that duplicate entries are not added. + /// The glyph typeface to add. Must not be and must have a non-empty . + /// if the glyph typeface was successfully added to the collection; otherwise, . + public bool TryAddGlyphTypeface(IGlyphTypeface glyphTypeface) { - foreach (var glyphTypefaces in _glyphTypefaceCache.Values) + if (glyphTypeface == null || string.IsNullOrEmpty(glyphTypeface.FamilyName)) { - foreach (var pair in glyphTypefaces) + return false; + } + + var key = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); + + if (glyphTypeface is IGlyphTypeface2 glyphTypeface2) + { + var result = false; + + //Add the TypographicFamilyName to the cache + if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) { - pair.Value?.Dispose(); + if (TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, glyphTypeface)) + { + result = true; + } + } + + foreach (var kvp in glyphTypeface2.FamilyNames) + { + if (TryAddGlyphTypeface(kvp.Value, key, glyphTypeface)) + { + result = true; + } } + + return result; } + else + { + return TryAddGlyphTypeface(glyphTypeface.FamilyName, key, glyphTypeface); + } + } - GC.SuppressFinalize(this); + /// + /// Attempts to add a glyph typeface from the specified font stream. + /// + /// The method first attempts to create a glyph typeface from the provided font stream. + /// If successful, it adds the created glyph typeface to the collection. + /// The font stream containing the font data. The stream must be readable and positioned at the beginning of the + /// font data. + /// When this method returns, contains the created instance if the operation + /// succeeds; otherwise, . + /// if the glyph typeface was successfully created and added; otherwise, . + public bool TryAddGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out glyphTypeface)) + { + return false; + } + + return TryAddGlyphTypeface(glyphTypeface); } - IEnumerator IEnumerable.GetEnumerator() + /// + /// Attempts to add a font source to the font collection. + /// + /// This method processes the specified font source and attempts to load all available + /// fonts from it. Fonts are added to the collection based on their family name and typographic family name (if + /// available). If the is , the method returns . + /// The URI of the font source to add. This can be a file path, a resource URI, or another valid font source + /// URI. + /// if at least one font from the specified source was successfully added to the font + /// collection; otherwise, . + public bool TryAddFontSource(Uri source) { - return GetEnumerator(); + if (source is null) + { + return false; + } + + var result = false; + + switch (source.Scheme) + { + case "avares": + case "resm": + { + var fontAssets = FontFamilyLoader.LoadFontAssets(source); + + foreach (var fontAsset in fontAssets) + { + var stream = _assetLoader.Open(fontAsset); + + if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) + { + continue; + } + + var key = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); + + //Add TypographicFamilyName to the cache + if (glyphTypeface is IGlyphTypeface2 glyphTypeface2 && !string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) + { + if (TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, glyphTypeface)) + { + result = true; + } + } + + if (TryAddGlyphTypeface(glyphTypeface.FamilyName, key, glyphTypeface)) + { + result = true; + } + } + + break; + } + case "file": + { + // If the path is a file, load the font file directly + if (FontFamilyLoader.IsFontSource(source)) + { + if (!File.Exists(source.LocalPath)) + { + return false; + } + + using var stream = File.OpenRead(source.LocalPath); + + if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) + { + if (TryAddGlyphTypeface(glyphTypeface)) + { + result = true; + } + } + } + // If the path is a directory, load all font files from that directory + else + { + if (!Directory.Exists(source.LocalPath)) + { + return false; + } + + foreach (var file in Directory.EnumerateFiles(source.LocalPath)) + { + if (FontFamilyLoader.IsFontFile(file)) + { + using var stream = File.OpenRead(file); + + if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) + { + if (TryAddGlyphTypeface(glyphTypeface)) + { + result = true; + } + } + } + } + } + + break; + } + default: + //Unsupported scheme + return false; + } + + return result; } - internal static bool TryGetNearestMatch( - ConcurrentDictionary glyphTypefaces, - FontCollectionKey key, - [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + /// + /// Inserts the specified font family into the internal collection, maintaining the collection in sorted order + /// by font family name. + /// + /// If a font family with the same name already exists in the collection, the new + /// instance will be inserted alongside it. The collection remains sorted after insertion. + /// The font family to add to the collection. Cannot be null. + protected void AddFontFamily(FontFamily fontFamily) + { + if (fontFamily == null) + { + throw new ArgumentNullException(nameof(fontFamily)); + } + + lock (_fontFamiliesLock) + { + var current = _fontFamilies; + int index = Array.BinarySearch(current, fontFamily, FontFamilyNameComparer); + + // If an existing family with the same name is present, do nothing + if (index >= 0) + { + // BinarySearch found an equal entry, so avoid + // allocating a new array and inserting a duplicate. + return; + } + + index = ~index; + + var copy = new FontFamily[current.Length + 1]; + + if (index > 0) + { + Array.Copy(current, 0, copy, 0, index); + } + + copy[index] = fontFamily; + + if (index < current.Length) + { + Array.Copy(current, index, copy, index + 1, current.Length - index); + } + + // Publish new array for readers + _fontFamilies = copy; + } + } + + /// + /// Attempts to retrieve a glyph typeface that matches the specified font family name and font collection key. + /// + /// This method performs a binary search to locate font families with names that match + /// the specified . If multiple matches are found, the method iterates over them to + /// find the best match based on the provided . + /// The name of the font family to search for. This parameter is case-insensitive. + /// The key representing the desired font collection attributes. + /// When this method returns, contains the matching if a match is found; otherwise, + /// . + /// if a matching glyph typeface is found; otherwise, . + protected bool TryGetGlyphTypeface(string familyName, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + glyphTypeface = null; + + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + + if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + { + var matchedKey = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); + + if (matchedKey != key) + { + if (TryCreateSyntheticGlyphTypeface(glyphTypeface, key.Style, key.Weight, key.Stretch, out var syntheticGlyphTypeface)) + { + glyphTypeface = syntheticGlyphTypeface; + } + else + { + // Cache the nearest match for future lookups + TryAddGlyphTypeface(familyName, key, glyphTypeface); + } + } + + return true; + } + } + + // Binary search for the first possible prefix match using the snapshot array + var snapshot = _fontFamilies; + int left = 0; + int right = snapshot.Length - 1; + int firstMatch = -1; + + while (left <= right) + { + int mid = (left + right) / 2; + + var compare = string.Compare(snapshot[mid].Name, familyName, StringComparison.OrdinalIgnoreCase); + + // If the current name is lexicographically less than the search name, move right + if (compare < 0) + { + left = mid + 1; + } + else if (compare == 0) + { + // Exact match found in snapshot. Use the exact family name for lookup + if (_glyphTypefaceCache.TryGetValue(snapshot[mid].Name, out var exactGlyphTypefaces) && + TryGetNearestMatch(exactGlyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + // Exact family present but no matching typeface found. + return false; + } + else + { + // Only check for prefix when snapshot[mid].Name is > familyName. This + // avoids the more expensive StartsWith call for names that are definitely + // ordered before the search term. + if (snapshot[mid].Name.StartsWith(familyName, StringComparison.OrdinalIgnoreCase)) + { + firstMatch = mid; + right = mid - 1; // Continue searching to the left for the first match + } + else + { + right = mid - 1; + } + } + } + + if (firstMatch != -1) + { + // Iterate over all consecutive prefix matches + for (int i = firstMatch; i < snapshot.Length; i++) + { + var fontFamily = snapshot[i]; + + if (!fontFamily.Name.StartsWith(familyName, StringComparison.OrdinalIgnoreCase)) + { + break; + } + + if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) && + TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + } + } + + return false; + } + + /// + /// Attempts to retrieve the nearest matching for the specified font key from the + /// provided collection of glyph typefaces. + /// + /// This method attempts to find the best match for the specified font key by considering + /// various fallback strategies, such as normalizing the font style, stretch, and weight. If no suitable match is found, the method will return the first available non-null from the + /// collection, if any. + /// A collection of glyph typefaces, indexed by . + /// The representing the desired font attributes. + /// When this method returns, contains the that most closely matches the specified + /// key, if a match is found; otherwise, . + /// if a matching is found; otherwise, . + protected bool TryGetNearestMatch(IDictionary glyphTypefaces, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) { @@ -218,7 +605,7 @@ namespace Avalonia.Media.Fonts //Take the first glyph typeface we can find. foreach (var typeface in glyphTypefaces.Values) { - if(typeface != null) + if (typeface != null) { glyphTypeface = typeface; @@ -229,11 +616,86 @@ namespace Avalonia.Media.Fonts return false; } - internal static bool TryFindStretchFallback( - ConcurrentDictionary glyphTypefaces, - FontCollectionKey key, - [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + /// + /// Attempts to add a glyph typeface to the cache for the specified font family and key. + /// + /// If the specified font family does not exist in the cache, it is added along with the + /// glyph typeface. The method ensures that the font family is inserted in a sorted order within the internal + /// collection. + /// The name of the font family to which the glyph typeface belongs. Cannot be null or empty. + /// The key associated with the glyph typeface in the cache. + /// The glyph typeface to add to the cache. Can be null. + /// if the glyph typeface was successfully added to the cache; otherwise, . + protected bool TryAddGlyphTypeface(string familyName, FontCollectionKey key, IGlyphTypeface? glyphTypeface) + { + if (string.IsNullOrEmpty(familyName)) + { + return false; + } + + // Check if the family already exists + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + { + if (glyphTypefaces.TryGetValue(key, out var existing)) + { + if (ReferenceEquals(existing, glyphTypeface) || (existing is null && glyphTypeface is null)) + { + return true; + } + + return false; + } + + return glyphTypefaces.TryAdd(key, glyphTypeface); + } + + // Family doesn't exist yet. Create a new dictionary instance and try to install it. + var newDict = new ConcurrentDictionary(); + + // GetOrAdd will return the instance that ended up in the dictionary. If it's our + // newDict instance then we won the race to add the family and should publish it. + var dict = _glyphTypefaceCache.GetOrAdd(familyName, newDict); + + if (ReferenceEquals(dict, newDict)) + { + // We successfully installed the dictionary; publish the FontFamily once. + var fontFamily = new FontFamily(Key + "#" + familyName); + + // Add the font family to the sorted array + AddFontFamily(fontFamily); + } + + // Add or compare the glyphTypeface in the resulting dictionary. + if (dict.TryGetValue(key, out var existingAfter)) + { + if (ReferenceEquals(existingAfter, glyphTypeface) || (existingAfter is null && glyphTypeface is null)) + { + return true; + } + + return false; + } + + return dict.TryAdd(key, glyphTypeface); + } + + /// + /// Attempts to locate a fallback glyph typeface with a similar font stretch to the specified key within the + /// provided collection. + /// + /// The search prioritizes font stretches closest to the requested value, expanding + /// outward until a match is found or all options are exhausted. + /// A dictionary mapping font collection keys to their corresponding glyph typefaces. Used as the source for + /// searching fallback typefaces. + /// The font collection key specifying the desired font stretch and other font attributes to match. + /// When this method returns, contains the found glyph typeface with a similar stretch if one exists; otherwise, + /// null. + /// true if a suitable fallback glyph typeface is found; otherwise, false. + private static bool TryFindStretchFallback( + IDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { glyphTypeface = null; @@ -263,9 +725,22 @@ namespace Avalonia.Media.Fonts return false; } - internal static bool TryFindWeightFallback( - ConcurrentDictionary glyphTypefaces, + /// + /// Attempts to locate a fallback glyph typeface in the specified collection that closely matches the weight of + /// the provided key. + /// + /// The method searches for the closest available weight to the requested value, + /// considering both lighter and heavier alternatives within the collection. If no exact match is found, it + /// progressively searches for the nearest available weight in both directions. + /// A dictionary mapping font collection keys to glyph typeface instances. The method searches this collection + /// for a suitable fallback. + /// The font collection key specifying the desired font attributes, including weight, for which a fallback glyph + /// typeface is sought. + /// When this method returns, contains the matching glyph typeface if a suitable fallback is found; otherwise, + /// null. + /// true if a fallback glyph typeface matching the requested weight is found; otherwise, false. + private static bool TryFindWeightFallback( + IDictionary glyphTypefaces, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { @@ -348,68 +823,22 @@ namespace Avalonia.Media.Fonts return false; } - internal static Typeface GetImplicitTypeface(Typeface typeface, out string normalizedFamilyName) + void IDisposable.Dispose() { - normalizedFamilyName = typeface.FontFamily.FamilyNames.PrimaryFamilyName; - - //Return early if no separator is present. - if (!normalizedFamilyName.Contains(' ')) - { - return typeface; - } - - var style = typeface.Style; - var weight = typeface.Weight; - var stretch = typeface.Stretch; - - StringBuilder? normalizedFamilyNameBuilder = null; - var totalCharsRemoved = 0; - - var tokenizer = new SpanStringTokenizer(normalizedFamilyName, ' '); - - // Skip initial family name. - tokenizer.ReadSpan(); - - while (tokenizer.TryReadSpan(out var token)) + foreach (var glyphTypefaces in _glyphTypefaceCache.Values) { - // Don't try to match numbers. - if (new SpanStringTokenizer(token).TryReadInt32(out _)) - { - continue; - } - - // Try match with font style, weight or stretch and update accordingly. - var match = false; - if (EnumHelper.TryParse(token, true, out var newStyle)) - { - style = newStyle; - match = true; - } - else if (EnumHelper.TryParse(token, true, out var newWeight)) - { - weight = newWeight; - match = true; - } - else if (EnumHelper.TryParse(token, true, out var newStretch)) - { - stretch = newStretch; - match = true; - } - - if (match) + foreach (var pair in glyphTypefaces) { - // Carve out matched word from the normalized name. - normalizedFamilyNameBuilder ??= new StringBuilder(normalizedFamilyName); - normalizedFamilyNameBuilder.Remove(tokenizer.CurrentTokenIndex - totalCharsRemoved, token.Length); - totalCharsRemoved += token.Length; + pair.Value?.Dispose(); } } - // Get rid of any trailing spaces. - normalizedFamilyName = (normalizedFamilyNameBuilder?.ToString() ?? normalizedFamilyName).TrimEnd(); + GC.SuppressFinalize(this); + } - //Preserve old font source - return new Typeface(typeface.FontFamily, style, weight, stretch); + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } } } diff --git a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs index 2c61ffd419..99b5defa5a 100644 --- a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs @@ -6,7 +6,7 @@ using Avalonia.Utilities; namespace Avalonia.Media.Fonts { - public static class FontFamilyLoader + internal static class FontFamilyLoader { /// /// Loads all font assets that belong to the specified @@ -17,7 +17,7 @@ namespace Avalonia.Media.Fonts { if (source.IsAvares() || source.IsAbsoluteResm()) { - return IsFontTtfOrOtf(source) ? + return IsFontSource(source) ? GetFontAssetsByExpression(source) : GetFontAssetsBySource(source); } @@ -25,6 +25,35 @@ namespace Avalonia.Media.Fonts return Enumerable.Empty(); } + /// + /// Determines whether the specified URI refers to a font file source. + /// + /// The URI to evaluate as a potential font file source. Must not be null. + /// true if the URI points to a recognized font file source; otherwise, false. + public static bool IsFontSource(Uri uri) + { + var sourceWithoutArguments = GetSubString(uri.OriginalString, '?'); + return IsFontFile(sourceWithoutArguments); + } + + /// + /// Determines whether the specified file path refers to a supported font file type. + /// + /// This method performs a case-insensitive check for common font file extensions. It + /// does not verify the existence or validity of the file at the specified path. + /// The path of the file to check. Can be a relative or absolute path. If null, the method returns false. + /// true if the file path ends with ".ttf", ".otf", or ".ttc" (case-insensitive); otherwise, false. + public static bool IsFontFile(string filePath) + { + if (filePath is null) + { + return false; + } + + return filePath.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) + || filePath.EndsWith(".otf", StringComparison.OrdinalIgnoreCase) + || filePath.EndsWith(".ttc", StringComparison.OrdinalIgnoreCase); + } /// /// Searches for font assets at a given location and returns a quantity of found assets @@ -35,7 +64,7 @@ namespace Avalonia.Media.Fonts { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); var availableAssets = assetLoader.GetAssets(source, null); - return availableAssets.Where(x => IsFontTtfOrOtf(x)); + return availableAssets.Where(x => IsFontSource(x)); } /// @@ -97,13 +126,6 @@ namespace Avalonia.Media.Fonts && path.EndsWith(fileExtension, StringComparison.OrdinalIgnoreCase); } - private static bool IsFontTtfOrOtf(Uri uri) - { - var sourceWithoutArguments = GetSubString(uri.OriginalString, '?'); - return sourceWithoutArguments.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) - || sourceWithoutArguments.EndsWith(".otf", StringComparison.OrdinalIgnoreCase); - } - private static (string fileNameWithoutExtension, string extension) GetFileNameAndExtension( string path, char directorySeparator = '/') { diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs index 2a30f0abd8..4579cb5a34 100644 --- a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using Avalonia.Platform; namespace Avalonia.Media.Fonts { @@ -13,12 +12,6 @@ namespace Avalonia.Media.Fonts /// Uri Key { get; } - /// - /// Initializes the font collection. - /// - /// The font manager the collection is registered with. - void Initialize(IFontManagerImpl fontManager); - /// /// Try to get a glyph typeface for given parameters. /// @@ -70,5 +63,23 @@ namespace Avalonia.Media.Fonts /// /// Returns true if a synthetic glyph typface can be created; otherwise, false bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface); + + /// + /// Attempts to retrieve the glyph typeface that most closely matches the specified font family name, style, + /// weight, and stretch. + /// + /// This method searches for a glyph typeface in the font collection cache that matches + /// the specified parameters. If an exact match is not found, fallback mechanisms are applied to find the + /// closest available match based on the specified style, weight, and stretch. If no suitable match is found, + /// the method returns and is set to . + /// The name of the font family to search for. This parameter cannot be or empty. + /// The desired font style. + /// The desired font weight. + /// The desired font stretch. + /// When this method returns, contains the that most closely matches the specified + /// parameters, if a match is found; otherwise, . This parameter is passed uninitialized. + /// if a matching glyph typeface is found; otherwise, . + bool TryGetNearestMatch(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 index 3b0c71ce20..7ff8df9951 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -8,45 +7,33 @@ using Avalonia.Platform; namespace Avalonia.Media.Fonts { - internal class SystemFontCollection : FontCollectionBase, IFontCollection2 + internal class SystemFontCollection : FontCollectionBase { - private readonly FontManager _fontManager; - private readonly List _familyNames; + private readonly IFontManagerImpl _platformImpl; - public SystemFontCollection(FontManager fontManager) + public SystemFontCollection(IFontManagerImpl platformImpl) { - _fontManager = fontManager; - _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().Where(x => !string.IsNullOrEmpty(x)).ToList(); - } + _platformImpl = platformImpl ?? throw new ArgumentNullException(nameof(platformImpl)); - public override Uri Key => FontManager.SystemFontsKey; + var familyNames = _platformImpl.GetInstalledFontFamilyNames().Where(x => !string.IsNullOrEmpty(x)); - public override FontFamily this[int index] - { - get + foreach (var familyName in familyNames) { - var familyName = _familyNames[index]; - - return new FontFamily(familyName); + AddFontFamily(familyName); } } - public override int Count => _familyNames.Count; - - public override IEnumerator GetEnumerator() - { - foreach (var familyName in _familyNames) - { - yield return new FontFamily(familyName); - } - } + public override Uri Key => FontManager.SystemFontsKey; public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - glyphTypeface = null; + var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName); - var typeface = GetImplicitTypeface(new Typeface(familyName, style, weight, stretch), out familyName); + if (base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + { + return true; + } style = typeface.Style; @@ -56,123 +43,68 @@ namespace Avalonia.Media.Fonts var key = new FontCollectionKey(style, weight, stretch); - if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + //Check cache first to avoid unnecessary calls to the font manager + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface)) { - if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) - { - return glyphTypeface != null; - } + return glyphTypeface != null; } - glyphTypefaces ??= _glyphTypefaceCache.GetOrAdd(familyName, - (_) => new ConcurrentDictionary()); - //Try to create the glyph typeface via system font manager - if (!_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, - out glyphTypeface)) + if (!_platformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) { - glyphTypefaces.TryAdd(key, null); + //Add null to cache to avoid future calls + TryAddGlyphTypeface(familyName, key, null); return false; } - var createdKey = - new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); - - //No exact match - if (createdKey != key) + //Add to cache + if (!TryAddGlyphTypeface(glyphTypeface)) { - //Add the created glyph typeface to the cache so we can match it. - glyphTypefaces.TryAdd(createdKey, glyphTypeface); - - //Try to find nearest match if possible - if (TryGetNearestMatch(glyphTypefaces, key, out var nearestMatch)) - { - glyphTypeface = nearestMatch; - } - - //Try to create a synthetic glyph typeface - if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface)) - { - glyphTypeface = syntheticGlyphTypeface; - - return true; - } + return false; } - glyphTypefaces.TryAdd(key, glyphTypeface); - - return glyphTypeface != null; + //Requested glyph typeface should be in cache now + return base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface); } - public override void Initialize(IFontManagerImpl fontManager) + public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) { - //We initialize the system font collection during construction. - } + familyTypefaces = null; - public void AddCustomFontSource(Uri source) - { - if (source is null) + if (_platformImpl is IFontManagerImpl2 fontManagerImpl2) { - return; + return fontManagerImpl2.TryGetFamilyTypefaces(familyName, out familyTypefaces); } - LoadGlyphTypefaces(_fontManager.PlatformImpl, source); + return false; } - private void LoadGlyphTypefaces(IFontManagerImpl fontManager, Uri source) + public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName, + CultureInfo? culture, out Typeface match) { - var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - - var fontAssets = FontFamilyLoader.LoadFontAssets(source); + var requestedKey = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }; - foreach (var fontAsset in fontAssets) + //TODO12: Think about removing familyName parameter + if (base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match)) { - var stream = assetLoader.Open(fontAsset); + var matchKey = new FontCollectionKey { Style = match.Style, Weight = match.Weight, Stretch = match.Stretch }; - if (!fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) + if (requestedKey == matchKey) { - continue; - } - - //Add TypographicFamilyName to the cache - if (glyphTypeface is IGlyphTypeface2 glyphTypeface2 && !string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) - { - AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, glyphTypeface); + return true; } - - AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface); } - return; - } - - public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) - { - familyTypefaces = null; - - if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2) + if (_platformImpl is IFontManagerImpl2 fontManagerImpl2) { - return fontManagerImpl2.TryGetFamilyTypefaces(familyName, out familyTypefaces); - } - - return false; - } - - public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName, - CultureInfo? culture, out Typeface match) - { - //TODO12: Think about removing familyName parameter - match = default; - - if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2) - { - if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, culture, out var glyphTypeface)) + if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out var glyphTypeface)) { - AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface); - match = new Typeface(glyphTypeface.FamilyName, glyphTypeface.Style, glyphTypeface.Weight, - glyphTypeface.Stretch); + glyphTypeface.Stretch); + + // Add to cache if not already present + TryAddGlyphTypeface(glyphTypeface); return true; } @@ -181,26 +113,8 @@ namespace Avalonia.Media.Fonts } else { - return _fontManager.PlatformImpl.TryMatchCharacter(codepoint, style, weight, stretch, culture, out match); - } - } - - private void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface) - { - // Add family name to the collection if not exists - if (!_familyNames.Contains(familyName)) - { - _familyNames.Add(familyName); + return _platformImpl.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match); } - - // Get or create the typefaces dictionary for the family name - if (!_glyphTypefaceCache.TryGetValue(familyName, out var typefaces)) - { - _glyphTypefaceCache[familyName] = typefaces = new ConcurrentDictionary(); - } - - // Add the glyph typeface to the cache - typefaces.TryAdd(new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch), glyphTypeface); } } } diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs index 1ceff7101f..c9ad271568 100644 --- a/src/Avalonia.Base/Media/Typeface.cs +++ b/src/Avalonia.Base/Media/Typeface.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics; +using System.Text; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -127,5 +129,81 @@ namespace Avalonia.Media return hashCode; } } + + /// + /// Normalizes the typeface by extracting and removing style, weight, and stretch information from the font + /// family name, and returns a new instance with the updated properties. + /// + /// This method analyzes the font family name to identify and extract any style, weight, + /// or stretch information embedded within it. If such information is found, it is removed from the family name, + /// and the corresponding properties of the returned are updated accordingly. If no such + /// information is found, the method returns the current instance without modification. + /// When this method returns, contains the normalized font family name with style, weight, and stretch + /// information removed. This parameter is passed uninitialized. + /// A new instance with the updated , , + /// and properties, or the current instance if no normalization was performed. + public Typeface Normalize(out string normalizedFamilyName) + { + normalizedFamilyName = FontFamily.FamilyNames.PrimaryFamilyName; + + //Return early if no separator is present. + if (!normalizedFamilyName.Contains(' ')) + { + return this; + } + + var style = Style; + var weight = Weight; + var stretch = Stretch; + + StringBuilder? normalizedFamilyNameBuilder = null; + var totalCharsRemoved = 0; + + var tokenizer = new SpanStringTokenizer(normalizedFamilyName, ' '); + + // Skip initial family name. + tokenizer.ReadSpan(); + + while (tokenizer.TryReadSpan(out var token)) + { + // Don't try to match numbers. + if (new SpanStringTokenizer(token).TryReadInt32(out _)) + { + continue; + } + + // Try match with font style, weight or stretch and update accordingly. + var match = false; + if (EnumHelper.TryParse(token, true, out var newStyle)) + { + style = newStyle; + match = true; + } + else if (EnumHelper.TryParse(token, true, out var newWeight)) + { + weight = newWeight; + match = true; + } + else if (EnumHelper.TryParse(token, true, out var newStretch)) + { + stretch = newStretch; + match = true; + } + + if (match) + { + // Carve out matched word from the normalized name. + normalizedFamilyNameBuilder ??= new StringBuilder(normalizedFamilyName); + normalizedFamilyNameBuilder.Remove(tokenizer.CurrentTokenIndex - totalCharsRemoved, token.Length); + totalCharsRemoved += token.Length; + } + } + + // Get rid of any trailing spaces. + normalizedFamilyName = (normalizedFamilyNameBuilder?.ToString() ?? normalizedFamilyName).TrimEnd(); + + //Preserve old font source + return new Typeface(FontFamily, style, weight, stretch); + } } } diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index 42c9b3623f..e978ba03a8 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Metadata; namespace Avalonia.Platform @@ -29,13 +28,14 @@ namespace Avalonia.Platform /// The font style. /// The font weight. /// The font stretch. + /// The family name. This is optional and can be used as an initial hint for matching. /// The culture. /// The matching typeface. /// /// True, if the could match the character to specified parameters, False otherwise. /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface); + FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface); /// /// Tries to get a glyph typeface for specified parameters. @@ -72,13 +72,14 @@ namespace Avalonia.Platform /// The font style. /// The font weight. /// The font stretch. + /// The family name. This is optional and can be used as an initial hint for matching. /// The culture. /// The matching typeface. /// /// True, if the could match the character to specified parameters, False otherwise. /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, [NotNullWhen(true)] out IGlyphTypeface? typeface); + FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, [NotNullWhen(true)] out IGlyphTypeface? typeface); /// /// Tries to get a list of typefaces for the specified family name. diff --git a/src/Avalonia.Controls/SystemFontAppBuilderExtension.cs b/src/Avalonia.Controls/SystemFontAppBuilderExtension.cs index 2e9cb2bcbf..2e626e7189 100644 --- a/src/Avalonia.Controls/SystemFontAppBuilderExtension.cs +++ b/src/Avalonia.Controls/SystemFontAppBuilderExtension.cs @@ -11,7 +11,7 @@ namespace Avalonia { if(fontManager.SystemFonts is SystemFontCollection systemFontCollection) { - systemFontCollection.AddCustomFontSource(fontSource); + systemFontCollection.TryAddFontSource(fontSource); } }); } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index eaf0dac909..167d3fb8c7 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -216,8 +216,7 @@ namespace Avalonia.Headless } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, - CultureInfo? culture, out Typeface fontKey) + FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey) { fontKey = new Typeface(_defaultFamilyName); @@ -281,8 +280,7 @@ namespace Avalonia.Headless } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, - CultureInfo? culture, out Typeface fontKey) + FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey) { fontKey = new Typeface(_defaultFamilyName); diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index e013124cf1..f8cc9f5f78 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Text.RegularExpressions; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -35,9 +34,9 @@ namespace Avalonia.Skia [ThreadStatic] private static string[]? t_languageTagBuffer; public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface fontKey) + FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey) { - if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out SKTypeface? skTypeface)) + if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out SKTypeface? skTypeface)) { fontKey = default; @@ -61,10 +60,11 @@ namespace Avalonia.Skia FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, + string? familyName, CultureInfo? culture, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out SKTypeface? skTypeface)) + if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out SKTypeface? skTypeface)) { glyphTypeface = null; @@ -81,6 +81,7 @@ namespace Avalonia.Skia FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, + string? familyName, CultureInfo? culture, [NotNullWhen(true)] out SKTypeface? skTypeface) { @@ -110,7 +111,7 @@ namespace Avalonia.Skia t_languageTagBuffer ??= new string[1]; t_languageTagBuffer[0] = culture.Name; - skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); + skTypeface = _skFontManager.MatchCharacter(string.IsNullOrEmpty(familyName) ? null : familyName, skFontStyle, t_languageTagBuffer, codepoint); return skTypeface != null; } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index 703496a834..3dd4bbd7c1 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -120,30 +120,22 @@ namespace Avalonia.Skia foreach (var nameRecord in _nameTable) { - if(nameRecord.NameID == KnownNameIds.FontFamilyName) + var languageId = nameRecord.LanguageID == 0 ? + (ushort)CultureInfo.InvariantCulture.LCID : + nameRecord.LanguageID; + + switch (nameRecord.NameID) { - if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0) - { - continue; - } - - if (!familyNames.ContainsKey(nameRecord.LanguageID)) - { - familyNames[nameRecord.LanguageID] = nameRecord.Value; - } - } - - if(nameRecord.NameID == KnownNameIds.FontSubfamilyName) - { - if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0) - { - continue; - } - - if (!faceNames.ContainsKey(nameRecord.LanguageID)) - { - faceNames[nameRecord.LanguageID] = nameRecord.Value; - } + case KnownNameIds.FontFamilyName: + { + familyNames.TryAdd(languageId, nameRecord.Value); + break; + } + case KnownNameIds.FontSubfamilyName: + { + faceNames.TryAdd(languageId, nameRecord.Value); + break; + } } } diff --git a/tests/Avalonia.Base.UnitTests/Media/TypefaceTests.cs b/tests/Avalonia.Base.UnitTests/Media/TypefaceTests.cs index 826c1f292b..1b9226452d 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TypefaceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TypefaceTests.cs @@ -23,5 +23,24 @@ namespace Avalonia.Base.UnitTests.Media { Assert.Equal(new Typeface("Font A").GetHashCode(), new Typeface("Font A").GetHashCode()); } + + [InlineData("Hello World 6", "Hello World 6", FontStyle.Normal, FontWeight.Normal)] + [InlineData("Hello World Italic", "Hello World", FontStyle.Italic, FontWeight.Normal)] + [InlineData("Hello World Italic Bold", "Hello World", FontStyle.Italic, FontWeight.Bold)] + [InlineData("FontAwesome 6 Free Regular", "FontAwesome 6 Free", FontStyle.Normal, FontWeight.Normal)] + [InlineData("FontAwesome 6 Free Solid", "FontAwesome 6 Free", FontStyle.Normal, FontWeight.Solid)] + [InlineData("FontAwesome 6 Brands", "FontAwesome 6 Brands", FontStyle.Normal, FontWeight.Normal)] + [Theory] + public void Should_Get_Implicit_Typeface(string input, string familyName, FontStyle style, FontWeight weight) + { + var typeface = new Typeface(input); + + var normalizedTypeface = typeface.Normalize(out var normalizedFamilyName); + + Assert.Equal(familyName, normalizedFamilyName); + Assert.Equal(style, normalizedTypeface.Style); + Assert.Equal(weight, normalizedTypeface.Weight); + Assert.Equal(FontStretch.Normal, normalizedTypeface.Stretch); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 2ebac5b3a1..4e9af4978e 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -15,6 +15,7 @@ using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; +using Avalonia.Platform; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -1238,7 +1239,8 @@ namespace Avalonia.Controls.UnitTests keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub())); + textShaperImpl: new HeadlessTextShaperStub(), + assetLoader: new StandardAssetLoader())); } private class ItemsControlWithContainer : ItemsControl diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index ce85984f1c..4e1742d2a4 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -980,7 +980,7 @@ namespace Avalonia.Controls.UnitTests private static IDisposable Start(TestServices services = null) { CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US"); - return UnitTestApplication.Start(services ?? Services); + return UnitTestApplication.Start((services ?? Services).With(assetLoader: new StandardAssetLoader())); } private class Class1 : NotifyingBase diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index c208c70b1d..4082bb47a1 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -13,6 +13,7 @@ using Avalonia.Data; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.Platform; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -1351,7 +1352,8 @@ namespace Avalonia.Controls.UnitTests.Primitives keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub())); + textShaperImpl: new HeadlessTextShaperStub(), + assetLoader: new StandardAssetLoader())); } private class TestSelector : SelectingItemsControl diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index c20943807e..42e1033b6d 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -749,7 +749,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void TextBox_CaretIndex_Persists_When_Focus_Lost() { - using (UnitTestApplication.Start(FocusServices)) + using (UnitTestApplication.Start(FocusServices.With(assetLoader: new StandardAssetLoader()))) { var target1 = new TextBox { @@ -2160,7 +2160,8 @@ namespace Avalonia.Controls.UnitTests standardCursorFactory: Mock.Of(), renderInterface: new HeadlessPlatformRenderInterface(), textShaperImpl: new HeadlessTextShaperStub(), - fontManagerImpl: new HeadlessFontManagerStub()); + fontManagerImpl: new HeadlessFontManagerStub(), + assetLoader: new StandardAssetLoader()); internal static IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs index 62430deb09..ad22824333 100644 --- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Headless; using Avalonia.Layout; +using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -325,7 +326,8 @@ namespace Avalonia.Controls.UnitTests TestServices.MockThreadingInterface.With( fontManagerImpl: new HeadlessFontManagerStub(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub())); + textShaperImpl: new HeadlessTextShaperStub(), + assetLoader: new StandardAssetLoader())); } private static (TransitioningContentControl, TestTransition) CreateTarget(object content) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 0033f836f1..ffb7ab9048 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -15,6 +15,7 @@ using Avalonia.Input.Platform; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml.Templates; +using Avalonia.Platform; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -1841,7 +1842,8 @@ namespace Avalonia.Controls.UnitTests keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub())); + textShaperImpl: new HeadlessTextShaperStub(), + assetLoader: new StandardAssetLoader())); } private class Node : NotifyingBase diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj index 98a93596fa..24ab26ac74 100644 --- a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj +++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj @@ -20,4 +20,9 @@ + + + PreserveNewest + + diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs new file mode 100644 index 0000000000..66afed7a3a --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs @@ -0,0 +1,188 @@ +using System; +using System.IO; +using System.Linq; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Skia.UnitTests.Media +{ + public class CustomFontCollectionTests + { + private const string NotoMono = + "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"; + + [Fact] + public void Should_AddGlyphTypeface_By_Stream() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + var fontManager = FontManager.Current; + + var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute)); + + fontManager.AddFontCollection(fontCollection); + + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var assets = assetLoader.GetAssets(new Uri(NotoMono, UriKind.Absolute), null).ToArray(); + + Assert.NotEmpty(assets); + + var notoMonoLocation = assets.First(); + + using var notoMonoStream = assetLoader.Open(notoMonoLocation); + + Assert.NotNull(notoMonoStream); + + Assert.True(fontCollection.TryAddGlyphTypeface(notoMonoStream, out var glyphTypeface)); + + Assert.Equal("Inter", glyphTypeface.FamilyName); + + Assert.True(fontManager.TryGetGlyphTypeface(new Typeface("fonts:custom#Inter"), out var secondGlyphTypeface)); + + Assert.Equal(glyphTypeface, secondGlyphTypeface); + } + } + + [Fact] + public void Should_Enumerate_FontFamilies() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + var fontManager = FontManager.Current; + + var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute)); + + fontManager.AddFontCollection(fontCollection); + + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var assets = assetLoader.GetAssets(new Uri(NotoMono, UriKind.Absolute), null).Where(x => x.AbsolutePath.EndsWith(".ttf")).ToArray(); + + foreach (var asset in assets) + { + fontCollection.TryAddGlyphTypeface(assetLoader.Open(asset), out _); + } + + var families = fontCollection.ToArray(); + + Assert.True(families.Length >= assets.Length); + + var other = new CustomFontCollection(new Uri("fonts:other", UriKind.Absolute)); + + foreach (var family in families) + { + var familyTypefaces = family.FamilyTypefaces; + + foreach (var typeface in familyTypefaces) + { + other.TryAddGlyphTypeface(typeface.GlyphTypeface); + } + } + + Assert.Equal(families.Length, other.Count); + + for (int i = 0; i < families.Length; i++) + { + Assert.Equal(families[i].Name, other[i].Name); + } + } + } + + [Fact] + public void Should_AddFontSource_From_File() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + var fontManager = FontManager.Current; + var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute)); + fontManager.AddFontCollection(fontCollection); + + // Path to the test font + var fontPath = Path.Combine(AppContext.BaseDirectory, "Assets", "Inter-Regular.ttf"); + Assert.True(File.Exists(fontPath)); + + var fontUri = new Uri(fontPath, UriKind.Absolute); + + // Add the font file + Assert.True(fontCollection.TryAddFontSource(fontUri)); + + // Check if the font was loaded + Assert.True(fontCollection.TryGetGlyphTypeface("Inter", FontStyle.Normal, FontWeight.Regular, FontStretch.Normal, out var glyphTypeface)); + Assert.Equal("Inter", glyphTypeface.FamilyName); + + // Check if the FontManager can find the font + Assert.True(fontManager.TryGetGlyphTypeface(new Typeface("fonts:custom#Inter"), out var glyphTypeface2)); + Assert.Equal(glyphTypeface, glyphTypeface2); + } + } + + [Fact] + public void Should_AddFontSource_From_Folder() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + var fontManager = FontManager.Current; + var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute)); + fontManager.AddFontCollection(fontCollection); + + // Path to the test fonts + var fontsFolder = Path.Combine(AppContext.BaseDirectory, "Assets"); + Assert.True(Directory.Exists(fontsFolder)); + + var folderUri = new Uri(fontsFolder + Path.DirectorySeparatorChar, UriKind.Absolute); + + // Add the fonts + Assert.True(fontCollection.TryAddFontSource(folderUri)); + + // Check if the font was loaded + Assert.True(fontCollection.TryGetGlyphTypeface("Inter", FontStyle.Normal, FontWeight.Regular, FontStretch.Normal, out var glyphTypeface)); + Assert.Equal("Inter", glyphTypeface.FamilyName); + + // Check if the FontManager can find the font + Assert.True(fontManager.TryGetGlyphTypeface(new Typeface("fonts:custom#Inter"), out var glyphTypeface2)); + Assert.Equal(glyphTypeface, glyphTypeface2); + } + } + + [Fact] + public void Should_AddFontSource_From_Resource() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + var fontManager = FontManager.Current; + var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute)); + fontManager.AddFontCollection(fontCollection); + + // Use the NotoMono resource as FontSource + var notoMonoUri = new Uri(NotoMono, UriKind.Absolute); + + // Add the font resource + Assert.True(fontCollection.TryAddFontSource(notoMonoUri)); + + // Get the loaded family names + var families = fontCollection.ToArray(); + + Assert.NotEmpty(families); + + // Try to get a GlyphTypeface + Assert.True(fontCollection.TryGetGlyphTypeface("Noto Mono", FontStyle.Normal, FontWeight.Regular, FontStretch.Normal, out var glyphTypeface)); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); + + // Check if the FontManager can find the font + Assert.True(fontManager.TryGetGlyphTypeface(new Typeface("fonts:custom#Noto Mono"), out var glyphTypeface2)); + Assert.Equal(glyphTypeface, glyphTypeface2); + } + } + + + + private class CustomFontCollection(Uri key) : FontCollectionBase + { + public override Uri Key { get; } = key; + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 34dc32ac6b..008f1eedbd 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using Avalonia.Media; @@ -13,18 +14,28 @@ namespace Avalonia.Skia.UnitTests.Media public class CustomFontManagerImpl : IFontManagerImpl, IDisposable { private readonly string _defaultFamilyName; - private readonly IFontCollection _customFonts; - private bool _isInitialized; + private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; + private IFontCollection? _systemFonts; public CustomFontManagerImpl() { - var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"); + _defaultFamilyName = FontManager.SystemFontsKey + "#Noto Mono"; + } - _defaultFamilyName = source.AbsoluteUri + "#Noto Mono"; + public IFontCollection SystemFonts + { + get + { + if(_systemFonts is null) + { + var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"); - _customFonts = new EmbeddedFontCollection(source, source); - } + _systemFonts = new EmbeddedFontCollection(FontManager.SystemFontsKey, source); + } + return _systemFonts; + } + } public string GetDefaultFontFamilyName() { return _defaultFamilyName; @@ -32,27 +43,46 @@ namespace Avalonia.Skia.UnitTests.Media public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { - if (!_isInitialized) + // Directly load from assets to avoid creating the full font collection + try { - _customFonts.Initialize(this); - - _isInitialized = true; + var key = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"); + + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var names = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + foreach (var fontAsset in fontAssets) + { + try + { + using var stream = assetLoader.Open(fontAsset); + using var sk = SKTypeface.FromStream(stream); + + if (sk != null && !string.IsNullOrEmpty(sk.FamilyName)) + { + names.Add(sk.FamilyName); + } + } + catch + { + // Ignore faulty assets + } + } + + return names.ToArray(); + } + catch + { + return Array.Empty(); } - - return _customFonts.Select(x=> x.Name).ToArray(); } - private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - CultureInfo culture, out Typeface typeface) + string? familyName, CultureInfo? culture, out Typeface typeface) { - if (!_isInitialized) - { - _customFonts.Initialize(this); - } - - if(_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out typeface)) + if(SystemFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) { return true; } @@ -68,12 +98,7 @@ namespace Avalonia.Skia.UnitTests.Media public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) { - if (!_isInitialized) - { - _customFonts.Initialize(this); - } - - if (_customFonts.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + if (SystemFonts.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) { return true; } @@ -97,7 +122,7 @@ namespace Avalonia.Skia.UnitTests.Media public void Dispose() { - _customFonts.Dispose(); + _systemFonts?.Dispose(); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs index 75ad5b1a10..ef55f1d6df 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs @@ -14,13 +14,8 @@ namespace Avalonia.Skia.UnitTests.Media { public class EmbeddedFontCollectionTests { - private const string s_notoMono = - "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"; - - private const string s_manrope = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"; - - private const string s_misans = "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#MiSans"; - + private const string s_fontAssets = + "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"; [InlineData(FontWeight.SemiLight, FontStyle.Normal)] [InlineData(FontWeight.Bold, FontStyle.Italic)] @@ -28,14 +23,13 @@ namespace Avalonia.Skia.UnitTests.Media [Theory] public void Should_Get_Near_Matching_Typeface(FontWeight fontWeight, FontStyle fontStyle) { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()))) { - var source = new Uri(s_notoMono, UriKind.Absolute); + var key = new Uri("fonts:testFonts", UriKind.Absolute); + var source = new Uri(s_fontAssets, UriKind.Absolute); var fontCollection = new TestEmbeddedFontCollection(source, source); - fontCollection.Initialize(new CustomFontManagerImpl()); - Assert.True(fontCollection.TryGetGlyphTypeface("Noto Mono", fontStyle, fontWeight, FontStretch.Normal, out var glyphTypeface)); var actual = glyphTypeface.FamilyName; @@ -47,13 +41,12 @@ namespace Avalonia.Skia.UnitTests.Media [Fact] public void Should_Not_Get_Typeface_For_Invalid_FamilyName() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()))) { - var source = new Uri(s_notoMono, UriKind.Absolute); - - var fontCollection = new TestEmbeddedFontCollection(source, source); + var key = new Uri("fonts:testFonts", UriKind.Absolute); + var source = new Uri(s_fontAssets, UriKind.Absolute); - fontCollection.Initialize(new CustomFontManagerImpl()); + var fontCollection = new TestEmbeddedFontCollection(key, source); Assert.False(fontCollection.TryGetGlyphTypeface("ABC", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _)); } @@ -62,13 +55,12 @@ namespace Avalonia.Skia.UnitTests.Media [Fact] public void Should_Get_Typeface_For_Partial_FamilyName() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + 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 key = new Uri("fonts:testFonts", UriKind.Absolute); + var source = new Uri(s_fontAssets, UriKind.Absolute); - var fontCollection = new TestEmbeddedFontCollection(source, source); - - fontCollection.Initialize(new CustomFontManagerImpl()); + var fontCollection = new TestEmbeddedFontCollection(key, source); Assert.True(fontCollection.TryGetGlyphTypeface("T", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface)); @@ -79,13 +71,12 @@ namespace Avalonia.Skia.UnitTests.Media [Fact] public void Should_Get_Typeface_For_TypographicFamilyName() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()))) { - var source = new Uri(s_manrope, UriKind.Absolute); - - var fontCollection = new TestEmbeddedFontCollection(source, source); + var key = new Uri("fonts:testFonts", UriKind.Absolute); + var source = new Uri(s_fontAssets, UriKind.Absolute); - fontCollection.Initialize(new CustomFontManagerImpl()); + var fontCollection = new TestEmbeddedFontCollection(key, source); Assert.True(fontCollection.TryGetGlyphTypeface("Manrope", FontStyle.Normal, FontWeight.Light, FontStretch.Normal, out var glyphTypeface)); @@ -102,13 +93,12 @@ namespace Avalonia.Skia.UnitTests.Media [Fact] public void Should_Cache_Synthetic_GlyphTypeface() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()))) { - var source = new Uri(s_manrope, UriKind.Absolute); - - var fontCollection = new TestEmbeddedFontCollection(source, source, true); + var key = new Uri("fonts:testFonts", UriKind.Absolute); + var source = new Uri(s_fontAssets, UriKind.Absolute); - fontCollection.Initialize(new CustomFontManagerImpl()); + var fontCollection = new TestEmbeddedFontCollection(key, source, true); Assert.True(fontCollection.TryGetGlyphTypeface("Manrope", FontStyle.Normal, FontWeight.ExtraBlack, FontStretch.Normal, out var glyphTypeface)); @@ -125,18 +115,19 @@ namespace Avalonia.Skia.UnitTests.Media [Fact] public void Should_Cache_Nearest_Match_For_MiSans() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) { - var source = new Uri(s_misans, UriKind.Absolute); + var source = new Uri(s_fontAssets, UriKind.Absolute); var fontCollection = new TestEmbeddedFontCollection(source, source); - fontCollection.Initialize(new CustomFontManagerImpl()); - + // Font weight 304 Assert.True(fontCollection.TryGetGlyphTypeface("MiSans", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var regularGlyphTypeface)); + // Font weight regular (400) Assert.True(fontCollection.TryGetGlyphTypeface("MiSans", FontStyle.Normal, FontWeight.Bold, FontStretch.Normal, out var boldGlyphTypeface)); + // Font weight 700 Assert.True(fontCollection.GlyphTypefaceCache.TryGetValue("MiSans", out var glyphTypefaces)); Assert.Equal(3, glyphTypefaces.Count); diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs index 5c830df4fa..9aae98efe9 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs @@ -18,33 +18,12 @@ namespace Avalonia.Skia.UnitTests.Media private const string NotoMono = "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"; - [InlineData("Hello World 6", "Hello World 6", FontStyle.Normal, FontWeight.Normal)] - [InlineData("Hello World Italic", "Hello World", FontStyle.Italic, FontWeight.Normal)] - [InlineData("Hello World Italic Bold", "Hello World", FontStyle.Italic, FontWeight.Bold)] - [InlineData("FontAwesome 6 Free Regular", "FontAwesome 6 Free", FontStyle.Normal, FontWeight.Normal)] - [InlineData("FontAwesome 6 Free Solid", "FontAwesome 6 Free", FontStyle.Normal, FontWeight.Solid)] - [InlineData("FontAwesome 6 Brands", "FontAwesome 6 Brands", FontStyle.Normal, FontWeight.Normal)] - [Theory] - public void Should_Get_Implicit_Typeface(string input, string familyName, FontStyle style, FontWeight weight) - { - var typeface = new Typeface(input); - - var result = FontCollectionBase.GetImplicitTypeface(typeface, out var normalizedFamilyName); - - Assert.Equal(familyName, normalizedFamilyName); - Assert.Equal(style, result.Style); - Assert.Equal(weight, result.Weight); - Assert.Equal(FontStretch.Normal, result.Stretch); - } - [Win32Fact("Relies on some installed font family")] public void Should_Cache_Nearest_Match() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) { - var fontManager = FontManager.Current; - - var fontCollection = new TestSystemFontCollection(FontManager.Current); + var fontCollection = new TestSystemFontCollection(FontManager.Current.PlatformImpl); Assert.True(fontCollection.TryGetGlyphTypeface("Arial", FontStyle.Normal, FontWeight.ExtraBlack, FontStretch.Normal, out var glyphTypeface)); @@ -62,9 +41,8 @@ namespace Avalonia.Skia.UnitTests.Media private class TestSystemFontCollection : SystemFontCollection { - public TestSystemFontCollection(FontManager fontManager) : base(fontManager) + public TestSystemFontCollection(IFontManagerImpl platformImpl) : base(platformImpl) { - } public IDictionary> GlyphTypefaceCache => _glyphTypefaceCache; @@ -81,8 +59,6 @@ namespace Avalonia.Skia.UnitTests.Media var fontCollection = new CustomizableFontCollection(source, source, new[] { fallback }); - fontCollection.Initialize(FontManager.Current.PlatformImpl); - Assert.True(fontCollection.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match)); Assert.Equal("Arial", match.FontFamily.Name); @@ -100,8 +76,6 @@ namespace Avalonia.Skia.UnitTests.Media var fontCollection = new CustomizableFontCollection(key, key, null, new[] { ignorable }); - fontCollection.Initialize(FontManager.Current.PlatformImpl); - var typeface = new Typeface(ignorable); var glyphTypeface = typeface.GlyphTypeface; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index faacaa7d6d..3e2c12a912 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -6,6 +6,7 @@ using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Utilities; using Xunit; @@ -1352,8 +1353,17 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting .With(renderInterface: new PlatformRenderInterface(), textShaperImpl: new TextShaperImpl())); + var customFontManagerImpl = new CustomFontManagerImpl(); + AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new FontManager(new CustomFontManagerImpl())); + .Bind().ToConstant(customFontManagerImpl); + + var fontManager = new FontManager(customFontManagerImpl); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(fontManager); + + fontManager.AddFontCollection(customFontManagerImpl.SystemFonts); return disposable; } diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs index 24599c5a9b..7ca32e2d3b 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs @@ -37,7 +37,7 @@ namespace Avalonia.UnitTests } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, CultureInfo culture, out Typeface fontKey) + FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey) { foreach (var customTypeface in _customTypefaces) {