Browse Source

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 <julien@lebosquain.net>
pull/20137/head
Benedikt Stebner 2 months ago
committed by GitHub
parent
commit
3e62621aaf
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 40
      api/Avalonia.Win32.Interoperability.nupkg.xml
  2. 264
      api/Avalonia.nupkg.xml
  3. 43
      src/Avalonia.Base/Media/FontManager.cs
  4. 156
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  5. 9
      src/Avalonia.Base/Media/Fonts/EmptySystemFontCollection.cs
  6. 623
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  7. 42
      src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
  8. 25
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  9. 170
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  10. 78
      src/Avalonia.Base/Media/Typeface.cs
  11. 7
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  12. 2
      src/Avalonia.Controls/SystemFontAppBuilderExtension.cs
  13. 6
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  14. 11
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  15. 28
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  16. 19
      tests/Avalonia.Base.UnitTests/Media/TypefaceTests.cs
  17. 4
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  18. 2
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  19. 4
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  20. 5
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  21. 4
      tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
  22. 4
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  23. 5
      tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
  24. 188
      tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs
  25. 73
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  26. 61
      tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs
  27. 30
      tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs
  28. 12
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  29. 2
      tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

40
api/Avalonia.Win32.Interoperability.nupkg.xml

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost.PreFilterMessage(System.Windows.Forms.Message@)</Target>
<Left>baseline/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll</Left>
<Right>current/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost.PreFilterMessage(System.Windows.Forms.Message@)</Target>
<Left>baseline/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll</Left>
<Right>current/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost.PreFilterMessage(System.Windows.Forms.Message@)</Target>
<Left>baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll</Left>
<Right>current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost</Target>
<Left>baseline/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll</Left>
<Right>current/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost</Target>
<Left>baseline/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll</Left>
<Right>current/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost</Target>
<Left>baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll</Left>
<Right>current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll</Right>
</Suppression>
</Suppressions>

264
api/Avalonia.nupkg.xml

@ -1,24 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Media.Fonts.FontFamilyLoader</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Media.Fonts.FontFamilyLoader</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Media.Fonts.FontFamilyLoader</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Media.Fonts.FontFamilyLoader</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Dialogs.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Dialogs.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Dialogs.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Dialogs.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)</Target>
@ -43,6 +169,12 @@
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target>
@ -97,6 +229,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target>
@ -151,6 +289,12 @@
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target>
@ -181,4 +325,124 @@
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.get_Count</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>P:Avalonia.Media.Fonts.FontCollectionBase.Count</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.get_Count</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>P:Avalonia.Media.Fonts.FontCollectionBase.Count</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.get_Count</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>P:Avalonia.Media.Fonts.FontCollectionBase.Count</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.get_Count</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>P:Avalonia.Media.Fonts.FontCollectionBase.Count</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0012</DiagnosticId>
<Target>P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
</Suppressions>

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

@ -31,8 +31,6 @@ namespace Avalonia.Media
{
PlatformImpl = platformImpl;
AddFontCollection(new SystemFontCollection(this));
var options = AvaloniaLocator.Current.GetService<FontManagerOptions>();
_fontFallbacks = options?.FontFallbacks;
_fontFamilyMappings = options?.FontFamilyMappings;
@ -76,7 +74,19 @@ namespace Avalonia.Media
/// <summary>
/// Get all system fonts.
/// </summary>
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; }
@ -99,6 +109,7 @@ namespace Avalonia.Media
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);
}
/// <summary>
@ -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 (source == SystemFontsKey)
{
fontCollection = new SystemFontCollection(PlatformImpl);
}
else
{
if (source.IsAbsoluteResm() || source.IsAvares())
{
fontCollection = new EmbeddedFontCollection(source, source);
}
}
if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
if (fontCollection != null)
{
fontCollection = embeddedFonts;
return _fontCollections.TryAdd(fontCollection.Key, fontCollection);
}
}

156
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<FontFamily> _fontFamilies = new List<FontFamily>(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<IAssetLoader>();
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);
}
}
}
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<FontFamily> 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<FontCollectionKey, IGlyphTypeface?>();
});
typefaces.TryAdd(
new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch),
glyphTypeface);
}
}
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
familyTypefaces = null;
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
var typefaces = new List<Typeface>(glyphTypefaces.Count);
foreach (var key in glyphTypefaces.Keys)
{
typefaces.Add(new Typeface(new FontFamily(_key, familyName), key.Style, key.Weight, key.Stretch));
TryAddFontSource(_source);
}
familyTypefaces = typefaces;
return true;
}
return false;
}
public override Uri Key => _key;
}
}

9
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;
}
}

623
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@ -4,24 +4,35 @@ 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<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> _glyphTypefaceCache = new();
private static readonly Comparer<FontFamily> FontFamilyNameComparer =
Comparer<FontFamily>.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<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> _glyphTypefaceCache = new();
public abstract int Count { get; }
private readonly object _fontFamiliesLock = new();
private volatile FontFamily[] _fontFamilies = Array.Empty<FontFamily>();
private readonly IFontManagerImpl _fontManagerImpl;
private readonly IAssetLoader _assetLoader;
public abstract FontFamily this[int index] { get; }
protected FontCollectionBase()
{
_fontManagerImpl = AvaloniaLocator.Current.GetRequiredService<IFontManagerImpl>();
_assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
}
public abstract Uri Key { get; }
public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
public int Count => _fontFamilies.Length;
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)
@ -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;
}
@ -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<FontFamily> GetEnumerator() => ((IEnumerable<FontFamily>)_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;
stretch = typeface.Stretch;
var key = new FontCollectionKey(style, weight, stretch);
void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface)
return TryGetGlyphTypeface(familyName, key, out glyphTypeface);
}
public virtual bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
var typefaces = _glyphTypefaceCache.GetOrAdd(familyName,
x =>
familyTypefaces = null;
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
return new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
});
// 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;
}
public abstract void Initialize(IFontManagerImpl fontManager);
return false;
}
public abstract IEnumerator<FontFamily> GetEnumerator();
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;
void IDisposable.Dispose()
return false;
}
var key = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch };
return TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface);
}
/// <summary>
/// Attempts to add the specified <see cref="IGlyphTypeface"/> to the font collection.
/// </summary>
/// <remarks>This method checks the <see cref="IGlyphTypeface.FamilyName"/> and, if applicable,
/// the typographic family name and other family names provided by the <see cref="IGlyphTypeface2"/> 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.</remarks>
/// <param name="glyphTypeface">The glyph typeface to add. Must not be <see langword="null"/> and must have a non-empty <see
/// cref="IGlyphTypeface.FamilyName"/>.</param>
/// <returns><see langword="true"/> if the glyph typeface was successfully added to the collection; otherwise, <see
/// langword="false"/>.</returns>
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)
{
pair.Value?.Dispose();
var result = false;
//Add the TypographicFamilyName to the cache
if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
{
if (TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, glyphTypeface))
{
result = true;
}
}
GC.SuppressFinalize(this);
foreach (var kvp in glyphTypeface2.FamilyNames)
{
if (TryAddGlyphTypeface(kvp.Value, key, glyphTypeface))
{
result = true;
}
}
IEnumerator IEnumerable.GetEnumerator()
return result;
}
else
{
return GetEnumerator();
return TryAddGlyphTypeface(glyphTypeface.FamilyName, key, glyphTypeface);
}
}
internal static bool TryGetNearestMatch(
ConcurrentDictionary<FontCollectionKey,
IGlyphTypeface?> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
/// <summary>
/// Attempts to add a glyph typeface from the specified font stream.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="stream">The font stream containing the font data. The stream must be readable and positioned at the beginning of the
/// font data.</param>
/// <param name="glyphTypeface">When this method returns, contains the created <see cref="IGlyphTypeface"/> instance if the operation
/// succeeds; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if the glyph typeface was successfully created and added; otherwise, <see
/// langword="false"/>.</returns>
public bool TryAddGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out glyphTypeface))
{
return false;
}
return TryAddGlyphTypeface(glyphTypeface);
}
/// <summary>
/// Attempts to add a font source to the font collection.
/// </summary>
/// <remarks>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 <paramref name="source"/> is <see langword="null"/>, the method returns <see
/// langword="false"/>.</remarks>
/// <param name="source">The URI of the font source to add. This can be a file path, a resource URI, or another valid font source
/// URI.</param>
/// <returns><see langword="true"/> if at least one font from the specified source was successfully added to the font
/// collection; otherwise, <see langword="false"/>.</returns>
public bool TryAddFontSource(Uri source)
{
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;
}
/// <summary>
/// Inserts the specified font family into the internal collection, maintaining the collection in sorted order
/// by font family name.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="fontFamily">The font family to add to the collection. Cannot be null.</param>
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;
}
}
/// <summary>
/// Attempts to retrieve a glyph typeface that matches the specified font family name and font collection key.
/// </summary>
/// <remarks>This method performs a binary search to locate font families with names that match
/// the specified <paramref name="familyName"/>. If multiple matches are found, the method iterates over them to
/// find the best match based on the provided <paramref name="key"/>.</remarks>
/// <param name="familyName">The name of the font family to search for. This parameter is case-insensitive.</param>
/// <param name="key">The key representing the desired font collection attributes.</param>
/// <param name="glyphTypeface">When this method returns, contains the matching <see cref="IGlyphTypeface"/> if a match is found; otherwise,
/// <see langword="null"/>.</param>
/// <returns><see langword="true"/> if a matching glyph typeface is found; otherwise, <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Attempts to retrieve the nearest matching <see cref="IGlyphTypeface"/> for the specified font key from the
/// provided collection of glyph typefaces.
/// </summary>
/// <remarks>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 <see cref="IGlyphTypeface"/> from the
/// collection, if any.</remarks>
/// <param name="glyphTypefaces">A collection of glyph typefaces, indexed by <see cref="FontCollectionKey"/>.</param>
/// <param name="key">The <see cref="FontCollectionKey"/> representing the desired font attributes.</param>
/// <param name="glyphTypeface">When this method returns, contains the <see cref="IGlyphTypeface"/> that most closely matches the specified
/// key, if a match is found; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if a matching <see cref="IGlyphTypeface"/> is found; otherwise, <see
/// langword="false"/>.</returns>
protected bool TryGetNearestMatch(IDictionary<FontCollectionKey, IGlyphTypeface?> 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,9 +616,84 @@ namespace Avalonia.Media.Fonts
return false;
}
internal static bool TryFindStretchFallback(
ConcurrentDictionary<FontCollectionKey,
IGlyphTypeface?> glyphTypefaces,
/// <summary>
/// Attempts to add a glyph typeface to the cache for the specified font family and key.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="familyName">The name of the font family to which the glyph typeface belongs. Cannot be null or empty.</param>
/// <param name="key">The key associated with the glyph typeface in the cache.</param>
/// <param name="glyphTypeface">The glyph typeface to add to the cache. Can be null.</param>
/// <returns><see langword="true"/> if the glyph typeface was successfully added to the cache; otherwise, <see
/// langword="false"/>.</returns>
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<FontCollectionKey, IGlyphTypeface?>();
// 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);
}
/// <summary>
/// Attempts to locate a fallback glyph typeface with a similar font stretch to the specified key within the
/// provided collection.
/// </summary>
/// <remarks>The search prioritizes font stretches closest to the requested value, expanding
/// outward until a match is found or all options are exhausted.</remarks>
/// <param name="glyphTypefaces">A dictionary mapping font collection keys to their corresponding glyph typefaces. Used as the source for
/// searching fallback typefaces.</param>
/// <param name="key">The font collection key specifying the desired font stretch and other font attributes to match.</param>
/// <param name="glyphTypeface">When this method returns, contains the found glyph typeface with a similar stretch if one exists; otherwise,
/// null.</param>
/// <returns>true if a suitable fallback glyph typeface is found; otherwise, false.</returns>
private static bool TryFindStretchFallback(
IDictionary<FontCollectionKey, IGlyphTypeface?> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
@ -263,9 +725,22 @@ namespace Avalonia.Media.Fonts
return false;
}
internal static bool TryFindWeightFallback(
ConcurrentDictionary<FontCollectionKey,
IGlyphTypeface?> glyphTypefaces,
/// <summary>
/// Attempts to locate a fallback glyph typeface in the specified collection that closely matches the weight of
/// the provided key.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="glyphTypefaces">A dictionary mapping font collection keys to glyph typeface instances. The method searches this collection
/// for a suitable fallback.</param>
/// <param name="key">The font collection key specifying the desired font attributes, including weight, for which a fallback glyph
/// typeface is sought.</param>
/// <param name="glyphTypeface">When this method returns, contains the matching glyph typeface if a suitable fallback is found; otherwise,
/// null.</param>
/// <returns>true if a fallback glyph typeface matching the requested weight is found; otherwise, false.</returns>
private static bool TryFindWeightFallback(
IDictionary<FontCollectionKey, IGlyphTypeface?> 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)
{
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))
{
// Don't try to match numbers.
if (new SpanStringTokenizer(token).TryReadInt32(out _))
void IDisposable.Dispose()
{
continue;
}
// Try match with font style, weight or stretch and update accordingly.
var match = false;
if (EnumHelper.TryParse<FontStyle>(token, true, out var newStyle))
foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
{
style = newStyle;
match = true;
}
else if (EnumHelper.TryParse<FontWeight>(token, true, out var newWeight))
foreach (var pair in glyphTypefaces)
{
weight = newWeight;
match = true;
pair.Value?.Dispose();
}
else if (EnumHelper.TryParse<FontStretch>(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;
}
GC.SuppressFinalize(this);
}
// Get rid of any trailing spaces.
normalizedFamilyName = (normalizedFamilyNameBuilder?.ToString() ?? normalizedFamilyName).TrimEnd();
//Preserve old font source
return new Typeface(typeface.FontFamily, style, weight, stretch);
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

42
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
{
/// <summary>
/// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
@ -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<Uri>();
}
/// <summary>
/// Determines whether the specified URI refers to a font file source.
/// </summary>
/// <param name="uri">The URI to evaluate as a potential font file source. Must not be null.</param>
/// <returns>true if the URI points to a recognized font file source; otherwise, false.</returns>
public static bool IsFontSource(Uri uri)
{
var sourceWithoutArguments = GetSubString(uri.OriginalString, '?');
return IsFontFile(sourceWithoutArguments);
}
/// <summary>
/// Determines whether the specified file path refers to a supported font file type.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="filePath">The path of the file to check. Can be a relative or absolute path. If null, the method returns false.</param>
/// <returns>true if the file path ends with ".ttf", ".otf", or ".ttc" (case-insensitive); otherwise, false.</returns>
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);
}
/// <summary>
/// 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<IAssetLoader>();
var availableAssets = assetLoader.GetAssets(source, null);
return availableAssets.Where(x => IsFontTtfOrOtf(x));
return availableAssets.Where(x => IsFontSource(x));
}
/// <summary>
@ -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 = '/')
{

25
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
/// </summary>
Uri Key { get; }
/// <summary>
/// Initializes the font collection.
/// </summary>
/// <param name="fontManager">The font manager the collection is registered with.</param>
void Initialize(IFontManagerImpl fontManager);
/// <summary>
/// Try to get a glyph typeface for given parameters.
/// </summary>
@ -70,5 +63,23 @@ namespace Avalonia.Media.Fonts
/// <param name="syntheticGlyphTypeface"></param>
/// <returns>Returns <c>true</c> if a synthetic glyph typface can be created; otherwise, <c>false</c></returns>
bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface);
/// <summary>
/// Attempts to retrieve the glyph typeface that most closely matches the specified font family name, style,
/// weight, and stretch.
/// </summary>
/// <remarks>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 <see langword="false"/> and <paramref name="glyphTypeface"/> is set to <see
/// langword="null"/>.</remarks>
/// <param name="familyName">The name of the font family to search for. This parameter cannot be <see langword="null"/> or empty.</param>
/// <param name="style">The desired font style.</param>
/// <param name="weight">The desired font weight.</param>
/// <param name="stretch">The desired font stretch.</param>
/// <param name="glyphTypeface">When this method returns, contains the <see cref="IGlyphTypeface"/> that most closely matches the specified
/// parameters, if a match is found; otherwise, <see langword="null"/>. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if a matching glyph typeface is found; otherwise, <see langword="false"/>.</returns>
bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
}
}

170
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<string> _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<FontFamily> 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,102 +43,36 @@ namespace Avalonia.Media.Fonts
var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
//Check cache first to avoid unnecessary calls to the font manager
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
return glyphTypeface != null;
}
}
glyphTypefaces ??= _glyphTypefaceCache.GetOrAdd(familyName,
(_) => new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>());
//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 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;
}
}
glyphTypefaces.TryAdd(key, glyphTypeface);
return glyphTypeface != null;
}
public override void Initialize(IFontManagerImpl fontManager)
{
//We initialize the system font collection during construction.
}
public void AddCustomFontSource(Uri source)
//Add to cache
if (!TryAddGlyphTypeface(glyphTypeface))
{
if (source is null)
{
return;
}
LoadGlyphTypefaces(_fontManager.PlatformImpl, source);
}
private void LoadGlyphTypefaces(IFontManagerImpl fontManager, Uri source)
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontAssets = FontFamilyLoader.LoadFontAssets(source);
foreach (var fontAsset in fontAssets)
{
var stream = assetLoader.Open(fontAsset);
if (!fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
{
continue;
}
//Add TypographicFamilyName to the cache
if (glyphTypeface is IGlyphTypeface2 glyphTypeface2 && !string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
{
AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, glyphTypeface);
}
AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface);
return false;
}
return;
//Requested glyph typeface should be in cache now
return base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface);
}
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
familyTypefaces = null;
if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2)
if (_platformImpl is IFontManagerImpl2 fontManagerImpl2)
{
return fontManagerImpl2.TryGetFamilyTypefaces(familyName, out familyTypefaces);
}
@ -162,18 +83,29 @@ namespace Avalonia.Media.Fonts
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;
var requestedKey = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch };
if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2)
//TODO12: Think about removing familyName parameter
if (base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match))
{
if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, culture, out var glyphTypeface))
var matchKey = new FontCollectionKey { Style = match.Style, Weight = match.Weight, Stretch = match.Stretch };
if (requestedKey == matchKey)
{
AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface);
return true;
}
}
if (_platformImpl is IFontManagerImpl2 fontManagerImpl2)
{
if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out var glyphTypeface))
{
match = new Typeface(glyphTypeface.FamilyName, glyphTypeface.Style, glyphTypeface.Weight,
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);
}
return _platformImpl.TryMatchCharacter(codepoint, style, weight, stretch, familyName, 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);
}
// Get or create the typefaces dictionary for the family name
if (!_glyphTypefaceCache.TryGetValue(familyName, out var typefaces))
{
_glyphTypefaceCache[familyName] = typefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
}
// Add the glyph typeface to the cache
typefaces.TryAdd(new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch), glyphTypeface);
}
}
}

78
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;
}
}
/// <summary>
/// Normalizes the typeface by extracting and removing style, weight, and stretch information from the font
/// family name, and returns a new <see cref="Typeface"/> instance with the updated properties.
/// </summary>
/// <remarks>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 <see cref="Typeface"/> are updated accordingly. If no such
/// information is found, the method returns the current instance without modification.</remarks>
/// <param name="normalizedFamilyName">When this method returns, contains the normalized font family name with style, weight, and stretch
/// information removed. This parameter is passed uninitialized.</param>
/// <returns>A new <see cref="Typeface"/> instance with the updated <see cref="FontStyle"/>, <see cref="FontWeight"/>,
/// and <see cref="FontStretch"/> properties, or the current instance if no normalization was performed.</returns>
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<FontStyle>(token, true, out var newStyle))
{
style = newStyle;
match = true;
}
else if (EnumHelper.TryParse<FontWeight>(token, true, out var newWeight))
{
weight = newWeight;
match = true;
}
else if (EnumHelper.TryParse<FontStretch>(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);
}
}
}

7
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
/// <param name="fontStyle">The font style.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStretch">The font stretch.</param>
/// <param name="familyName">The family name. This is optional and can be used as an initial hint for matching.</param>
/// <param name="culture">The culture.</param>
/// <param name="typeface">The matching typeface.</param>
/// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
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);
/// <summary>
/// Tries to get a glyph typeface for specified parameters.
@ -72,13 +72,14 @@ namespace Avalonia.Platform
/// <param name="fontStyle">The font style.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStretch">The font stretch.</param>
/// <param name="familyName">The family name. This is optional and can be used as an initial hint for matching.</param>
/// <param name="culture">The culture.</param>
/// <param name="typeface">The matching typeface.</param>
/// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
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);
/// <summary>
/// Tries to get a list of typefaces for the specified family name.

2
src/Avalonia.Controls/SystemFontAppBuilderExtension.cs

@ -11,7 +11,7 @@ namespace Avalonia
{
if(fontManager.SystemFonts is SystemFontCollection systemFontCollection)
{
systemFontCollection.AddCustomFontSource(fontSource);
systemFontCollection.TryAddFontSource(fontSource);
}
});
}

6
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);

11
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;
}

28
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -120,29 +120,21 @@ namespace Avalonia.Skia
foreach (var nameRecord in _nameTable)
{
if(nameRecord.NameID == KnownNameIds.FontFamilyName)
{
if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0)
{
continue;
}
if (!familyNames.ContainsKey(nameRecord.LanguageID))
{
familyNames[nameRecord.LanguageID] = nameRecord.Value;
}
}
var languageId = nameRecord.LanguageID == 0 ?
(ushort)CultureInfo.InvariantCulture.LCID :
nameRecord.LanguageID;
if(nameRecord.NameID == KnownNameIds.FontSubfamilyName)
switch (nameRecord.NameID)
{
if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0)
case KnownNameIds.FontFamilyName:
{
continue;
familyNames.TryAdd(languageId, nameRecord.Value);
break;
}
if (!faceNames.ContainsKey(nameRecord.LanguageID))
case KnownNameIds.FontSubfamilyName:
{
faceNames[nameRecord.LanguageID] = nameRecord.Value;
faceNames.TryAdd(languageId, nameRecord.Value);
break;
}
}
}

19
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);
}
}
}

4
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

2
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

4
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

5
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<ICursorFactory>(),
renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub(),
fontManagerImpl: new HeadlessFontManagerStub());
fontManagerImpl: new HeadlessFontManagerStub(),
assetLoader: new StandardAssetLoader());
internal static IControlTemplate CreateTemplate()
{

4
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)

4
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

5
tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj

@ -20,4 +20,9 @@
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="..\Avalonia.RenderTests\Assets\Inter-Regular.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
</Project>

188
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<IAssetLoader>();
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<IAssetLoader>();
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;
}
}
}

73
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);
var key = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests");
_isInitialized = true;
}
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
return _customFonts.Select(x=> x.Name).ToArray();
}
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
var names = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
foreach (var fontAsset in fontAssets)
{
try
{
using var stream = assetLoader.Open(fontAsset);
using var sk = SKTypeface.FromStream(stream);
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,
CultureInfo culture, out Typeface typeface)
if (sk != null && !string.IsNullOrEmpty(sk.FamilyName))
{
names.Add(sk.FamilyName);
}
}
catch
{
if (!_isInitialized)
// Ignore faulty assets
}
}
return names.ToArray();
}
catch
{
_customFonts.Initialize(this);
return Array.Empty<string>();
}
}
if(_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out typeface))
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,
string? familyName, CultureInfo? culture, out Typeface 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();
}
}
}

61
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);

30
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<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> 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;

12
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<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl()));
.Bind<IFontManagerImpl>().ToConstant(customFontManagerImpl);
var fontManager = new FontManager(customFontManagerImpl);
AvaloniaLocator.CurrentMutable
.Bind<FontManager>().ToConstant(fontManager);
fontManager.AddFontCollection(customFontManagerImpl.SystemFonts);
return disposable;
}

2
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)
{

Loading…
Cancel
Save