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. 49
      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. 639
      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. 174
      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. 38
      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. 81
      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"?> <?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids --> <!-- 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"> <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> <Suppression>
<DiagnosticId>CP0002</DiagnosticId> <DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)</Target> <Target>M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Dialogs.dll</Left> <Left>baseline/Avalonia/lib/net6.0/Avalonia.Dialogs.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Dialogs.dll</Right> <Right>current/Avalonia/lib/net6.0/Avalonia.Dialogs.dll</Right>
</Suppression> </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> <Suppression>
<DiagnosticId>CP0002</DiagnosticId> <DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)</Target> <Target>M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Dialogs.dll</Left> <Left>baseline/Avalonia/lib/net8.0/Avalonia.Dialogs.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Dialogs.dll</Right> <Right>current/Avalonia/lib/net8.0/Avalonia.Dialogs.dll</Right>
</Suppression> </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> <Suppression>
<DiagnosticId>CP0002</DiagnosticId> <DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)</Target> <Target>M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll</Left> <Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll</Right> <Right>current/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll</Right>
</Suppression> </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> <Suppression>
<DiagnosticId>CP0006</DiagnosticId> <DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)</Target> <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> <Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right> <Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression> </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> <Suppression>
<DiagnosticId>CP0006</DiagnosticId> <DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target> <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> <Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right> <Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression> </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> <Suppression>
<DiagnosticId>CP0006</DiagnosticId> <DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target> <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> <Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right> <Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression> </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> <Suppression>
<DiagnosticId>CP0006</DiagnosticId> <DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target> <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> <Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Right> <Right>current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Right>
</Suppression> </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> </Suppressions>

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

@ -31,8 +31,6 @@ namespace Avalonia.Media
{ {
PlatformImpl = platformImpl; PlatformImpl = platformImpl;
AddFontCollection(new SystemFontCollection(this));
var options = AvaloniaLocator.Current.GetService<FontManagerOptions>(); var options = AvaloniaLocator.Current.GetService<FontManagerOptions>();
_fontFallbacks = options?.FontFallbacks; _fontFallbacks = options?.FontFallbacks;
_fontFamilyMappings = options?.FontFamilyMappings; _fontFamilyMappings = options?.FontFamilyMappings;
@ -76,7 +74,19 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Get all system fonts. /// Get all system fonts.
/// </summary> /// </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; } internal IFontManagerImpl PlatformImpl { get; }
@ -93,12 +103,13 @@ namespace Avalonia.Media
glyphTypeface = null; glyphTypeface = null;
var fontFamily = GetMappedFontFamily(typeface.FontFamily); var fontFamily = GetMappedFontFamily(typeface.FontFamily);
if (typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName) if (typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName)
{ {
return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
} }
if (fontFamily.Key != null) if (fontFamily.Key != null)
{ {
if (fontFamily.Key is CompositeFontFamilyKey compositeKey) if (fontFamily.Key is CompositeFontFamilyKey compositeKey)
@ -167,7 +178,7 @@ namespace Avalonia.Media
FontFamily GetMappedFontFamily(FontFamily fontFamily) 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; return fontFamily;
} }
@ -222,8 +233,6 @@ namespace Avalonia.Media
return fontCollection; return fontCollection;
}); });
fontCollection.Initialize(PlatformImpl);
} }
/// <summary> /// <summary>
@ -288,7 +297,7 @@ namespace Avalonia.Media
if (TryGetFontCollection(source, out var fontCollection) && if (TryGetFontCollection(source, out var fontCollection) &&
// With composite fonts we need to first check if the font collection contains the family if not we skip it // With composite fonts we need to first check if the font collection contains the family if not we skip it
fontCollection.TryGetGlyphTypeface(familyName, fontStyle, fontWeight, fontStretch, out _) && fontCollection.TryGetGlyphTypeface(familyName, fontStyle, fontWeight, fontStretch, out _) &&
fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{ {
return true; return true;
@ -319,7 +328,7 @@ namespace Avalonia.Media
if (key == null) if (key == null)
{ {
if(SystemFonts is IFontCollection2 fontCollection2) if (SystemFonts is IFontCollection2 fontCollection2)
{ {
if (fontCollection2.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces)) if (fontCollection2.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces))
{ {
@ -352,15 +361,23 @@ namespace Avalonia.Media
source = SystemFontsKey; source = SystemFontsKey;
} }
if (!_fontCollections.TryGetValue(source, out fontCollection) && (source.IsAbsoluteResm() || source.IsAvares())) if (!_fontCollections.TryGetValue(source, out fontCollection))
{ {
var embeddedFonts = new EmbeddedFontCollection(source, source); if (source == SystemFontsKey)
{
embeddedFonts.Initialize(PlatformImpl); fontCollection = new SystemFontCollection(PlatformImpl);
}
if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts)) else
{
if (source.IsAbsoluteResm() || source.IsAvares())
{
fontCollection = new EmbeddedFontCollection(source, source);
}
}
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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts 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 _key;
private readonly Uri _source; private readonly Uri _source;
public EmbeddedFontCollection(Uri key, Uri source) public EmbeddedFontCollection(Uri key, Uri source)
@ -20,152 +12,10 @@ namespace Avalonia.Media.Fonts
_key = key; _key = key;
_source = source; _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)) TryAddFontSource(_source);
{
AddGlyphTypeface(glyphTypeface);
}
}
} }
public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, public override Uri Key => _key;
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));
}
familyTypefaces = typefaces;
return true;
}
return false;
}
} }
} }

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

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

@ -4,30 +4,41 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Text; using System.IO;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media.Fonts 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();
private readonly object _fontFamiliesLock = new();
private volatile FontFamily[] _fontFamilies = Array.Empty<FontFamily>();
private readonly IFontManagerImpl _fontManagerImpl;
private readonly IAssetLoader _assetLoader;
protected FontCollectionBase()
{
_fontManagerImpl = AvaloniaLocator.Current.GetRequiredService<IFontManagerImpl>();
_assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
}
public abstract int Count { get; } public abstract Uri Key { get; }
public abstract FontFamily this[int index] { get; } public int Count => _fontFamilies.Length;
public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, public FontFamily this[int index] => _fontFamilies[index];
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch,
string? familyName, CultureInfo? culture, out Typeface match) string? familyName, CultureInfo? culture, out Typeface match)
{ {
match = default; match = default;
//If a font family is defined we try to find a match inside that family first //If a font family is defined we try to find a match inside that family first
if (familyName != null && _glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) if (familyName != null && _glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{ {
@ -45,7 +56,7 @@ namespace Avalonia.Media.Fonts
//Try to find a match in any font family //Try to find a match in any font family
foreach (var pair in _glyphTypefaceCache) foreach (var pair in _glyphTypefaceCache)
{ {
if(pair.Key == familyName) if (pair.Key == familyName)
{ {
//We already tried this before //We already tried this before
continue; continue;
@ -57,7 +68,11 @@ namespace Avalonia.Media.Fonts
{ {
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) 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; return true;
} }
@ -69,9 +84,9 @@ namespace Avalonia.Media.Fonts
public virtual bool TryCreateSyntheticGlyphTypeface( public virtual bool TryCreateSyntheticGlyphTypeface(
IGlyphTypeface glyphTypeface, IGlyphTypeface glyphTypeface,
FontStyle style, FontStyle style,
FontWeight weight, FontWeight weight,
FontStretch stretch, FontStretch stretch,
[NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface) [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
{ {
syntheticGlyphTypeface = null; syntheticGlyphTypeface = null;
@ -82,8 +97,6 @@ namespace Avalonia.Media.Fonts
return false; return false;
} }
var fontManager = FontManager.Current.PlatformImpl;
var key = new FontCollectionKey(style, weight, stretch); var key = new FontCollectionKey(style, weight, stretch);
var currentKey = var currentKey =
@ -115,17 +128,17 @@ namespace Avalonia.Media.Fonts
{ {
using (stream) using (stream)
{ {
if (fontManager.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface)) if (_fontManagerImpl.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface))
{ {
//Add the TypographicFamilyName to the cache //Add the TypographicFamilyName to the cache
if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
{ {
AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, syntheticGlyphTypeface); TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, syntheticGlyphTypeface);
} }
foreach (var kvp in glyphTypeface2.FamilyNames) foreach (var kvp in glyphTypeface2.FamilyNames)
{ {
AddGlyphTypefaceByFamilyName(kvp.Value, syntheticGlyphTypeface); TryAddGlyphTypeface(kvp.Value, key, syntheticGlyphTypeface);
} }
return true; return true;
@ -136,46 +149,420 @@ namespace Avalonia.Media.Fonts
} }
return false; 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;
void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface) stretch = typeface.Stretch;
var key = new FontCollectionKey(style, weight, stretch);
return TryGetGlyphTypeface(familyName, key, out glyphTypeface);
}
public virtual bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
familyTypefaces = null;
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{ {
var typefaces = _glyphTypefaceCache.GetOrAdd(familyName, // Take a snapshot of the entries to avoid issues with concurrent modifications
x => var entries = glyphTypefaces.ToArray();
{
return new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
});
typefaces.TryAdd(key, glyphTypeface); var typefaces = new Typeface[entries.Length];
for (var i = 0; i < entries.Length; i++)
{
var key = entries[i].Key;
typefaces[i] = new Typeface(new FontFamily(Key + "#" + familyName), key.Style, key.Weight, key.Stretch);
}
familyTypefaces = typefaces;
return true;
} }
return false;
} }
public abstract void Initialize(IFontManagerImpl fontManager); public bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
glyphTypeface = null;
return false;
}
public abstract IEnumerator<FontFamily> GetEnumerator(); var key = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch };
void IDisposable.Dispose() 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)
{
var result = false;
//Add the TypographicFamilyName to the cache
if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
{ {
pair.Value?.Dispose(); if (TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, glyphTypeface))
{
result = true;
}
}
foreach (var kvp in glyphTypeface2.FamilyNames)
{
if (TryAddGlyphTypeface(kvp.Value, key, glyphTypeface))
{
result = true;
}
} }
return result;
} }
else
{
return TryAddGlyphTypeface(glyphTypeface.FamilyName, key, glyphTypeface);
}
}
GC.SuppressFinalize(this); /// <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);
} }
IEnumerator IEnumerable.GetEnumerator() /// <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)
{ {
return GetEnumerator(); if (source is null)
{
return false;
}
var result = false;
switch (source.Scheme)
{
case "avares":
case "resm":
{
var fontAssets = FontFamilyLoader.LoadFontAssets(source);
foreach (var fontAsset in fontAssets)
{
var stream = _assetLoader.Open(fontAsset);
if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
{
continue;
}
var key = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
//Add TypographicFamilyName to the cache
if (glyphTypeface is IGlyphTypeface2 glyphTypeface2 && !string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
{
if (TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, glyphTypeface))
{
result = true;
}
}
if (TryAddGlyphTypeface(glyphTypeface.FamilyName, key, glyphTypeface))
{
result = true;
}
}
break;
}
case "file":
{
// If the path is a file, load the font file directly
if (FontFamilyLoader.IsFontSource(source))
{
if (!File.Exists(source.LocalPath))
{
return false;
}
using var stream = File.OpenRead(source.LocalPath);
if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
{
if (TryAddGlyphTypeface(glyphTypeface))
{
result = true;
}
}
}
// If the path is a directory, load all font files from that directory
else
{
if (!Directory.Exists(source.LocalPath))
{
return false;
}
foreach (var file in Directory.EnumerateFiles(source.LocalPath))
{
if (FontFamilyLoader.IsFontFile(file))
{
using var stream = File.OpenRead(file);
if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
{
if (TryAddGlyphTypeface(glyphTypeface))
{
result = true;
}
}
}
}
}
break;
}
default:
//Unsupported scheme
return false;
}
return result;
} }
internal static bool TryGetNearestMatch( /// <summary>
ConcurrentDictionary<FontCollectionKey, /// Inserts the specified font family into the internal collection, maintaining the collection in sorted order
IGlyphTypeface?> glyphTypefaces, /// by font family name.
FontCollectionKey key, /// </summary>
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) /// <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) if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
{ {
@ -218,7 +605,7 @@ namespace Avalonia.Media.Fonts
//Take the first glyph typeface we can find. //Take the first glyph typeface we can find.
foreach (var typeface in glyphTypefaces.Values) foreach (var typeface in glyphTypefaces.Values)
{ {
if(typeface != null) if (typeface != null)
{ {
glyphTypeface = typeface; glyphTypeface = typeface;
@ -229,11 +616,86 @@ namespace Avalonia.Media.Fonts
return false; return false;
} }
internal static bool TryFindStretchFallback( /// <summary>
ConcurrentDictionary<FontCollectionKey, /// Attempts to add a glyph typeface to the cache for the specified font family and key.
IGlyphTypeface?> glyphTypefaces, /// </summary>
FontCollectionKey key, /// <remarks>If the specified font family does not exist in the cache, it is added along with the
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) /// 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)
{ {
glyphTypeface = null; glyphTypeface = null;
@ -263,9 +725,22 @@ namespace Avalonia.Media.Fonts
return false; return false;
} }
internal static bool TryFindWeightFallback( /// <summary>
ConcurrentDictionary<FontCollectionKey, /// Attempts to locate a fallback glyph typeface in the specified collection that closely matches the weight of
IGlyphTypeface?> glyphTypefaces, /// 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, FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{ {
@ -348,68 +823,22 @@ namespace Avalonia.Media.Fonts
return false; return false;
} }
internal static Typeface GetImplicitTypeface(Typeface typeface, out string normalizedFamilyName) void IDisposable.Dispose()
{ {
normalizedFamilyName = typeface.FontFamily.FamilyNames.PrimaryFamilyName; foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
//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. foreach (var pair in glyphTypefaces)
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. pair.Value?.Dispose();
normalizedFamilyNameBuilder ??= new StringBuilder(normalizedFamilyName);
normalizedFamilyNameBuilder.Remove(tokenizer.CurrentTokenIndex - totalCharsRemoved, token.Length);
totalCharsRemoved += token.Length;
} }
} }
// Get rid of any trailing spaces. GC.SuppressFinalize(this);
normalizedFamilyName = (normalizedFamilyNameBuilder?.ToString() ?? normalizedFamilyName).TrimEnd(); }
//Preserve old font source IEnumerator IEnumerable.GetEnumerator()
return new Typeface(typeface.FontFamily, style, weight, stretch); {
return GetEnumerator();
} }
} }
} }

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

@ -6,7 +6,7 @@ using Avalonia.Utilities;
namespace Avalonia.Media.Fonts namespace Avalonia.Media.Fonts
{ {
public static class FontFamilyLoader internal static class FontFamilyLoader
{ {
/// <summary> /// <summary>
/// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/> /// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
@ -17,7 +17,7 @@ namespace Avalonia.Media.Fonts
{ {
if (source.IsAvares() || source.IsAbsoluteResm()) if (source.IsAvares() || source.IsAbsoluteResm())
{ {
return IsFontTtfOrOtf(source) ? return IsFontSource(source) ?
GetFontAssetsByExpression(source) : GetFontAssetsByExpression(source) :
GetFontAssetsBySource(source); GetFontAssetsBySource(source);
} }
@ -25,6 +25,35 @@ namespace Avalonia.Media.Fonts
return Enumerable.Empty<Uri>(); 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> /// <summary>
/// Searches for font assets at a given location and returns a quantity of found assets /// Searches for font assets at a given location and returns a quantity of found assets
@ -35,7 +64,7 @@ namespace Avalonia.Media.Fonts
{ {
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var availableAssets = assetLoader.GetAssets(source, null); var availableAssets = assetLoader.GetAssets(source, null);
return availableAssets.Where(x => IsFontTtfOrOtf(x)); return availableAssets.Where(x => IsFontSource(x));
} }
/// <summary> /// <summary>
@ -97,13 +126,6 @@ namespace Avalonia.Media.Fonts
&& path.EndsWith(fileExtension, StringComparison.OrdinalIgnoreCase); && 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( private static (string fileNameWithoutExtension, string extension) GetFileNameAndExtension(
string path, char directorySeparator = '/') string path, char directorySeparator = '/')
{ {

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

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts namespace Avalonia.Media.Fonts
{ {
@ -13,12 +12,6 @@ namespace Avalonia.Media.Fonts
/// </summary> /// </summary>
Uri Key { get; } 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> /// <summary>
/// Try to get a glyph typeface for given parameters. /// Try to get a glyph typeface for given parameters.
/// </summary> /// </summary>
@ -70,5 +63,23 @@ namespace Avalonia.Media.Fonts
/// <param name="syntheticGlyphTypeface"></param> /// <param name="syntheticGlyphTypeface"></param>
/// <returns>Returns <c>true</c> if a synthetic glyph typface can be created; otherwise, <c>false</c></returns> /// <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); 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);
} }
} }

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

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
@ -8,45 +7,33 @@ using Avalonia.Platform;
namespace Avalonia.Media.Fonts namespace Avalonia.Media.Fonts
{ {
internal class SystemFontCollection : FontCollectionBase, IFontCollection2 internal class SystemFontCollection : FontCollectionBase
{ {
private readonly FontManager _fontManager; private readonly IFontManagerImpl _platformImpl;
private readonly List<string> _familyNames;
public SystemFontCollection(FontManager fontManager) public SystemFontCollection(IFontManagerImpl platformImpl)
{ {
_fontManager = fontManager; _platformImpl = platformImpl ?? throw new ArgumentNullException(nameof(platformImpl));
_familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().Where(x => !string.IsNullOrEmpty(x)).ToList();
}
public override Uri Key => FontManager.SystemFontsKey; var familyNames = _platformImpl.GetInstalledFontFamilyNames().Where(x => !string.IsNullOrEmpty(x));
public override FontFamily this[int index] foreach (var familyName in familyNames)
{
get
{ {
var familyName = _familyNames[index]; AddFontFamily(familyName);
return new FontFamily(familyName);
} }
} }
public override int Count => _familyNames.Count; public override Uri Key => FontManager.SystemFontsKey;
public override IEnumerator<FontFamily> GetEnumerator()
{
foreach (var familyName in _familyNames)
{
yield return new FontFamily(familyName);
}
}
public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) 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; style = typeface.Style;
@ -56,123 +43,68 @@ namespace Avalonia.Media.Fonts
var key = new FontCollectionKey(style, weight, stretch); var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) //Check cache first to avoid unnecessary calls to the font manager
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface))
{ {
if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) return glyphTypeface != null;
{
return glyphTypeface != null;
}
} }
glyphTypefaces ??= _glyphTypefaceCache.GetOrAdd(familyName,
(_) => new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>());
//Try to create the glyph typeface via system font manager //Try to create the glyph typeface via system font manager
if (!_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, if (!_platformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
out glyphTypeface))
{ {
glyphTypefaces.TryAdd(key, null); //Add null to cache to avoid future calls
TryAddGlyphTypeface(familyName, key, null);
return false; return false;
} }
var createdKey = //Add to cache
new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); if (!TryAddGlyphTypeface(glyphTypeface))
//No exact match
if (createdKey != key)
{ {
//Add the created glyph typeface to the cache so we can match it. return false;
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); //Requested glyph typeface should be in cache now
return base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface);
return glyphTypeface != null;
} }
public override void Initialize(IFontManagerImpl fontManager) public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{ {
//We initialize the system font collection during construction. familyTypefaces = null;
}
public void AddCustomFontSource(Uri source) if (_platformImpl is IFontManagerImpl2 fontManagerImpl2)
{
if (source is null)
{ {
return; return fontManagerImpl2.TryGetFamilyTypefaces(familyName, out familyTypefaces);
} }
LoadGlyphTypefaces(_fontManager.PlatformImpl, source); return false;
} }
private void LoadGlyphTypefaces(IFontManagerImpl fontManager, Uri source) public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName,
CultureInfo? culture, out Typeface match)
{ {
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); var requestedKey = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch };
var fontAssets = FontFamilyLoader.LoadFontAssets(source);
foreach (var fontAsset in fontAssets) //TODO12: Think about removing familyName parameter
if (base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match))
{ {
var stream = assetLoader.Open(fontAsset); var matchKey = new FontCollectionKey { Style = match.Style, Weight = match.Weight, Stretch = match.Stretch };
if (!fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) if (requestedKey == matchKey)
{ {
continue; return true;
}
//Add TypographicFamilyName to the cache
if (glyphTypeface is IGlyphTypeface2 glyphTypeface2 && !string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
{
AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, glyphTypeface);
} }
AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface);
} }
return; if (_platformImpl is IFontManagerImpl2 fontManagerImpl2)
}
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
familyTypefaces = null;
if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2)
{ {
return fontManagerImpl2.TryGetFamilyTypefaces(familyName, out familyTypefaces); if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out var glyphTypeface))
}
return false;
}
public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName,
CultureInfo? culture, out Typeface match)
{
//TODO12: Think about removing familyName parameter
match = default;
if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2)
{
if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, culture, out var glyphTypeface))
{ {
AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface);
match = new Typeface(glyphTypeface.FamilyName, glyphTypeface.Style, glyphTypeface.Weight, match = new Typeface(glyphTypeface.FamilyName, glyphTypeface.Style, glyphTypeface.Weight,
glyphTypeface.Stretch); glyphTypeface.Stretch);
// Add to cache if not already present
TryAddGlyphTypeface(glyphTypeface);
return true; return true;
} }
@ -181,26 +113,8 @@ namespace Avalonia.Media.Fonts
} }
else 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;
using System.Diagnostics; using System.Diagnostics;
using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -127,5 +129,81 @@ namespace Avalonia.Media
return hashCode; 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.Globalization;
using System.IO; using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Platform namespace Avalonia.Platform
@ -29,13 +28,14 @@ namespace Avalonia.Platform
/// <param name="fontStyle">The font style.</param> /// <param name="fontStyle">The font style.</param>
/// <param name="fontWeight">The font weight.</param> /// <param name="fontWeight">The font weight.</param>
/// <param name="fontStretch">The font stretch.</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="culture">The culture.</param>
/// <param name="typeface">The matching typeface.</param> /// <param name="typeface">The matching typeface.</param>
/// <returns> /// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise. /// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns> /// </returns>
bool TryMatchCharacter(int codepoint, FontStyle fontStyle, 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> /// <summary>
/// Tries to get a glyph typeface for specified parameters. /// 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="fontStyle">The font style.</param>
/// <param name="fontWeight">The font weight.</param> /// <param name="fontWeight">The font weight.</param>
/// <param name="fontStretch">The font stretch.</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="culture">The culture.</param>
/// <param name="typeface">The matching typeface.</param> /// <param name="typeface">The matching typeface.</param>
/// <returns> /// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise. /// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns> /// </returns>
bool TryMatchCharacter(int codepoint, FontStyle fontStyle, 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> /// <summary>
/// Tries to get a list of typefaces for the specified family name. /// 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) 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, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch, FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey)
CultureInfo? culture, out Typeface fontKey)
{ {
fontKey = new Typeface(_defaultFamilyName); fontKey = new Typeface(_defaultFamilyName);
@ -281,8 +280,7 @@ namespace Avalonia.Headless
} }
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch, FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey)
CultureInfo? culture, out Typeface fontKey)
{ {
fontKey = new Typeface(_defaultFamilyName); 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.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text.RegularExpressions;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using SkiaSharp; using SkiaSharp;
@ -35,9 +34,9 @@ namespace Avalonia.Skia
[ThreadStatic] private static string[]? t_languageTagBuffer; [ThreadStatic] private static string[]? t_languageTagBuffer;
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, 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; fontKey = default;
@ -61,10 +60,11 @@ namespace Avalonia.Skia
FontStyle fontStyle, FontStyle fontStyle,
FontWeight fontWeight, FontWeight fontWeight,
FontStretch fontStretch, FontStretch fontStretch,
string? familyName,
CultureInfo? culture, CultureInfo? culture,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) [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; glyphTypeface = null;
@ -81,6 +81,7 @@ namespace Avalonia.Skia
FontStyle fontStyle, FontStyle fontStyle,
FontWeight fontWeight, FontWeight fontWeight,
FontStretch fontStretch, FontStretch fontStretch,
string? familyName,
CultureInfo? culture, CultureInfo? culture,
[NotNullWhen(true)] out SKTypeface? skTypeface) [NotNullWhen(true)] out SKTypeface? skTypeface)
{ {
@ -110,7 +111,7 @@ namespace Avalonia.Skia
t_languageTagBuffer ??= new string[1]; t_languageTagBuffer ??= new string[1];
t_languageTagBuffer[0] = culture.Name; 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; return skTypeface != null;
} }

38
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

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

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()); 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.LogicalTree;
using Avalonia.Markup.Xaml.Templates; using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -1238,7 +1239,8 @@ namespace Avalonia.Controls.UnitTests
keyboardNavigation: () => new KeyboardNavigationHandler(), keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(), inputManager: new InputManager(),
renderInterface: new HeadlessPlatformRenderInterface(), renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub())); textShaperImpl: new HeadlessTextShaperStub(),
assetLoader: new StandardAssetLoader()));
} }
private class ItemsControlWithContainer : ItemsControl 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) private static IDisposable Start(TestServices services = null)
{ {
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US"); 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 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.Headless;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -1351,7 +1352,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
keyboardNavigation: () => new KeyboardNavigationHandler(), keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(), inputManager: new InputManager(),
renderInterface: new HeadlessPlatformRenderInterface(), renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub())); textShaperImpl: new HeadlessTextShaperStub(),
assetLoader: new StandardAssetLoader()));
} }
private class TestSelector : SelectingItemsControl private class TestSelector : SelectingItemsControl

5
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -749,7 +749,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void TextBox_CaretIndex_Persists_When_Focus_Lost() public void TextBox_CaretIndex_Persists_When_Focus_Lost()
{ {
using (UnitTestApplication.Start(FocusServices)) using (UnitTestApplication.Start(FocusServices.With(assetLoader: new StandardAssetLoader())))
{ {
var target1 = new TextBox var target1 = new TextBox
{ {
@ -2160,7 +2160,8 @@ namespace Avalonia.Controls.UnitTests
standardCursorFactory: Mock.Of<ICursorFactory>(), standardCursorFactory: Mock.Of<ICursorFactory>(),
renderInterface: new HeadlessPlatformRenderInterface(), renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub(), textShaperImpl: new HeadlessTextShaperStub(),
fontManagerImpl: new HeadlessFontManagerStub()); fontManagerImpl: new HeadlessFontManagerStub(),
assetLoader: new StandardAssetLoader());
internal static IControlTemplate CreateTemplate() 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.Controls.Templates;
using Avalonia.Headless; using Avalonia.Headless;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Xunit; using Xunit;
@ -325,7 +326,8 @@ namespace Avalonia.Controls.UnitTests
TestServices.MockThreadingInterface.With( TestServices.MockThreadingInterface.With(
fontManagerImpl: new HeadlessFontManagerStub(), fontManagerImpl: new HeadlessFontManagerStub(),
renderInterface: new HeadlessPlatformRenderInterface(), renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub())); textShaperImpl: new HeadlessTextShaperStub(),
assetLoader: new StandardAssetLoader()));
} }
private static (TransitioningContentControl, TestTransition) CreateTarget(object content) 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.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml.Templates; using Avalonia.Markup.Xaml.Templates;
using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -1841,7 +1842,8 @@ namespace Avalonia.Controls.UnitTests
keyboardNavigation: () => new KeyboardNavigationHandler(), keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(), inputManager: new InputManager(),
renderInterface: new HeadlessPlatformRenderInterface(), renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub())); textShaperImpl: new HeadlessTextShaperStub(),
assetLoader: new StandardAssetLoader()));
} }
private class Node : NotifyingBase 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="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" /> <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Update="..\Avalonia.RenderTests\Assets\Inter-Regular.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
</Project> </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;
}
}
}

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

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Avalonia.Media; using Avalonia.Media;
@ -13,18 +14,28 @@ namespace Avalonia.Skia.UnitTests.Media
public class CustomFontManagerImpl : IFontManagerImpl, IDisposable public class CustomFontManagerImpl : IFontManagerImpl, IDisposable
{ {
private readonly string _defaultFamilyName; private readonly string _defaultFamilyName;
private readonly IFontCollection _customFonts; private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
private bool _isInitialized; private IFontCollection? _systemFonts;
public CustomFontManagerImpl() 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() public string GetDefaultFontFamilyName()
{ {
return _defaultFamilyName; return _defaultFamilyName;
@ -32,27 +43,46 @@ namespace Avalonia.Skia.UnitTests.Media
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) 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>();
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
var names = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
foreach (var fontAsset in fontAssets)
{
try
{
using var stream = assetLoader.Open(fontAsset);
using var sk = SKTypeface.FromStream(stream);
if (sk != null && !string.IsNullOrEmpty(sk.FamilyName))
{
names.Add(sk.FamilyName);
}
}
catch
{
// Ignore faulty assets
}
}
return names.ToArray();
}
catch
{
return Array.Empty<string>();
} }
return _customFonts.Select(x=> x.Name).ToArray();
} }
private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,
CultureInfo culture, out Typeface typeface) string? familyName, CultureInfo? culture, out Typeface typeface)
{ {
if (!_isInitialized) if(SystemFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{
_customFonts.Initialize(this);
}
if(_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out typeface))
{ {
return true; return true;
} }
@ -68,12 +98,7 @@ namespace Avalonia.Skia.UnitTests.Media
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{ {
if (!_isInitialized) if (SystemFonts.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
{
_customFonts.Initialize(this);
}
if (_customFonts.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
{ {
return true; return true;
} }
@ -97,7 +122,7 @@ namespace Avalonia.Skia.UnitTests.Media
public void Dispose() 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 public class EmbeddedFontCollectionTests
{ {
private const string s_notoMono = private const string s_fontAssets =
"resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"; "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests";
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";
[InlineData(FontWeight.SemiLight, FontStyle.Normal)] [InlineData(FontWeight.SemiLight, FontStyle.Normal)]
[InlineData(FontWeight.Bold, FontStyle.Italic)] [InlineData(FontWeight.Bold, FontStyle.Italic)]
@ -28,14 +23,13 @@ namespace Avalonia.Skia.UnitTests.Media
[Theory] [Theory]
public void Should_Get_Near_Matching_Typeface(FontWeight fontWeight, FontStyle fontStyle) 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); var fontCollection = new TestEmbeddedFontCollection(source, source);
fontCollection.Initialize(new CustomFontManagerImpl());
Assert.True(fontCollection.TryGetGlyphTypeface("Noto Mono", fontStyle, fontWeight, FontStretch.Normal, out var glyphTypeface)); Assert.True(fontCollection.TryGetGlyphTypeface("Noto Mono", fontStyle, fontWeight, FontStretch.Normal, out var glyphTypeface));
var actual = glyphTypeface.FamilyName; var actual = glyphTypeface.FamilyName;
@ -47,13 +41,12 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact] [Fact]
public void Should_Not_Get_Typeface_For_Invalid_FamilyName() 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 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.False(fontCollection.TryGetGlyphTypeface("ABC", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _)); Assert.False(fontCollection.TryGetGlyphTypeface("ABC", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _));
} }
@ -62,13 +55,12 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact] [Fact]
public void Should_Get_Typeface_For_Partial_FamilyName() 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); var fontCollection = new TestEmbeddedFontCollection(key, source);
fontCollection.Initialize(new CustomFontManagerImpl());
Assert.True(fontCollection.TryGetGlyphTypeface("T", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface)); Assert.True(fontCollection.TryGetGlyphTypeface("T", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface));
@ -79,13 +71,12 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact] [Fact]
public void Should_Get_Typeface_For_TypographicFamilyName() 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 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("Manrope", FontStyle.Normal, FontWeight.Light, FontStretch.Normal, out var glyphTypeface)); Assert.True(fontCollection.TryGetGlyphTypeface("Manrope", FontStyle.Normal, FontWeight.Light, FontStretch.Normal, out var glyphTypeface));
@ -102,13 +93,12 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact] [Fact]
public void Should_Cache_Synthetic_GlyphTypeface() 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 key = new Uri("fonts:testFonts", UriKind.Absolute);
var source = new Uri(s_fontAssets, UriKind.Absolute);
var fontCollection = new TestEmbeddedFontCollection(source, source, true);
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)); Assert.True(fontCollection.TryGetGlyphTypeface("Manrope", FontStyle.Normal, FontWeight.ExtraBlack, FontStretch.Normal, out var glyphTypeface));
@ -125,18 +115,19 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact] [Fact]
public void Should_Cache_Nearest_Match_For_MiSans() 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); 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)); 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)); 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.True(fontCollection.GlyphTypefaceCache.TryGetValue("MiSans", out var glyphTypefaces));
Assert.Equal(3, glyphTypefaces.Count); 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 = private const string NotoMono =
"resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"; "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")] [Win32Fact("Relies on some installed font family")]
public void Should_Cache_Nearest_Match() public void Should_Cache_Nearest_Match()
{ {
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{ {
var fontManager = FontManager.Current; var fontCollection = new TestSystemFontCollection(FontManager.Current.PlatformImpl);
var fontCollection = new TestSystemFontCollection(FontManager.Current);
Assert.True(fontCollection.TryGetGlyphTypeface("Arial", FontStyle.Normal, FontWeight.ExtraBlack, FontStretch.Normal, out var glyphTypeface)); 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 private class TestSystemFontCollection : SystemFontCollection
{ {
public TestSystemFontCollection(FontManager fontManager) : base(fontManager) public TestSystemFontCollection(IFontManagerImpl platformImpl) : base(platformImpl)
{ {
} }
public IDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> GlyphTypefaceCache => _glyphTypefaceCache; 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 }); 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.True(fontCollection.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match));
Assert.Equal("Arial", match.FontFamily.Name); Assert.Equal("Arial", match.FontFamily.Name);
@ -100,8 +76,6 @@ namespace Avalonia.Skia.UnitTests.Media
var fontCollection = new CustomizableFontCollection(key, key, null, new[] { ignorable }); var fontCollection = new CustomizableFontCollection(key, key, null, new[] { ignorable });
fontCollection.Initialize(FontManager.Current.PlatformImpl);
var typeface = new Typeface(ignorable); var typeface = new Typeface(ignorable);
var glyphTypeface = typeface.GlyphTypeface; 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;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Utilities; using Avalonia.Utilities;
using Xunit; using Xunit;
@ -1352,8 +1353,17 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
.With(renderInterface: new PlatformRenderInterface(), .With(renderInterface: new PlatformRenderInterface(),
textShaperImpl: new TextShaperImpl())); textShaperImpl: new TextShaperImpl()));
var customFontManagerImpl = new CustomFontManagerImpl();
AvaloniaLocator.CurrentMutable 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; return disposable;
} }

2
tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

@ -37,7 +37,7 @@ namespace Avalonia.UnitTests
} }
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, 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) foreach (var customTypeface in _customTypefaces)
{ {

Loading…
Cancel
Save