Browse Source

Introduce a universal IGlyphTypeface implementation that does not rely on any platform implementation

pull/20304/head
Benedikt Stebner 5 months ago
parent
commit
628d2e4ff8
  1. 13
      Avalonia.sln
  2. 378
      api/Avalonia.nupkg.xml
  3. 4
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  4. 4
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  5. 11
      src/Avalonia.Base/Media/FontManager.cs
  6. 19
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  7. 21
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  8. 2
      src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs
  9. 41
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  10. 336
      src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs
  11. 34
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapEncoding.cs
  12. 16
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat.cs
  13. 183
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs
  14. 316
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat4Table.cs
  15. 42
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapSubtableEntry.cs
  16. 166
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs
  17. 21
      src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs
  18. 118
      src/Avalonia.Base/Media/Fonts/Tables/HeadTable.cs
  19. 57
      src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeaderTable.cs
  20. 37
      src/Avalonia.Base/Media/Fonts/Tables/MaxpTable.cs
  21. 26
      src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalGlyphMetric.cs
  22. 113
      src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalMetricsTable.cs
  23. 24
      src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalGlyphMetric.cs
  24. 110
      src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalMetricsTable.cs
  25. 54
      src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs
  26. 51
      src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs
  27. 31
      src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs
  28. 2
      src/Avalonia.Base/Media/Fonts/Tables/PlatformID.cs
  29. 46
      src/Avalonia.Base/Media/Fonts/Tables/PostTable.cs
  30. 38
      src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs
  31. 128
      src/Avalonia.Base/Media/Fonts/Tables/VerticalHeaderTable.cs
  32. 407
      src/Avalonia.Base/Media/Fonts/UnmanagedFontMemory.cs
  33. 6
      src/Avalonia.Base/Media/GlyphMetrics.cs
  34. 13
      src/Avalonia.Base/Media/GlyphRun.cs
  35. 395
      src/Avalonia.Base/Media/GlyphTypeface.cs
  36. 169
      src/Avalonia.Base/Media/IGlyphTypeface.cs
  37. 39
      src/Avalonia.Base/Media/IGlyphTypeface2.cs
  38. 10
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  39. 2
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  40. 8
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  41. 4
      src/Avalonia.Base/Platform/IGlyphRunImpl.cs
  42. 11
      src/Avalonia.Base/Platform/ITextShaperImpl.cs
  43. 7
      src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs
  44. 9
      src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs
  45. 1
      src/Avalonia.Desktop/Avalonia.Desktop.csproj
  46. 22
      src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.Harfbuzz.csproj
  47. 27
      src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzApplicationExtensions.cs
  48. 34
      src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTextShaper.cs
  49. 67
      src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTypeface.cs
  50. 1
      src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
  51. 7
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  52. 367
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  53. 24
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  54. 6
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  55. 393
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  56. 10
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  57. 3
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  58. 81
      src/Skia/Avalonia.Skia/SkiaTypeface.cs
  59. 7
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  60. 111
      src/Windows/Avalonia.Direct2D1/Media/DWriteTypeface.cs
  61. 17
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  62. 10
      src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
  63. 216
      src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
  64. 204
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  65. 3
      tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
  66. 185
      tests/Avalonia.Base.UnitTests/Media/Fonts/UnmanagedFontMemoryTests.cs
  67. 4
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  68. 131
      tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs
  69. 1
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  70. 3
      tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
  71. 32
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  72. 3
      tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
  73. 3
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  74. 19
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  75. 20
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  76. 11
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  77. 28
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  78. 5
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  79. 3
      tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs
  80. 3
      tests/Avalonia.Controls.UnitTests/TimePickerTests.cs
  81. 3
      tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
  82. 5
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  83. 1
      tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj
  84. 4
      tests/Avalonia.Headless.UnitTests/TestApplication.cs
  85. 1
      tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj
  86. 1
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  87. 3
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs
  88. 3
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  89. 1
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs
  90. BIN
      tests/Avalonia.RenderTests/Assets/NotoSansTamil-Regular.ttf
  91. 6
      tests/Avalonia.RenderTests/Media/GlyphRunTests.cs
  92. 3
      tests/Avalonia.RenderTests/TestRenderHelper.cs
  93. 31
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  94. 6
      tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs
  95. 2
      tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs
  96. 8
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
  97. 1
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  98. 113
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/Tables/CmapTableTests.cs
  99. 5
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  100. 10
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

13
Avalonia.sln

@ -42,8 +42,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE
src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
src\Shared\StringCompatibilityExtensions.cs = src\Shared\StringCompatibilityExtensions.cs
src\Shared\StreamCompatibilityExtensions.cs = src\Shared\StreamCompatibilityExtensions.cs
src\Shared\StringCompatibilityExtensions.cs = src\Shared\StringCompatibilityExtensions.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}"
@ -92,6 +92,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
ProjectSection(SolutionItems) = preProject
build\AnalyzerProject.targets = build\AnalyzerProject.targets
build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props
build\Base.props = build\Base.props
build\Binding.props = build\Binding.props
@ -122,7 +123,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
build\WarnAsErrors.props = build\WarnAsErrors.props
build\XUnit.props = build\XUnit.props
build\AnalyzerProject.targets = build\AnalyzerProject.targets
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"
@ -302,6 +302,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.MacCatalyst"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.tvOS", "samples\ControlCatalog.tvOS\ControlCatalog.tvOS.csproj", "{14342787-B4EF-4076-8C91-BA6C523DE8DF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Harfbuzz", "Harfbuzz", "{7670D720-6E84-4AFC-8331-A5C399481905}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Harfbuzz", "src\Harfbuzz\Avalonia.Harfbuzz\Avalonia.Harfbuzz.csproj", "{E2BFA463-6402-4EF8-8945-FD9A10A914D1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -704,6 +708,10 @@ Global
{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Release|Any CPU.Build.0 = Release|Any CPU
{E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -793,6 +801,7 @@ Global
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{DE3C28DD-B602-4750-831D-345102A54CA0} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{14342787-B4EF-4076-8C91-BA6C523DE8DF} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{E2BFA463-6402-4EF8-8945-FD9A10A914D1} = {7670D720-6E84-4AFC-8331-A5C399481905}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

378
api/Avalonia.nupkg.xml

@ -1,6 +1,168 @@
<?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.Media.IGlyphTypeface.get_GlyphCount</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.IGlyphTypeface.GetGlyph(System.UInt32)</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.IGlyphTypeface.GetGlyphAdvance(System.UInt16)</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.IGlyphTypeface.GetGlyphAdvances(System.ReadOnlySpan{System.UInt16})</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.IGlyphTypeface.GetGlyphs(System.ReadOnlySpan{System.UInt32})</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.IGlyphTypeface.TryGetGlyph(System.UInt32,System.UInt16@)</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.IGlyphTypeface.TryGetTable(System.UInt32,System.Byte[]@)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IGlyphRunImpl.get_GlyphTypeface</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.IGlyphTypeface.get_GlyphCount</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.IGlyphTypeface.GetGlyph(System.UInt32)</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.IGlyphTypeface.GetGlyphAdvance(System.UInt16)</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.IGlyphTypeface.GetGlyphAdvances(System.ReadOnlySpan{System.UInt16})</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.IGlyphTypeface.GetGlyphs(System.ReadOnlySpan{System.UInt32})</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.IGlyphTypeface.TryGetGlyph(System.UInt32,System.UInt16@)</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.IGlyphTypeface.TryGetTable(System.UInt32,System.Byte[]@)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IGlyphRunImpl.get_GlyphTypeface</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.IGlyphTypeface.get_GlyphCount</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.IGlyphTypeface.GetGlyph(System.UInt32)</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.IGlyphTypeface.GetGlyphAdvance(System.UInt16)</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.IGlyphTypeface.GetGlyphAdvances(System.ReadOnlySpan{System.UInt16})</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.IGlyphTypeface.GetGlyphs(System.ReadOnlySpan{System.UInt32})</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.IGlyphTypeface.TryGetGlyph(System.UInt32,System.UInt16@)</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.IGlyphTypeface.TryGetTable(System.UInt32,System.Byte[]@)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IGlyphRunImpl.get_GlyphTypeface</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)</Target>
@ -25,12 +187,84 @@
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryGetFamilyTypefaces(System.String,System.Collections.Generic.IReadOnlyList{Avalonia.Media.Typeface}@)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.IPlatformTypeface@)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.ITextShaperImpl.CreateTypeface(Avalonia.Media.IGlyphTypeface)</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.CharacterToGlyphMap</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.FaceNames</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.FamilyNames</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.GlyphCount</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.PlatformTypeface</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.SupportedFeatures</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.TextShaperTypeface</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.TypographicFamilyName</Target>
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64)</Target>
@ -73,12 +307,84 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryGetFamilyTypefaces(System.String,System.Collections.Generic.IReadOnlyList{Avalonia.Media.Typeface}@)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.IPlatformTypeface@)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.ITextShaperImpl.CreateTypeface(Avalonia.Media.IGlyphTypeface)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.CharacterToGlyphMap</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.FaceNames</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.FamilyNames</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.GlyphCount</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.PlatformTypeface</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.SupportedFeatures</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.TextShaperTypeface</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.TypographicFamilyName</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64)</Target>
@ -121,12 +427,84 @@
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryGetFamilyTypefaces(System.String,System.Collections.Generic.IReadOnlyList{Avalonia.Media.Typeface}@)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.IPlatformTypeface@)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.ITextShaperImpl.CreateTypeface(Avalonia.Media.IGlyphTypeface)</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.CharacterToGlyphMap</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.FaceNames</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.FamilyNames</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.GlyphCount</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.PlatformTypeface</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.SupportedFeatures</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.TextShaperTypeface</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IGlyphTypeface.TypographicFamilyName</Target>
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64)</Target>

4
samples/RenderDemo/Pages/CustomSkiaPage.cs

@ -1,6 +1,5 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
@ -9,7 +8,6 @@ using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using Avalonia.Threading;
using Avalonia.Utilities;
using SkiaSharp;
namespace RenderDemo.Pages
@ -21,7 +19,7 @@ namespace RenderDemo.Pages
{
ClipToBounds = true;
var text = "Current rendering API is not Skia";
var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.GetGlyph(ch)).ToArray();
var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.CharacterToGlyphMap[ch]).ToArray();
_noSkia = new GlyphRun(Typeface.Default.GlyphTypeface, 12, text.AsMemory(), glyphs);
}

4
samples/RenderDemo/Pages/GlyphRunPage.xaml.cs

@ -69,7 +69,7 @@ namespace RenderDemo.Pages
_fontSize += _direction;
_glyphIndices[0] = _glyphTypeface.GetGlyph(c);
_glyphIndices[0] = _glyphTypeface.CharacterToGlyphMap[c];
_characters[0] = c;
@ -128,7 +128,7 @@ namespace RenderDemo.Pages
_fontSize += _direction;
_glyphIndices[0] = _glyphTypeface.GetGlyph(c);
_glyphIndices[0] = _glyphTypeface.CharacterToGlyphMap[c];
_characters[0] = c;

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

@ -262,7 +262,7 @@ namespace Avalonia.Media
{
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.CharacterToGlyphMap.TryGetValue(codepoint, out _))
{
return true;
}
@ -289,6 +289,11 @@ namespace Avalonia.Media
if (TryGetFontCollection(source, out var fontCollection) &&
fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{
if (typeface.FontFamily.Name == DefaultFontFamily.Name && i + 1 < compositeKey.Keys.Count)
{
continue;
}
return true;
}
}
@ -306,8 +311,8 @@ namespace Avalonia.Media
}
}
//Try to find a match with the system font manager
return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface);
//Try to find a match with the system fonts
return SystemFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily?.Name, culture, out typeface);
}
internal IReadOnlyList<Typeface> GetFamilyTypefaces(FontFamily fontFamily)

19
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@ -105,22 +105,15 @@ namespace Avalonia.Media.Fonts
private void AddGlyphTypeface(IGlyphTypeface glyphTypeface)
{
if (glyphTypeface is IGlyphTypeface2 glyphTypeface2)
//Add the TypographicFamilyName to the cache
if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName))
{
//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);
}
AddGlyphTypefaceByFamilyName(glyphTypeface.TypographicFamilyName, glyphTypeface);
}
else
foreach (var kvp in glyphTypeface.FamilyNames)
{
AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface);
AddGlyphTypefaceByFamilyName(kvp.Value, glyphTypeface);
}
return;

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

@ -33,7 +33,7 @@ namespace Avalonia.Media.Fonts
{
if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
{
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
if (glyphTypeface.CharacterToGlyphMap.TryGetValue(codepoint, out _))
{
match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), style, weight, stretch);
@ -55,7 +55,7 @@ namespace Avalonia.Media.Fonts
if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
{
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
if (glyphTypeface.CharacterToGlyphMap.TryGetValue(codepoint, out _))
{
match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), style, weight, stretch);
@ -94,36 +94,31 @@ namespace Avalonia.Media.Fonts
return false;
}
if (glyphTypeface is not IGlyphTypeface2 glyphTypeface2)
{
return false;
}
var fontSimulations = FontSimulations.None;
if (style != FontStyle.Normal && glyphTypeface2.Style != style)
if (style != FontStyle.Normal && glyphTypeface.Style != style)
{
fontSimulations |= FontSimulations.Oblique;
}
if ((int)weight >= 600 && glyphTypeface2.Weight < weight)
if ((int)weight >= 600 && glyphTypeface.Weight < weight)
{
fontSimulations |= FontSimulations.Bold;
}
if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream))
if (fontSimulations != FontSimulations.None && glyphTypeface.PlatformTypeface.TryGetStream(out var stream))
{
using (stream)
{
if (fontManager.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface))
{
//Add the TypographicFamilyName to the cache
if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName))
{
AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, syntheticGlyphTypeface);
AddGlyphTypefaceByFamilyName(glyphTypeface.TypographicFamilyName, syntheticGlyphTypeface);
}
foreach (var kvp in glyphTypeface2.FamilyNames)
foreach (var kvp in glyphTypeface.FamilyNames)
{
AddGlyphTypefaceByFamilyName(kvp.Value, syntheticGlyphTypeface);
}

2
src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs

@ -2,7 +2,7 @@
namespace Avalonia.Media.Fonts
{
internal readonly record struct OpenTypeTag
public readonly record struct OpenTypeTag
{
public static readonly OpenTypeTag None = new OpenTypeTag(0, 0, 0, 0);
public static readonly OpenTypeTag Max = new OpenTypeTag(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);

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

@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Avalonia.Platform;
@ -135,9 +136,9 @@ namespace Avalonia.Media.Fonts
}
//Add TypographicFamilyName to the cache
if (glyphTypeface is IGlyphTypeface2 glyphTypeface2 && !string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName))
{
AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, glyphTypeface);
AddGlyphTypefaceByFamilyName(glyphTypeface.TypographicFamilyName, glyphTypeface);
}
AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface);
@ -161,13 +162,41 @@ namespace Avalonia.Media.Fonts
}
}
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces) =>
_fontManager.PlatformImpl.TryGetFamilyTypefaces(familyName, out familyTypefaces);
public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName, CultureInfo? culture, out Typeface typeface)
{
familyTypefaces = null;
if (base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out typeface))
{
return true;
}
if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2)
if (_fontManager.PlatformImpl.TryMatchCharacter(codepoint, style, weight, stretch, culture, out var match))
{
return fontManagerImpl2.TryGetFamilyTypefaces(familyName, out familyTypefaces);
var createdKey =
new FontCollectionKey(match.Style, match.Weight, match.Stretch);
var glyphTypeface = new GlyphTypeface(match, FontSimulations.None);
if (_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
{
if (!glyphTypefaces.TryAdd(createdKey, glyphTypeface))
{
return false;
}
}
else
{
if (!_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>() { [createdKey] = glyphTypeface }))
{
return false;
}
}
typeface = new Typeface(glyphTypeface.FamilyName, match.Style, match.Weight, match.Stretch);
return true;
}
return false;

336
src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs

@ -5,104 +5,92 @@
using System;
using System.Buffers.Binary;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
namespace Avalonia.Media.Fonts.Tables
{
/// <summary>
/// BinaryReader using big-endian encoding.
/// BinaryReader using big-endian encoding for ReadOnlySpan&lt;byte&gt;.
/// </summary>
[DebuggerDisplay("Start: {StartOfStream}, Position: {BaseStream.Position}")]
internal class BigEndianBinaryReader : IDisposable
[DebuggerDisplay("Start: {StartOfSpan}, Position: {Position}")]
internal ref struct BigEndianBinaryReader
{
/// <summary>
/// Buffer used for temporary storage before conversion into primitives
/// </summary>
private readonly byte[] _buffer = new byte[16];
private readonly bool _leaveOpen;
private readonly ReadOnlySpan<byte> _span;
private int _position;
private readonly int _startOfSpan;
/// <summary>
/// Initializes a new instance of the <see cref="BigEndianBinaryReader" /> class.
/// Constructs a new binary reader with the given bit converter, reading
/// to the given stream, using the given encoding.
/// </summary>
/// <param name="stream">Stream to read data from</param>
/// <param name="leaveOpen">if set to <c>true</c> [leave open].</param>
public BigEndianBinaryReader(Stream stream, bool leaveOpen)
/// <param name="span">Span to read data from</param>
public BigEndianBinaryReader(ReadOnlySpan<byte> span)
{
BaseStream = stream;
StartOfStream = stream.Position;
_leaveOpen = leaveOpen;
_span = span;
_position = 0;
_startOfSpan = 0;
}
private long StartOfStream { get; }
private readonly int StartOfSpan => _startOfSpan;
/// <summary>
/// Gets the underlying stream of the EndianBinaryReader.
/// Gets the current position in the span.
/// </summary>
public Stream BaseStream { get; }
public readonly int Position => _position;
/// <summary>
/// Seeks within the stream.
/// Seeks within the span.
/// </summary>
/// <param name="offset">Offset to seek to.</param>
/// <param name="origin">Origin of seek operation. If SeekOrigin.Begin, the offset will be set to the start of stream position.</param>
public void Seek(long offset, SeekOrigin origin)
public void Seek(int offset)
{
// If SeekOrigin.Begin, the offset will be set to the start of stream position.
if (origin == SeekOrigin.Begin)
int absoluteOffset = _startOfSpan + offset;
if (offset < 0 || absoluteOffset > _span.Length)
{
offset += StartOfStream;
throw new ArgumentOutOfRangeException(nameof(offset));
}
BaseStream.Seek(offset, origin);
_position = absoluteOffset;
}
/// <summary>
/// Reads a single byte from the stream.
/// </summary>
/// <returns>The byte read</returns>
public byte ReadByte()
{
ReadInternal(_buffer, 1);
return _buffer[0];
EnsureAvailable(1);
return _span[_position++];
}
/// <summary>
/// Reads a single signed byte from the stream.
/// </summary>
/// <returns>The byte read</returns>
public sbyte ReadSByte()
{
ReadInternal(_buffer, 1);
return unchecked((sbyte)_buffer[0]);
EnsureAvailable(1);
return unchecked((sbyte)_span[_position++]);
}
public float ReadF2dot14()
{
const float f2Dot14ToFloat = 16384.0f;
return ReadInt16() / f2Dot14ToFloat;
}
/// <summary>
/// Reads a 16-bit signed integer from the stream, using the bit converter
/// for this reader. 2 bytes are read.
/// </summary>
/// <returns>The 16-bit integer read</returns>
public short ReadInt16()
{
ReadInternal(_buffer, 2);
EnsureAvailable(2);
return BinaryPrimitives.ReadInt16BigEndian(_buffer);
short value = BinaryPrimitives.ReadInt16BigEndian(_span.Slice(_position, 2));
_position += 2;
return value;
}
public TEnum ReadInt16<TEnum>()
where TEnum : struct, Enum
{
TryConvert(ReadUInt16(), out TEnum value);
return value;
}
@ -112,77 +100,64 @@ namespace Avalonia.Media.Fonts.Tables
public ushort ReadUFWORD() => ReadUInt16();
/// <summary>
/// Reads a fixed 32-bit value from the stream.
/// 4 bytes are read.
/// </summary>
/// <returns>The 32-bit value read.</returns>
public float ReadFixed()
{
ReadInternal(_buffer, 4);
return BinaryPrimitives.ReadInt32BigEndian(_buffer) / 65536F;
EnsureAvailable(4);
float value = BinaryPrimitives.ReadInt32BigEndian(_span.Slice(_position, 4)) / 65536F;
_position += 4;
return value;
}
/// <summary>
/// Reads a 32-bit signed integer from the stream, using the bit converter
/// for this reader. 4 bytes are read.
/// </summary>
/// <returns>The 32-bit integer read</returns>
public int ReadInt32()
{
ReadInternal(_buffer, 4);
EnsureAvailable(4);
int value = BinaryPrimitives.ReadInt32BigEndian(_span.Slice(_position, 4));
return BinaryPrimitives.ReadInt32BigEndian(_buffer);
_position += 4;
return value;
}
/// <summary>
/// Reads a 64-bit signed integer from the stream.
/// 8 bytes are read.
/// </summary>
/// <returns>The 64-bit integer read.</returns>
public long ReadInt64()
{
ReadInternal(_buffer, 8);
EnsureAvailable(8);
return BinaryPrimitives.ReadInt64BigEndian(_buffer);
long value = BinaryPrimitives.ReadInt64BigEndian(_span.Slice(_position, 8));
_position += 8;
return value;
}
/// <summary>
/// Reads a 16-bit unsigned integer from the stream.
/// 2 bytes are read.
/// </summary>
/// <returns>The 16-bit unsigned integer read.</returns>
public ushort ReadUInt16()
{
ReadInternal(_buffer, 2);
EnsureAvailable(2);
ushort value = BinaryPrimitives.ReadUInt16BigEndian(_span.Slice(_position, 2));
return BinaryPrimitives.ReadUInt16BigEndian(_buffer);
_position += 2;
return value;
}
/// <summary>
/// Reads a 16-bit unsigned integer from the stream representing an offset position.
/// 2 bytes are read.
/// </summary>
/// <returns>The 16-bit unsigned integer read.</returns>
public ushort ReadOffset16() => ReadUInt16();
public TEnum ReadUInt16<TEnum>()
where TEnum : struct, Enum
{
TryConvert(ReadUInt16(), out TEnum value);
return value;
}
/// <summary>
/// Reads array of 16-bit unsigned integers from the stream.
/// </summary>
/// <param name="length">The length.</param>
/// <returns>
/// The 16-bit unsigned integer read.
/// </returns>
public ushort[] ReadUInt16Array(int length)
{
ushort[] data = new ushort[length];
for (int i = 0; i < length; i++)
{
data[i] = ReadUInt16();
@ -191,10 +166,6 @@ namespace Avalonia.Media.Fonts.Tables
return data;
}
/// <summary>
/// Reads array of 16-bit unsigned integers from the stream to the buffer.
/// </summary>
/// <param name="buffer">The buffer to read to.</param>
public void ReadUInt16Array(Span<ushort> buffer)
{
for (int i = 0; i < buffer.Length; i++)
@ -203,16 +174,10 @@ namespace Avalonia.Media.Fonts.Tables
}
}
/// <summary>
/// Reads array or 32-bit unsigned integers from the stream.
/// </summary>
/// <param name="length">The length.</param>
/// <returns>
/// The 32-bit unsigned integer read.
/// </returns>
public uint[] ReadUInt32Array(int length)
{
uint[] data = new uint[length];
for (int i = 0; i < length; i++)
{
data[i] = ReadUInt32();
@ -225,21 +190,15 @@ namespace Avalonia.Media.Fonts.Tables
{
byte[] data = new byte[length];
ReadInternal(data, length);
ReadBytesInternal(data, length);
return data;
}
/// <summary>
/// Reads array of 16-bit unsigned integers from the stream.
/// </summary>
/// <param name="length">The length.</param>
/// <returns>
/// The 16-bit signed integer read.
/// </returns>
public short[] ReadInt16Array(int length)
{
short[] data = new short[length];
for (int i = 0; i < length; i++)
{
data[i] = ReadInt16();
@ -248,10 +207,6 @@ namespace Avalonia.Media.Fonts.Tables
return data;
}
/// <summary>
/// Reads an array of 16-bit signed integers from the stream to the buffer.
/// </summary>
/// <param name="buffer">The buffer to read to.</param>
public void ReadInt16Array(Span<short> buffer)
{
for (int i = 0; i < buffer.Length; i++)
@ -260,110 +215,104 @@ namespace Avalonia.Media.Fonts.Tables
}
}
/// <summary>
/// Reads a 8-bit unsigned integer from the stream, using the bit converter
/// for this reader. 1 bytes are read.
/// </summary>
/// <returns>The 8-bit unsigned integer read.</returns>
public byte ReadUInt8()
{
ReadInternal(_buffer, 1);
return _buffer[0];
EnsureAvailable(1);
return _span[_position++];
}
/// <summary>
/// Reads a 24-bit unsigned integer from the stream, using the bit converter
/// for this reader. 3 bytes are read.
/// </summary>
/// <returns>The 24-bit unsigned integer read.</returns>
public int ReadUInt24()
{
byte highByte = ReadByte();
return (highByte << 16) | ReadUInt16();
}
/// <summary>
/// Reads a 32-bit unsigned integer from the stream, using the bit converter
/// for this reader. 4 bytes are read.
/// </summary>
/// <returns>The 32-bit unsigned integer read.</returns>
public uint ReadUInt32()
{
ReadInternal(_buffer, 4);
EnsureAvailable(4);
uint value = BinaryPrimitives.ReadUInt32BigEndian(_span.Slice(_position, 4));
_position += 4;
return BinaryPrimitives.ReadUInt32BigEndian(_buffer);
return value;
}
/// <summary>
/// Reads a 32-bit unsigned integer from the stream representing an offset position.
/// 4 bytes are read.
/// </summary>
/// <returns>The 32-bit unsigned integer read.</returns>
public uint ReadOffset32() => ReadUInt32();
/// <summary>
/// Reads the specified number of bytes, returning them in a new byte array.
/// If not enough bytes are available before the end of the stream, this
/// method will return what is available.
/// </summary>
/// <param name="count">The number of bytes to read.</param>
/// <returns>The bytes read.</returns>
public byte[] ReadBytes(int count)
{
byte[] ret = new byte[count];
int index = 0;
while (index < count)
{
int read = BaseStream.Read(ret, index, count - index);
int available = Math.Min(count, _span.Length - _position);
// Stream has finished half way through. That's fine, return what we've got.
if (read == 0)
{
byte[] copy = new byte[index];
Buffer.BlockCopy(ret, 0, copy, 0, index);
return copy;
}
byte[] ret = new byte[available];
index += read;
}
ReadBytesInternal(ret, available);
return ret;
}
/// <summary>
/// Reads a string of a specific length, which specifies the number of bytes
/// to read from the stream. These bytes are then converted into a string with
/// the encoding for this reader.
/// </summary>
/// <param name="bytesToRead">The bytes to read.</param>
/// <param name="encoding">The encoding.</param>
/// <returns>
/// The string read from the stream.
/// </returns>
public string ReadString(int bytesToRead, Encoding encoding)
{
byte[] data = new byte[bytesToRead];
ReadInternal(data, bytesToRead);
return encoding.GetString(data, 0, data.Length);
EnsureAvailable(bytesToRead);
#if NETSTANDARD2_0
byte[] buffer = System.Buffers.ArrayPool<byte>.Shared.Rent(bytesToRead);
try
{
_span.Slice(_position, bytesToRead).CopyTo(buffer);
string result = encoding.GetString(buffer, 0, bytesToRead);
_position += bytesToRead;
return result;
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(buffer);
}
#else
string result = encoding.GetString(_span.Slice(_position, bytesToRead));
_position += bytesToRead;
return result;
#endif
}
/// <summary>
/// Reads the uint32 string.
/// </summary>
/// <returns>a 4 character long UTF8 encoded string.</returns>
public string ReadTag()
{
ReadInternal(_buffer, 4);
EnsureAvailable(4);
#if NETSTANDARD2_0
byte[] buffer = System.Buffers.ArrayPool<byte>.Shared.Rent(4);
try
{
_span.Slice(_position, 4).CopyTo(buffer);
string tag = Encoding.UTF8.GetString(buffer, 0, 4);
return Encoding.UTF8.GetString(_buffer, 0, 4);
_position += 4;
return tag;
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(buffer);
}
#else
string tag = Encoding.UTF8.GetString(_span.Slice(_position, 4));
_position += 4;
return tag;
#endif
}
/// <summary>
/// Reads an offset consuming the given nuber of bytes.
/// </summary>
/// <param name="size">The offset size in bytes.</param>
/// <returns>The 32-bit signed integer representing the offset.</returns>
/// <exception cref="InvalidOperationException">Size is not in range.</exception>
public int ReadOffset(int size)
=> size switch
{
@ -374,33 +323,20 @@ namespace Avalonia.Media.Fonts.Tables
_ => throw new InvalidOperationException(),
};
/// <summary>
/// Reads the given number of bytes from the stream, throwing an exception
/// if they can't all be read.
/// </summary>
/// <param name="data">Buffer to read into.</param>
/// <param name="size">Number of bytes to read.</param>
private void ReadInternal(byte[] data, int size)
private void ReadBytesInternal(byte[] data, int size)
{
int index = 0;
EnsureAvailable(size);
while (index < size)
{
int read = BaseStream.Read(data, index, size - index);
if (read == 0)
{
throw new EndOfStreamException($"End of stream reached with {size - index} byte{(size - index == 1 ? "s" : string.Empty)} left to read.");
}
_span.Slice(_position, size).CopyTo(data);
index += read;
}
_position += size;
}
public void Dispose()
private readonly void EnsureAvailable(int size)
{
if (!_leaveOpen)
if (_position + size > _span.Length)
{
BaseStream?.Dispose();
throw new InvalidOperationException($"End of span reached with {size - (_span.Length - _position)} byte{(size - (_span.Length - _position) == 1 ? "s" : string.Empty)} left to read.");
}
}

34
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapEncoding.cs

@ -0,0 +1,34 @@
namespace Avalonia.Media.Fonts.Tables.Cmap
{
// Encoding IDs. The meaning depends on the platform; common values are listed here.
internal enum CmapEncoding : ushort
{
// Unicode platform encodings
Unicode_1_0 = 0,
Unicode_1_1 = 1,
Unicode_ISO_10646 = 2,
Unicode_2_0_BMP = 3,
Unicode_2_0_full = 4,
// Macintosh encodings (selected)
Macintosh_Roman = 0,
Macintosh_Japanese = 1,
Macintosh_ChineseTraditional = 2,
Macintosh_Korean = 3,
Macintosh_Arabic = 4,
Macintosh_Hebrew = 5,
Macintosh_Greek = 6,
Macintosh_Russian = 7,
Macintosh_RSymbol = 8,
// Microsoft encodings
Microsoft_Symbol = 0,
Microsoft_UnicodeBMP = 1, // UCS-2 / UTF-16 (BMP)
Microsoft_ShiftJIS = 2,
Microsoft_PRChina = 3,
Microsoft_Big5 = 4,
Microsoft_Wansung = 5,
Microsoft_Johab = 6,
Microsoft_UCS4 = 10 // UTF-32 (format 12)
}
}

16
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat.cs

@ -0,0 +1,16 @@
namespace Avalonia.Media.Fonts.Tables.Cmap
{
// cmap format types
internal enum CmapFormat : ushort
{
Format0 = 0, // Byte encoding table
Format2 = 2, // High-byte mapping through table (multi-byte charsets)
Format4 = 4, // Segment mapping to delta values (most common)
Format6 = 6, // Trimmed table mapping
Format8 = 8, // Mixed 16/32-bit coverage
Format10 = 10, // Trimmed array mapping (32-bit)
Format12 = 12, // Segmented coverage (32-bit)
Format13 = 13, // Many-to-one mappings
Format14 = 14, // Unicode Variation Sequences
}
}

183
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs

@ -0,0 +1,183 @@
using System;
using System.Buffers.Binary;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
namespace Avalonia.Media.Fonts.Tables.Cmap
{
internal sealed class CmapFormat12Table : IReadOnlyDictionary<int, ushort>
{
private readonly ReadOnlyMemory<byte> _table;
private readonly int _groupCount;
private readonly ReadOnlyMemory<byte> _groups;
private int? _count;
public CmapFormat12Table(ReadOnlyMemory<byte> table)
{
var reader = new BigEndianBinaryReader(table.Span);
ushort format = reader.ReadUInt16();
Debug.Assert(format == 12, "Format must be 12.");
ushort reserved = reader.ReadUInt16();
Debug.Assert(reserved == 0, "Reserved field must be 0.");
uint length = reader.ReadUInt32();
_table = table.Slice(0, (int)length);
uint language = reader.ReadUInt32();
_groupCount = (int)reader.ReadUInt32();
int groupsOffset = reader.Position;
int groupsLength = _groupCount * 12;
Debug.Assert(length >= groupsOffset + groupsLength, "Length must cover all groups.");
_groups = _table.Slice(groupsOffset, groupsLength);
}
private static uint ReadUInt32BE(ReadOnlyMemory<byte> mem, int groupIndex, int fieldOffset)
{
var span = mem.Span;
int byteIndex = groupIndex * 12 + fieldOffset;
return BinaryPrimitives.ReadUInt32BigEndian(span.Slice(byteIndex, 4));
}
// Binary search to find the group containing the code point
private int FindGroupIndex(int codePoint)
{
int lo = 0;
int hi = _groupCount - 1;
while (lo <= hi)
{
int mid = (lo + hi) >> 1;
uint start = ReadUInt32BE(_groups, mid, 0);
uint end = ReadUInt32BE(_groups, mid, 4);
if (codePoint < start)
{
hi = mid - 1;
}
else if (codePoint > end)
{
lo = mid + 1;
}
else
{
return mid;
}
}
// Not found
return -1;
}
public ushort this[int codePoint]
{
get
{
int groupIndex = FindGroupIndex(codePoint);
if (groupIndex < 0)
{
return 0;
}
uint start = ReadUInt32BE(_groups, groupIndex, 0);
uint startGlyph = ReadUInt32BE(_groups, groupIndex, 8);
// Calculate glyph index
return (ushort)(startGlyph + (codePoint - start));
}
}
public int Count
{
get
{
if (_count.HasValue)
{
return _count.Value;
}
long total = 0;
for (int g = 0; g < _groupCount; g++)
{
uint start = ReadUInt32BE(_groups, g, 0);
uint end = ReadUInt32BE(_groups, g, 4);
total += (end - start + 1);
}
_count = (int)total;
return _count.Value;
}
}
public IEnumerable<int> Keys
{
get
{
for (int g = 0; g < _groupCount; g++)
{
uint start = ReadUInt32BE(_groups, g, 0);
uint end = ReadUInt32BE(_groups, g, 4);
for (uint cp = start; cp <= end; cp++)
{
yield return (int)cp;
}
}
}
}
public IEnumerable<ushort> Values
{
get
{
for (int g = 0; g < _groupCount; g++)
{
uint start = ReadUInt32BE(_groups, g, 0);
uint end = ReadUInt32BE(_groups, g, 4);
uint startGlyph = ReadUInt32BE(_groups, g, 8);
for (uint cp = start; cp <= end; cp++)
{
yield return (ushort)(startGlyph + (cp - start));
}
}
}
}
public bool ContainsKey(int key) => this[key] != 0;
public bool TryGetValue(int key, out ushort value)
{
value = this[key];
return value != 0;
}
public IEnumerator<KeyValuePair<int, ushort>> GetEnumerator()
{
for (int g = 0; g < _groupCount; g++)
{
uint start = ReadUInt32BE(_groups, g, 0);
uint end = ReadUInt32BE(_groups, g, 4);
uint startGlyph = ReadUInt32BE(_groups, g, 8);
for (uint cp = start; cp <= end; cp++)
{
yield return new KeyValuePair<int, ushort>((int)cp, (ushort)(startGlyph + (cp - start)));
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

316
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat4Table.cs

@ -0,0 +1,316 @@
using System;
using System.Buffers.Binary;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
namespace Avalonia.Media.Fonts.Tables.Cmap
{
internal sealed class CmapFormat4Table : IReadOnlyDictionary<int, ushort>
{
private readonly ReadOnlyMemory<byte> _table;
private readonly int _segCount;
private readonly ReadOnlyMemory<byte> _endCodes;
private readonly ReadOnlyMemory<byte> _startCodes;
private readonly ReadOnlyMemory<byte> _idDeltas;
private readonly ReadOnlyMemory<byte> _idRangeOffsets;
private readonly ReadOnlyMemory<byte> _glyphIdArray;
private int? _count;
public CmapFormat4Table(ReadOnlyMemory<byte> table)
{
var reader = new BigEndianBinaryReader(table.Span);
ushort format = reader.ReadUInt16(); // must be 4
Debug.Assert(format == 4, "Format must be 4.");
ushort length = reader.ReadUInt16(); // length in bytes of this subtable
_table = table.Slice(0, length);
ushort language = reader.ReadUInt16(); // language code, 0 for non-language-specific
ushort segCountX2 = reader.ReadUInt16(); // 2 * segCount
_segCount = segCountX2 / 2;
ushort searchRange = reader.ReadUInt16(); // searchRange = 2 * (2^floor(log2(segCount)))
ushort entrySelector = reader.ReadUInt16(); // entrySelector = log2(searchRange/2)
ushort rangeShift = reader.ReadUInt16(); // rangeShift = segCountX2 - searchRange
// Spec sanity checks
Debug.Assert(searchRange == (ushort)(2 * (1 << (int)Math.Floor(Math.Log(_segCount, 2)))),
"searchRange must equal 2 * (2^floor(log2(segCount))).");
Debug.Assert(entrySelector == (ushort)Math.Floor(Math.Log(_segCount, 2)),
"entrySelector must equal log2(searchRange/2).");
Debug.Assert(rangeShift == (ushort)(segCountX2 - searchRange),
"rangeShift must equal segCountX2 - searchRange.");
// Compute offsets
int endCodeOffset = reader.Position;
int startCodeOffset = endCodeOffset + _segCount * 2 + 2; // + reservedPad
int idDeltaOffset = startCodeOffset + _segCount * 2; // after startCodes
int idRangeOffsetOffset = idDeltaOffset + _segCount * 2; // after idDeltas
int glyphIdArrayOffset = idRangeOffsetOffset + _segCount * 2; // after idRangeOffsets
// Ensure declared length is consistent
Debug.Assert(length >= glyphIdArrayOffset,
"Subtable length must be at least large enough to contain glyphIdArray.");
// Slice directly
_endCodes = _table.Slice(endCodeOffset, _segCount * 2);
_startCodes = _table.Slice(startCodeOffset, _segCount * 2);
_idDeltas = _table.Slice(idDeltaOffset, _segCount * 2);
_idRangeOffsets = _table.Slice(idRangeOffsetOffset, _segCount * 2);
int glyphCount = (length - glyphIdArrayOffset) / 2;
Debug.Assert(glyphCount >= 0, "GlyphIdArray length must not be negative.");
_glyphIdArray = _table.Slice(glyphIdArrayOffset, glyphCount * 2);
}
// Reads a big-endian UInt16 from the specified word index in the given memory
private static ushort ReadUInt16BE(ReadOnlyMemory<byte> mem, int wordIndex)
{
var span = mem.Span;
int byteIndex = wordIndex * 2;
// Ensure we don't go out of bounds
return BinaryPrimitives.ReadUInt16BigEndian(span.Slice(byteIndex, 2));
}
public int Count
{
get
{
if (_count.HasValue)
{
return _count.Value;
}
int count = 0;
for (int seg = 0; seg < _segCount; seg++)
{
// Get start and end of segment
int start = ReadUInt16BE(_startCodes, seg);
int end = ReadUInt16BE(_endCodes, seg);
for (int cp = start; cp <= end; cp++)
{
// Only count if maps to non-zero glyph
if (this[cp] != 0)
{
count++;
}
}
}
_count = count;
return count;
}
}
public ushort this[int codePoint]
{
get
{
// Find the segment containing the codePoint
int segmentIndex = FindSegmentIndex(codePoint);
if (segmentIndex < 0)
{
return 0;
}
ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets, segmentIndex);
ushort idDelta = ReadUInt16BE(_idDeltas, segmentIndex);
// If idRangeOffset is 0, glyphId = (codePoint + idDelta) % 65536
if (idRangeOffset == 0)
{
return (ushort)((codePoint + idDelta) & 0xFFFF);
}
else
{
int start = ReadUInt16BE(_startCodes, segmentIndex);
int ro = idRangeOffset / 2; // words
// The index into the glyphIdArray
int idx = (codePoint - start) + ro - (_segCount - segmentIndex);
// Ensure index is within bounds of glyphIdArray
int glyphArrayWords = _glyphIdArray.Length / 2;
if ((uint)idx < (uint)glyphArrayWords)
{
ushort glyphId = ReadUInt16BE(_glyphIdArray, idx);
// If glyphId is not 0, apply idDelta
if (glyphId != 0)
{
glyphId = (ushort)((glyphId + idDelta) & 0xFFFF);
}
return glyphId;
}
}
// Not found or maps to missing glyph
return 0;
}
}
public bool ContainsKey(int key) => this[key] != 0;
public bool TryGetValue(int key, out ushort value)
{
value = this[key];
return value != 0;
}
public IEnumerable<int> Keys
{
get
{
for (int seg = 0; seg < _segCount; seg++)
{
int start = ReadUInt16BE(_startCodes, seg);
int end = ReadUInt16BE(_endCodes, seg);
for (int cp = start; cp <= end; cp++)
{
ushort gid = ResolveGlyph(seg, cp);
// Only yield code points that map to non-zero glyphs
if (gid != 0)
{
yield return cp;
}
}
}
}
}
public IEnumerable<ushort> Values
{
get
{
for (int seg = 0; seg < _segCount; seg++)
{
int start = ReadUInt16BE(_startCodes, seg);
int end = ReadUInt16BE(_endCodes, seg);
for (int cp = start; cp <= end; cp++)
{
ushort gid = ResolveGlyph(seg, cp);
// Only yield non-zero glyphs
if (gid != 0)
{
yield return gid;
}
}
}
}
}
public IEnumerator<KeyValuePair<int, ushort>> GetEnumerator()
{
for (int seg = 0; seg < _segCount; seg++)
{
int start = ReadUInt16BE(_startCodes, seg);
int end = ReadUInt16BE(_endCodes, seg);
for (int cp = start; cp <= end; cp++)
{
ushort gid = ResolveGlyph(seg, cp);
// Only yield mappings to non-zero glyphs
if (gid != 0)
{
yield return new KeyValuePair<int, ushort>(cp, gid);
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// Resolves the glyph ID for a given code point within a specific segment
private ushort ResolveGlyph(int segmentIndex, int codePoint)
{
ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets, segmentIndex);
ushort idDelta = ReadUInt16BE(_idDeltas, segmentIndex);
if (idRangeOffset == 0)
{
return (ushort)((codePoint + idDelta) & 0xFFFF);
}
else
{
int start = ReadUInt16BE(_startCodes, segmentIndex);
int ro = idRangeOffset / 2; // words
int idx = (codePoint - start) + ro - (_segCount - segmentIndex);
int glyphArrayWords = _glyphIdArray.Length / 2;
if ((uint)idx < (uint)glyphArrayWords)
{
ushort glyphId = ReadUInt16BE(_glyphIdArray, idx);
if (glyphId != 0)
{
glyphId = (ushort)((glyphId + idDelta) & 0xFFFF);
}
return glyphId;
}
}
// Not found or maps to missing glyph
return 0;
}
private int FindSegmentIndex(int codePoint)
{
int lo = 0;
int hi = _segCount - 1;
// Binary search over endCodes (sorted ascending)
while (lo <= hi)
{
int mid = (lo + hi) >> 1;
int end = ReadUInt16BE(_endCodes, mid);
if (codePoint > end)
{
lo = mid + 1;
}
else
{
hi = mid - 1;
}
}
// lo is now the first segment whose endCode >= codePoint
if (lo < _segCount)
{
int start = ReadUInt16BE(_startCodes, lo);
if (codePoint >= start)
{
return lo;
}
}
return -1; // not found
}
}
}

42
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapSubtableEntry.cs

@ -0,0 +1,42 @@
using System;
namespace Avalonia.Media.Fonts.Tables.Cmap
{
// Representation of a subtable entry in the 'cmap' table directory
internal readonly record struct CmapSubtableEntry
{
public CmapSubtableEntry(PlatformID platform, CmapEncoding encoding, int offset, CmapFormat format) : this()
{
Platform = platform;
Encoding = encoding;
Offset = offset;
Format = format;
}
/// <summary>
/// Gets the platform identifier for the current environment.
/// </summary>
public PlatformID Platform { get; init; }
/// <summary>
/// Gets the character map (CMap) encoding associated with this instance.
/// </summary>
///
public CmapEncoding Encoding { get; init; }
/// <summary>
/// Gets the offset of the sub table.
/// </summary>
public int Offset { get; init; }
/// <summary>
/// Gets the format of the character-to-glyph mapping (cmap) table.
/// </summary>
public CmapFormat Format { get; init; }
public ReadOnlyMemory<byte> GetSubtableMemory(ReadOnlyMemory<byte> table)
{
return table.Slice(Offset);
}
}
}

166
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs

@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Media.Fonts.Tables.Cmap
{
/// <summary>
/// Represents the 'cmap' table in an OpenType font, which maps character codes to glyph indices.
/// </summary>
/// <remarks>The 'cmap' table is a critical component of an OpenType font, enabling the mapping of
/// character codes (e.g., Unicode) to glyph indices used for rendering text. This class provides functionality to
/// load and parse the 'cmap' table from a font's platform-specific typeface.</remarks>
internal sealed class CmapTable
{
internal const string TableName = "cmap";
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName);
public static IReadOnlyDictionary<int, ushort> Load(IGlyphTypeface glyphTypeface)
{
if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table))
{
throw new InvalidOperationException("No cmap table found.");
}
var reader = new BigEndianBinaryReader(table.Span);
reader.ReadUInt16(); // version
var numTables = reader.ReadUInt16();
var entries = new CmapSubtableEntry[numTables];
for (var i = 0; i < numTables; i++)
{
var platformID = (PlatformID)reader.ReadUInt16();
var encodingID = (CmapEncoding)reader.ReadUInt16();
var offset = (int)reader.ReadUInt32();
var position = reader.Position;
reader.Seek(offset);
var format = (CmapFormat)reader.ReadUInt16();
reader.Seek(position);
var entry = new CmapSubtableEntry(platformID, encodingID, offset, format);
entries[i] = entry;
}
// Try to find the best Format 12 subtable entry
if (TryFindFormat12Entry(entries, out var format12Entry))
{
// Prefer Format 12 if available
return new CmapFormat12Table(format12Entry.GetSubtableMemory(table));
}
// Fallback to Format 4
if (TryFindFormat4Entry(entries, out var format4Entry))
{
return new CmapFormat4Table(format4Entry.GetSubtableMemory(table));
}
throw new InvalidOperationException("No suitable cmap subtable found.");
// Tries to find the best Format 12 subtable entry based on platform and encoding preferences
static bool TryFindFormat12Entry(CmapSubtableEntry[] entries, out CmapSubtableEntry result)
{
result = default;
var foundPlatformScore = int.MaxValue;
var foundEncodingScore = int.MaxValue;
foreach (var entry in entries)
{
if (entry.Format != CmapFormat.Format12)
{
continue;
}
var platformScore = entry.Platform switch
{
PlatformID.Unicode => 0,
PlatformID.Windows => 1,
_ => 2
};
var encodingScore = 2; // Default: lowest preference
switch (entry.Platform)
{
case PlatformID.Unicode when entry.Encoding == CmapEncoding.Unicode_2_0_full:
encodingScore = 0; // non-BMP preferred
break;
case PlatformID.Unicode when entry.Encoding == CmapEncoding.Unicode_2_0_BMP:
encodingScore = 1; // BMP
break;
case PlatformID.Windows when entry.Encoding == CmapEncoding.Microsoft_UCS4 && platformScore != 0:
encodingScore = 0; // non-BMP preferred
break;
case PlatformID.Windows when entry.Encoding == CmapEncoding.Microsoft_UnicodeBMP && platformScore != 0:
encodingScore = 1; // BMP
break;
}
if (encodingScore < foundEncodingScore || encodingScore == foundEncodingScore && platformScore < foundPlatformScore)
{
result = entry;
foundEncodingScore = encodingScore;
foundPlatformScore = platformScore;
}
else
{
if (platformScore < foundPlatformScore)
{
result = entry;
foundEncodingScore = encodingScore;
foundPlatformScore = platformScore;
}
}
if (foundPlatformScore == 0 && foundEncodingScore == 0)
{
break; // Best possible match found
}
}
return result.Format != CmapFormat.Format0;
}
// Tries to find the best Format 4 subtable entry based on platform preferences
static bool TryFindFormat4Entry(CmapSubtableEntry[] entries, out CmapSubtableEntry result)
{
result = default;
var foundPlatformScore = int.MaxValue;
foreach (var entry in entries)
{
if (entry.Format != CmapFormat.Format4)
{
continue;
}
var platformScore = entry.Platform switch
{
PlatformID.Unicode => 0,
PlatformID.Windows => 1,
_ => 2
};
if (platformScore < foundPlatformScore)
{
result = entry;
foundPlatformScore = platformScore;
}
if (foundPlatformScore == 0)
{
break; // Best possible match found
}
}
return result.Format != CmapFormat.Format0;
}
}
}
}

21
src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs

@ -3,7 +3,6 @@
// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
using System.Collections.Generic;
using System.IO;
namespace Avalonia.Media.Fonts.Tables
{
@ -17,8 +16,8 @@ namespace Avalonia.Media.Fonts.Tables
/// </summary>
internal class FeatureListTable
{
private static OpenTypeTag GSubTag = OpenTypeTag.Parse("GSUB");
private static OpenTypeTag GPosTag = OpenTypeTag.Parse("GPOS");
private static OpenTypeTag GSubTag { get; } = OpenTypeTag.Parse("GSUB");
private static OpenTypeTag GPosTag { get; } = OpenTypeTag.Parse("GPOS");
private FeatureListTable(IReadOnlyList<OpenTypeTag> features)
{
@ -29,26 +28,24 @@ namespace Avalonia.Media.Fonts.Tables
public static FeatureListTable? LoadGSub(IGlyphTypeface glyphTypeface)
{
if (!glyphTypeface.TryGetTable(GSubTag, out var gPosTable))
if (!glyphTypeface.PlatformTypeface.TryGetTable(GSubTag, out var gPosTable))
{
return null;
}
using var stream = new MemoryStream(gPosTable);
using var reader = new BigEndianBinaryReader(stream, false);
var reader = new BigEndianBinaryReader(gPosTable.Span);
return Load(reader);
}
public static FeatureListTable? LoadGPos(IGlyphTypeface glyphTypeface)
{
if (!glyphTypeface.TryGetTable(GPosTag, out var gSubTable))
if (!glyphTypeface.PlatformTypeface.TryGetTable(GPosTag, out var gSubTable))
{
return null;
}
using var stream = new MemoryStream(gSubTable);
using var reader = new BigEndianBinaryReader(stream, false);
var reader = new BigEndianBinaryReader(gSubTable.Span);
return Load(reader);
@ -73,14 +70,14 @@ namespace Avalonia.Media.Fonts.Tables
reader.ReadUInt16();
reader.ReadUInt16();
reader.ReadOffset16();
var featureListOffset = reader.ReadOffset16();
return Load(reader, featureListOffset);
}
private static FeatureListTable Load(BigEndianBinaryReader reader, long offset)
private static FeatureListTable Load(BigEndianBinaryReader reader, int offset)
{
// FeatureList
// +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+
@ -90,7 +87,7 @@ namespace Avalonia.Media.Fonts.Tables
// +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+
// | FeatureRecord | featureRecords[featureCount] | Array of FeatureRecords — zero-based (first feature has FeatureIndex = 0), listed alphabetically by feature tag |
// +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+
reader.Seek(offset, SeekOrigin.Begin);
reader.Seek(offset);
var featureCount = reader.ReadUInt16();

118
src/Avalonia.Base/Media/Fonts/Tables/HeadTable.cs

@ -0,0 +1,118 @@
using System;
namespace Avalonia.Media.Fonts.Tables
{
internal sealed class HeadTable
{
internal const string TableName = "head";
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName);
public float Version { get; }
public float FontRevision { get; }
public uint CheckSumAdjustment { get; }
public uint MagicNumber { get; }
public ushort Flags { get; }
public ushort UnitsPerEm { get; }
public long Created { get; }
public long Modified { get; }
public short XMin { get; }
public short YMin { get; }
public short XMax { get; }
public short YMax { get; }
public ushort MacStyle { get; }
public ushort LowestRecPPEM { get; }
public short FontDirectionHint { get; }
public short IndexToLocFormat { get; }
public short GlyphDataFormat { get; }
private HeadTable(
float version,
float fontRevision,
uint checkSumAdjustment,
uint magicNumber,
ushort flags,
ushort unitsPerEm,
long created,
long modified,
short xMin,
short yMin,
short xMax,
short yMax,
ushort macStyle,
ushort lowestRecPPEM,
short fontDirectionHint,
short indexToLocFormat,
short glyphDataFormat)
{
Version = version;
FontRevision = fontRevision;
CheckSumAdjustment = checkSumAdjustment;
MagicNumber = magicNumber;
Flags = flags;
UnitsPerEm = unitsPerEm;
Created = created;
Modified = modified;
XMin = xMin;
YMin = yMin;
XMax = xMax;
YMax = yMax;
MacStyle = macStyle;
LowestRecPPEM = lowestRecPPEM;
FontDirectionHint = fontDirectionHint;
IndexToLocFormat = indexToLocFormat;
GlyphDataFormat = glyphDataFormat;
}
public static HeadTable Load(IGlyphTypeface glyphTypeface)
{
if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table))
{
throw new InvalidOperationException("Could not load the 'head' table.");
}
var reader = new BigEndianBinaryReader(table.Span);
return Load(reader);
}
private static HeadTable Load(BigEndianBinaryReader reader)
{
float version = reader.ReadFixed();
float fontRevision = reader.ReadFixed();
uint checkSumAdjustment = reader.ReadUInt32();
uint magicNumber = reader.ReadUInt32();
ushort flags = reader.ReadUInt16();
ushort unitsPerEm = reader.ReadUInt16();
long created = reader.ReadInt64();
long modified = reader.ReadInt64();
short xMin = reader.ReadInt16();
short yMin = reader.ReadInt16();
short xMax = reader.ReadInt16();
short yMax = reader.ReadInt16();
ushort macStyle = reader.ReadUInt16();
ushort lowestRecPPEM = reader.ReadUInt16();
short fontDirectionHint = reader.ReadInt16();
short indexToLocFormat = reader.ReadInt16();
short glyphDataFormat = reader.ReadInt16();
return new HeadTable(
version,
fontRevision,
checkSumAdjustment,
magicNumber,
flags,
unitsPerEm,
created,
modified,
xMin,
yMin,
xMax,
yMax,
macStyle,
lowestRecPPEM,
fontDirectionHint,
indexToLocFormat,
glyphDataFormat);
}
}
}

57
src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs → src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeaderTable.cs

@ -2,16 +2,18 @@
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
using System.IO;
namespace Avalonia.Media.Fonts.Tables
{
internal class HorizontalHeadTable
internal class HorizontalHeaderTable
{
internal const string TableName = "hhea";
internal static OpenTypeTag Tag = OpenTypeTag.Parse(TableName);
public HorizontalHeadTable(
/// <summary>
/// Gets the OpenType tag identifying this table ("hhea").
/// </summary>
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName);
public HorizontalHeaderTable(
short ascender,
short descender,
short lineGap,
@ -37,43 +39,74 @@ namespace Avalonia.Media.Fonts.Tables
NumberOfHMetrics = numberOfHMetrics;
}
/// <summary>
/// Gets the maximum advance width value for all glyphs in the font.
/// </summary>
public ushort AdvanceWidthMax { get; }
/// <summary>
/// Distance from the baseline to the highest ascender.
/// </summary>
public short Ascender { get; }
/// <summary>
/// Offset of the caret for slanted fonts. Set to 0 for non-slanted fonts.
/// </summary>
public short CaretOffset { get; }
/// <summary>
/// Rise component used to calculate the slope of the caret (rise/run).
/// </summary>
public short CaretSlopeRise { get; }
/// <summary>
/// Run component used to calculate the slope of the caret (rise/run).
/// </summary>
public short CaretSlopeRun { get; }
/// <summary>
/// Distance from the baseline to the lowest descender.
/// </summary>
public short Descender { get; }
/// <summary>
/// Typographic line gap.
/// </summary>
public short LineGap { get; }
/// <summary>
/// Minimum left side bearing value. Must be consistent with horizontal metrics.
/// </summary>
public short MinLeftSideBearing { get; }
/// <summary>
/// Minimum right side bearing value. Must be consistent with horizontal metrics.
/// </summary>
public short MinRightSideBearing { get; }
/// <summary>
/// Number of advance widths in the horizontal metrics table (numOfLongHorMetrics).
/// </summary>
public ushort NumberOfHMetrics { get; }
/// <summary>
/// Maximum horizontal extent: max(lsb + (xMax - xMin)).
/// </summary>
public short XMaxExtent { get; }
public static HorizontalHeadTable? Load(IGlyphTypeface glyphTypeface)
public static HorizontalHeaderTable? Load(IGlyphTypeface fontFace)
{
if (!glyphTypeface.TryGetTable(Tag, out var table))
if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table))
{
return null;
}
using var stream = new MemoryStream(table);
using var binaryReader = new BigEndianBinaryReader(stream, false);
var binaryReader = new BigEndianBinaryReader(table.Span);
// Move to start of table.
return Load(binaryReader);
}
public static HorizontalHeadTable Load(BigEndianBinaryReader reader)
private static HorizontalHeaderTable Load(BigEndianBinaryReader reader)
{
// +--------+---------------------+---------------------------------------------------------------------------------+
// | Type | Name | Description |
@ -136,7 +169,7 @@ namespace Avalonia.Media.Fonts.Tables
ushort numberOfHMetrics = reader.ReadUInt16();
return new HorizontalHeadTable(
return new HorizontalHeaderTable(
ascender,
descender,
lineGap,

37
src/Avalonia.Base/Media/Fonts/Tables/MaxpTable.cs

@ -0,0 +1,37 @@
namespace Avalonia.Media.Fonts.Tables
{
internal readonly struct MaxpTable
{
internal const string TableName = "maxp";
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName);
public ushort NumGlyphs { get; }
private MaxpTable(ushort numGlyphs)
{
NumGlyphs = numGlyphs;
}
public static MaxpTable? Load(IGlyphTypeface fontFace)
{
if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table))
{
return null;
}
var binaryReader = new BigEndianBinaryReader(table.Span);
return Load(binaryReader);
}
private static MaxpTable Load(BigEndianBinaryReader reader)
{
// Skip version (4 bytes)
reader.ReadUInt32();
var numGlyphs = reader.ReadUInt16();
return new MaxpTable(numGlyphs);
}
}
}

26
src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalGlyphMetric.cs

@ -0,0 +1,26 @@
namespace Avalonia.Media.Fonts.Tables.Metrics
{
/// <summary>
/// Represents a single horizontal metric record from the 'hmtx' table.
/// </summary>
internal readonly record struct HorizontalGlyphMetric
{
/// <summary>
/// The advance width of the glyph.
/// </summary>
public ushort AdvanceWidth { get; }
/// <summary>
/// The left side bearing of the glyph.
/// </summary>
public short LeftSideBearing { get; }
public HorizontalGlyphMetric(ushort advanceWidth, short leftSideBearing)
{
AdvanceWidth = advanceWidth;
LeftSideBearing = leftSideBearing;
}
public override string ToString() => $"Advance={AdvanceWidth}, LSB={LeftSideBearing}";
}
}

113
src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalMetricsTable.cs

@ -0,0 +1,113 @@
using System;
namespace Avalonia.Media.Fonts.Tables.Metrics
{
internal class HorizontalMetricsTable
{
public const string TagName = "hmtx";
public static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TagName);
private readonly ReadOnlyMemory<byte> _data;
private readonly ushort _numOfHMetrics;
private readonly uint _numGlyphs;
private HorizontalMetricsTable(ReadOnlyMemory<byte> data, ushort numOfHMetrics, uint numGlyphs)
{
_data = data;
_numOfHMetrics = numOfHMetrics;
_numGlyphs = numGlyphs;
}
internal static HorizontalMetricsTable? Load(IGlyphTypeface glyphTypeface, ushort numberOfHMetrics, uint glyphCount)
{
if (glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table))
{
return new HorizontalMetricsTable(table, numberOfHMetrics, glyphCount);
}
return null;
}
/// <summary>
/// Retrieves the horizontal glyph metrics for the specified glyph index.
/// </summary>
/// <remarks>This method retrieves the horizontal metrics for a single glyph by its index. The
/// returned metrics include information such as advance width, left side bearing, and other glyph-specific
/// data.</remarks>
/// <param name="glyphIndex">The index of the glyph for which to retrieve metrics. Must be a valid glyph index within the font.</param>
/// <returns>A <see cref="HorizontalGlyphMetric"/> object containing the horizontal metrics for the specified glyph.</returns>
public HorizontalGlyphMetric GetMetrics(ushort glyphIndex)
{
// Validate glyph index
if (glyphIndex >= _numGlyphs)
{
throw new ArgumentOutOfRangeException(nameof(glyphIndex), $"Glyph index {glyphIndex} is out of range.");
}
var reader = new BigEndianBinaryReader(_data.Span);
if (glyphIndex < _numOfHMetrics)
{
// Each record is 4 bytes
reader.Seek(glyphIndex * 4);
ushort advanceWidth = reader.ReadUInt16();
short leftSideBearing = reader.ReadInt16();
return new HorizontalGlyphMetric(advanceWidth, leftSideBearing);
}
else
{
// Last advance width
reader.Seek((_numOfHMetrics - 1) * 4);
ushort lastAdvanceWidth = reader.ReadUInt16();
// Offset into trailing LSB array
int lsbIndex = glyphIndex - _numOfHMetrics;
int lsbOffset = _numOfHMetrics * 4 + lsbIndex * 2;
reader.Seek(lsbOffset);
short leftSideBearing = reader.ReadInt16();
return new HorizontalGlyphMetric(lastAdvanceWidth, leftSideBearing);
}
}
/// <summary>
/// Retrieves the advance width for a single glyph.
/// </summary>
/// <param name="glyphIndex">Glyph index to query.</param>
/// <returns>Advance width for the glyph.</returns>
public ushort GetAdvance(ushort glyphIndex)
{
// Validate glyph index
if (glyphIndex >= _numGlyphs)
{
throw new ArgumentOutOfRangeException(nameof(glyphIndex));
}
var reader = new BigEndianBinaryReader(_data.Span);
if (glyphIndex < _numOfHMetrics)
{
// Each record is 4 bytes
reader.Seek(glyphIndex * 4);
ushort advanceWidth = reader.ReadUInt16();
return advanceWidth;
}
else
{
// Last advance width
reader.Seek((_numOfHMetrics - 1) * 4);
ushort lastAdvanceWidth = reader.ReadUInt16();
return lastAdvanceWidth;
}
}
}
}

24
src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalGlyphMetric.cs

@ -0,0 +1,24 @@
namespace Avalonia.Media.Fonts.Tables.Metrics
{
/// <summary>
/// Represents a single vertical metric record from the 'vmtx' table.
/// </summary>
internal readonly record struct VerticalGlyphMetric
{
public VerticalGlyphMetric(ushort advanceHeight, short topSideBearing)
{
AdvanceHeight = advanceHeight;
TopSideBearing = topSideBearing;
}
/// <summary>
/// The advance height of the glyph.
/// </summary>
public ushort AdvanceHeight { get; }
/// <summary>
/// The top side bearing of the glyph.
/// </summary>
public short TopSideBearing { get; }
}
}

110
src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalMetricsTable.cs

@ -0,0 +1,110 @@
using System;
namespace Avalonia.Media.Fonts.Tables.Metrics
{
internal class VerticalMetricsTable
{
public const string TagName = "vmtx";
public static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TagName);
private readonly ReadOnlyMemory<byte> _data;
private readonly ushort _numOfVMetrics;
private readonly uint _numGlyphs;
private VerticalMetricsTable(ReadOnlyMemory<byte> data, ushort numOfVMetrics, uint numGlyphs)
{
_data = data;
_numOfVMetrics = numOfVMetrics;
_numGlyphs = numGlyphs;
}
public static VerticalMetricsTable? Load(IGlyphTypeface glyphTypeface, ushort numberOfVMetrics, uint glyphCount)
{
if (glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table))
{
return new VerticalMetricsTable(table, numberOfVMetrics, glyphCount);
}
return null;
}
/// <summary>
/// Retrieves the vertical glyph metrics for the specified glyph index.
/// </summary>
/// <param name="glyphIndex">The index of the glyph for which to retrieve metrics.</param>
/// <returns>A <see cref="VerticalGlyphMetric"/> containing the vertical metrics for the specified glyph.</returns>
public VerticalGlyphMetric GetMetrics(ushort glyphIndex)
{
// Validate glyph index
if (glyphIndex >= _numGlyphs)
{
throw new ArgumentOutOfRangeException(nameof(glyphIndex), $"Glyph index {glyphIndex} is out of range.");
}
var reader = new BigEndianBinaryReader(_data.Span);
if (glyphIndex < _numOfVMetrics)
{
// Each record is 4 bytes
reader.Seek(glyphIndex * 4);
ushort advanceHeight = reader.ReadUInt16();
short topSideBearing = reader.ReadInt16();
return new VerticalGlyphMetric(advanceHeight, topSideBearing);
}
else
{
// Last advance height
reader.Seek((_numOfVMetrics - 1) * 4);
ushort lastAdvanceHeight = reader.ReadUInt16();
// Offset into trailing TSB array
int tsbIndex = glyphIndex - _numOfVMetrics;
int tsbOffset = _numOfVMetrics * 4 + tsbIndex * 2;
reader.Seek(tsbOffset);
short tsb = reader.ReadInt16();
return new VerticalGlyphMetric(lastAdvanceHeight, tsb);
}
}
/// <summary>
/// Retrieves the advance height for a single glyph.
/// </summary>
/// <param name="glyphIndex">Glyph index to query.</param>
/// <returns>Advance height for the glyph.</returns>
public ushort GetAdvance(ushort glyphIndex)
{
// Validate glyph index
if (glyphIndex >= _numGlyphs)
{
throw new ArgumentOutOfRangeException(nameof(glyphIndex));
}
var reader = new BigEndianBinaryReader(_data.Span);
if (glyphIndex < _numOfVMetrics)
{
// Each record is 4 bytes
reader.Seek(glyphIndex * 4);
ushort advanceHeight = reader.ReadUInt16();
return advanceHeight;
}
else
{
// Last advance height
reader.Seek((_numOfVMetrics - 1) * 4);
ushort lastAdvanceHeight = reader.ReadUInt16();
return lastAdvanceHeight;
}
}
}
}

54
src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs

@ -2,44 +2,64 @@
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
using System;
namespace Avalonia.Media.Fonts.Tables.Name
{
internal class NameRecord
{
private readonly string value;
private readonly ReadOnlyMemory<byte> _stringStorage;
public NameRecord(PlatformIDs platform, ushort languageId, KnownNameIds nameId, string value)
public NameRecord(
ReadOnlyMemory<byte> stringStorage,
PlatformID platform,
ushort languageId,
KnownNameIds nameId,
ushort offset,
ushort length,
System.Text.Encoding encoding)
{
_stringStorage = stringStorage;
Platform = platform;
LanguageID = languageId;
NameID = nameId;
this.value = value;
Offset = offset;
Length = length;
Encoding = encoding;
}
public PlatformIDs Platform { get; }
public PlatformID Platform { get; }
public ushort LanguageID { get; }
public KnownNameIds NameID { get; }
internal StringLoader? StringReader { get; private set; }
public ushort Offset { get; }
public string Value => StringReader?.Value ?? value;
public ushort Length { get; }
public static NameRecord Read(BigEndianBinaryReader reader)
public System.Text.Encoding Encoding { get; }
public string Value
{
var platform = reader.ReadUInt16<PlatformIDs>();
var encodingId = reader.ReadUInt16<EncodingIDs>();
var encoding = encodingId.AsEncoding();
var languageID = reader.ReadUInt16();
var nameID = reader.ReadUInt16<KnownNameIds>();
get
{
if (Length == 0)
{
return string.Empty;
}
var stringReader = StringLoader.Create(reader, encoding);
var reader = new BigEndianBinaryReader(_stringStorage.Span);
return new NameRecord(platform, languageID, nameID, string.Empty)
{
StringReader = stringReader
};
reader.Seek(Offset);
byte[] data = reader.ReadBytes(Length);
var value = Encoding.GetString(data);
return value;
}
}
}
}

51
src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs

@ -4,7 +4,6 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Avalonia.Utilities;
namespace Avalonia.Media.Fonts.Tables.Name
@ -69,7 +68,7 @@ namespace Avalonia.Media.Fonts.Tables.Name
{
// Get just the first one, just in case.
first ??= name;
if (name.Platform == PlatformIDs.Windows)
if (name.Platform == PlatformID.Windows)
{
// If us not found return the first windows one.
firstWindows ??= name;
@ -99,46 +98,30 @@ namespace Avalonia.Media.Fonts.Tables.Name
public static NameTable? Load(IGlyphTypeface glyphTypeface)
{
if (!glyphTypeface.TryGetTable(Tag, out var table))
if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table))
{
return null;
}
using var stream = new MemoryStream(table);
using var binaryReader = new BigEndianBinaryReader(stream, false);
var reader = new BigEndianBinaryReader(table.Span);
// Move to start of table.
return Load(binaryReader);
}
public static NameTable Load(BigEndianBinaryReader reader)
{
var strings = new List<StringLoader>();
var format = reader.ReadUInt16();
var nameCount = reader.ReadUInt16();
var stringOffset = reader.ReadUInt16();
var names = new NameRecord[nameCount];
for (var i = 0; i < nameCount; i++)
{
names[i] = NameRecord.Read(reader);
reader.ReadUInt16(); // version
var count = reader.ReadUInt16();
var storageOffset = reader.ReadUInt16();
var sr = names[i].StringReader;
var names = new NameRecord[count];
if (sr is not null)
{
strings.Add(sr);
}
}
foreach (var readable in strings)
for (var i = 0; i < count; i++)
{
var readableStartOffset = stringOffset + readable.Offset;
reader.Seek(readableStartOffset, SeekOrigin.Begin);
readable.LoadValue(reader);
var platform = reader.ReadUInt16<PlatformID>();
var encodingId = reader.ReadUInt16<EncodingIDs>();
var encoding = encodingId.AsEncoding();
var languageID = reader.ReadUInt16();
var nameID = reader.ReadUInt16<KnownNameIds>();
var length = reader.ReadUInt16();
var offset = reader.ReadUInt16();
names[i] = new NameRecord(table.Slice(storageOffset), platform, languageID, nameID, offset, length, encoding);
}
return new NameTable(names);

31
src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs

@ -3,14 +3,13 @@
// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
using System;
using System.IO;
namespace Avalonia.Media.Fonts.Tables
{
internal sealed class OS2Table
{
internal const string TableName = "OS/2";
internal static OpenTypeTag Tag = OpenTypeTag.Parse(TableName);
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName);
private readonly byte[] panose;
private readonly short capHeight;
@ -54,7 +53,7 @@ namespace Avalonia.Media.Fonts.Tables
uint unicodeRange3,
uint unicodeRange4,
string tag,
FontStyleSelection fontStyle,
FontSelectionFlags fontStyle,
ushort firstCharIndex,
ushort lastCharIndex,
short typoAscender,
@ -66,7 +65,7 @@ namespace Avalonia.Media.Fonts.Tables
this.averageCharWidth = averageCharWidth;
WeightClass = weightClass;
WidthClass = widthClass;
StyleType = styleType;
Type = styleType;
SubscriptXSize = subscriptXSize;
SubscriptYSize = subscriptYSize;
SubscriptXOffset = subscriptXOffset;
@ -84,7 +83,7 @@ namespace Avalonia.Media.Fonts.Tables
this.unicodeRange3 = unicodeRange3;
this.unicodeRange4 = unicodeRange4;
this.tag = tag;
FontStyle = fontStyle;
Selection = fontStyle;
this.firstCharIndex = firstCharIndex;
this.lastCharIndex = lastCharIndex;
TypoAscender = typoAscender;
@ -107,7 +106,7 @@ namespace Avalonia.Media.Fonts.Tables
version0Table.averageCharWidth,
version0Table.WeightClass,
version0Table.WidthClass,
version0Table.StyleType,
version0Table.Type,
version0Table.SubscriptXSize,
version0Table.SubscriptYSize,
version0Table.SubscriptXOffset,
@ -125,7 +124,7 @@ namespace Avalonia.Media.Fonts.Tables
version0Table.unicodeRange3,
version0Table.unicodeRange4,
version0Table.tag,
version0Table.FontStyle,
version0Table.Selection,
version0Table.firstCharIndex,
version0Table.lastCharIndex,
version0Table.TypoAscender,
@ -159,7 +158,7 @@ namespace Avalonia.Media.Fonts.Tables
}
[Flags]
internal enum FontStyleSelection : ushort
internal enum FontSelectionFlags : ushort
{
/// <summary>
/// Font contains italic or oblique characters, otherwise they are upright.
@ -214,7 +213,7 @@ namespace Avalonia.Media.Fonts.Tables
// 10–15 <reserved> Reserved; set to 0.
}
public FontStyleSelection FontStyle { get; }
public FontSelectionFlags Selection { get; }
public short TypoAscender { get; }
@ -246,27 +245,25 @@ namespace Avalonia.Media.Fonts.Tables
public short SuperscriptYSize { get; }
public ushort StyleType { get; }
public ushort Type { get; }
public ushort WeightClass { get; }
public ushort WidthClass { get; }
public static OS2Table? Load(IGlyphTypeface glyphTypeface)
public static OS2Table? Load(IGlyphTypeface fontFace)
{
if (!glyphTypeface.TryGetTable(Tag, out var table))
if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table))
{
return null;
}
using var stream = new MemoryStream(table);
using var binaryReader = new BigEndianBinaryReader(stream, false);
var binaryReader = new BigEndianBinaryReader(table.Span);
// Move to start of table.
return Load(binaryReader);
}
public static OS2Table Load(BigEndianBinaryReader reader)
private static OS2Table Load(BigEndianBinaryReader reader)
{
// Version 1.0
// Type | Name | Comments
@ -334,7 +331,7 @@ namespace Avalonia.Media.Fonts.Tables
uint unicodeRange3 = reader.ReadUInt32(); // Bits 64–95
uint unicodeRange4 = reader.ReadUInt32(); // Bits 96–127
string tag = reader.ReadTag();
FontStyleSelection fontStyle = reader.ReadUInt16<FontStyleSelection>();
FontSelectionFlags fontStyle = reader.ReadUInt16<FontSelectionFlags>();
ushort firstCharIndex = reader.ReadUInt16();
ushort lastCharIndex = reader.ReadUInt16();
short typoAscender = reader.ReadInt16();

2
src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs → src/Avalonia.Base/Media/Fonts/Tables/PlatformID.cs

@ -7,7 +7,7 @@ namespace Avalonia.Media.Fonts.Tables
/// <summary>
/// platforms ids
/// </summary>
internal enum PlatformIDs : ushort
internal enum PlatformID : ushort
{
/// <summary>
/// Unicode platform

46
src/Avalonia.Base/Media/Fonts/Tables/PostTable.cs

@ -0,0 +1,46 @@
namespace Avalonia.Media.Fonts.Tables
{
internal readonly struct PostTable
{
internal const string TableName = "post";
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName);
public float Version { get; }
public float ItalicAngle { get; }
public short UnderlinePosition { get; }
public short UnderlineThickness { get; }
public bool IsFixedPitch { get; }
private PostTable(float version, float italicAngle, short underlinePosition, short underlineThickness, uint isFixedPitch)
{
Version = version;
ItalicAngle = italicAngle;
UnderlinePosition = underlinePosition;
UnderlineThickness = underlineThickness;
IsFixedPitch = isFixedPitch != 0;
}
public static PostTable Load(IGlyphTypeface glyphTypeface)
{
if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table))
{
return default;
}
var binaryReader = new BigEndianBinaryReader(table.Span);
return Load(binaryReader);
}
private static PostTable Load(BigEndianBinaryReader reader)
{
float version = reader.ReadFixed();
float italicAngle = reader.ReadFixed();
short underlinePosition = reader.ReadFWORD();
short underlineThickness = reader.ReadFWORD();
uint isFixedPitch = reader.ReadUInt32();
return new PostTable(version, italicAngle, underlinePosition, underlineThickness, isFixedPitch);
}
}
}

38
src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs

@ -1,38 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
using System.Diagnostics;
using System.Text;
namespace Avalonia.Media.Fonts.Tables
{
[DebuggerDisplay("Offset: {Offset}, Length: {Length}, Value: {Value}")]
internal class StringLoader
{
public StringLoader(ushort length, ushort offset, Encoding encoding)
{
Length = length;
Offset = offset;
Encoding = encoding;
Value = string.Empty;
}
public ushort Length { get; }
public ushort Offset { get; }
public string Value { get; private set; }
public Encoding Encoding { get; }
public static StringLoader Create(BigEndianBinaryReader reader)
=> Create(reader, Encoding.BigEndianUnicode);
public static StringLoader Create(BigEndianBinaryReader reader, Encoding encoding)
=> new StringLoader(reader.ReadUInt16(), reader.ReadUInt16(), encoding);
public void LoadValue(BigEndianBinaryReader reader)
=> Value = reader.ReadString(Length, Encoding).Replace("\0", string.Empty);
}
}

128
src/Avalonia.Base/Media/Fonts/Tables/VerticalHeaderTable.cs

@ -0,0 +1,128 @@
namespace Avalonia.Media.Fonts.Tables
{
internal class VerticalHeaderTable
{
internal const string TableName = "vhea";
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName);
public VerticalHeaderTable(
short ascender,
short descender,
short lineGap,
ushort advanceHeightMax,
short minTopSideBearing,
short minBottomSideBearing,
short yMaxExtent,
short caretSlopeRise,
short caretSlopeRun,
short caretOffset,
ushort numberOfVMetrics)
{
Ascender = ascender;
Descender = descender;
LineGap = lineGap;
AdvanceHeightMax = advanceHeightMax;
MinTopSideBearing = minTopSideBearing;
MinBottomSideBearing = minBottomSideBearing;
YMaxExtent = yMaxExtent;
CaretSlopeRise = caretSlopeRise;
CaretSlopeRun = caretSlopeRun;
CaretOffset = caretOffset;
NumberOfVMetrics = numberOfVMetrics;
}
public ushort AdvanceHeightMax { get; }
public short Ascender { get; }
public short CaretOffset { get; }
public short CaretSlopeRise { get; }
public short CaretSlopeRun { get; }
public short Descender { get; }
public short LineGap { get; }
public short MinTopSideBearing { get; }
public short MinBottomSideBearing { get; }
public ushort NumberOfVMetrics { get; }
public short YMaxExtent { get; }
public static VerticalHeaderTable? Load(IGlyphTypeface fontFace)
{
if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table))
{
return null;
}
var binaryReader = new BigEndianBinaryReader(table.Span);
// Move to start of table.
return Load(binaryReader);
}
private static VerticalHeaderTable Load(BigEndianBinaryReader reader)
{
// See OpenType spec for vhea:
// | Fixed | version | 0x00010000 (1.0) |
// | FWord | ascender | Distance from baseline of highest ascender (vertical) |
// | FWord | descender | Distance from baseline of lowest descender (vertical) |
// | FWord | lineGap | typographic line gap (vertical) |
// | uFWord | advanceHeightMax | must be consistent with vertical metrics |
// | FWord | minTopSideBearing | must be consistent with vertical metrics |
// | FWord | minBottomSideBearing| must be consistent with vertical metrics |
// | FWord | yMaxExtent | max(tsb + (yMax-yMin)) |
// | int16 | caretSlopeRise | used to calculate the slope of the caret (rise/run) set to 1 for vertical caret |
// | int16 | caretSlopeRun | 0 for vertical |
// | FWord | caretOffset | set value to 0 for non-slanted fonts |
// | int16 | reserved | set value to 0 |
// | int16 | reserved | set value to 0 |
// | int16 | reserved | set value to 0 |
// | int16 | reserved | set value to 0 |
// | int16 | metricDataFormat | 0 for current format |
// | uint16 | numOfLongVerMetrics | number of advance heights in vertical metrics table |
ushort majorVersion = reader.ReadUInt16();
ushort minorVersion = reader.ReadUInt16();
short ascender = reader.ReadFWORD();
short descender = reader.ReadFWORD();
short lineGap = reader.ReadFWORD();
ushort advanceHeightMax = reader.ReadUFWORD();
short minTopSideBearing = reader.ReadFWORD();
short minBottomSideBearing = reader.ReadFWORD();
short yMaxExtent = reader.ReadFWORD();
short caretSlopeRise = reader.ReadInt16();
short caretSlopeRun = reader.ReadInt16();
short caretOffset = reader.ReadInt16();
reader.ReadInt16(); // reserved
reader.ReadInt16(); // reserved
reader.ReadInt16(); // reserved
reader.ReadInt16(); // reserved
short metricDataFormat = reader.ReadInt16(); // 0
if (metricDataFormat != 0)
{
throw new InvalidFontTableException($"Expected metricDataFormat = 0 found {metricDataFormat}", TableName);
}
ushort numberOfVMetrics = reader.ReadUInt16();
return new VerticalHeaderTable(
ascender,
descender,
lineGap,
advanceHeightMax,
minTopSideBearing,
minBottomSideBearing,
yMaxExtent,
caretSlopeRise,
caretSlopeRun,
caretOffset,
numberOfVMetrics);
}
}
}

407
src/Avalonia.Base/Media/Fonts/UnmanagedFontMemory.cs

@ -0,0 +1,407 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
namespace Avalonia.Media.Fonts
{
/// <summary>
/// Represents a memory manager for unmanaged font data, providing functionality to access and manage font memory
/// and OpenType table data.
/// </summary>
/// <remarks>This class encapsulates unmanaged memory containing font data and provides methods to
/// retrieve specific OpenType table data. It ensures thread-safe access to the memory and supports pinning for
/// interoperability scenarios. Instances of this class must be properly disposed to release unmanaged
/// resources.</remarks>
internal sealed unsafe class UnmanagedFontMemory : MemoryManager<byte>, IFontMemory
{
private IntPtr _ptr;
private int _length;
private bool _disposed;
private int _pinCount;
// Reader/writer lock to protect lifetime and cache access.
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.NoRecursion);
/// <summary>
/// Represents a cache of font table data, where each entry maps an OpenType tag to its corresponding byte data.
/// </summary>
/// <remarks>This dictionary is used to store preloaded font table data for efficient access. The
/// keys are OpenType tags, which identify specific font tables, and the values are the corresponding byte data
/// stored as read-only memory. This ensures that the data cannot be modified after being loaded into the
/// cache.</remarks>
private readonly Dictionary<OpenTypeTag, ReadOnlyMemory<byte>> _tableCache = [];
private UnmanagedFontMemory(IntPtr ptr, int length)
{
_ptr = ptr;
_length = length;
}
/// <summary>
/// Attempts to retrieve the memory region corresponding to the specified OpenType table tag.
/// </summary>
/// <remarks>This method searches for the specified OpenType table in the font data and retrieves
/// its memory region if found. The method performs bounds checks to ensure the requested table is valid and
/// safely accessible. If the table is not found or the font data is invalid, the method returns <see
/// langword="false"/>.</remarks>
/// <param name="tag">The <see cref="OpenTypeTag"/> identifying the table to retrieve. Must not be <see cref="OpenTypeTag.None"/>.</param>
/// <param name="table">When this method returns, contains the memory region of the requested table if the operation succeeds;
/// otherwise, contains the default value.</param>
/// <returns><see langword="true"/> if the table memory was successfully retrieved; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ObjectDisposedException">Thrown if the font memory has been disposed.</exception>
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table)
{
table = default;
// Validate tag
if (tag == OpenTypeTag.None)
{
return false;
}
_lock.EnterUpgradeableReadLock();
try
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(UnmanagedFontMemory));
}
if (_ptr == IntPtr.Zero || _length < 12)
{
return false;
}
// Create a span over the unmanaged memory (read-only view)
var fontData = Memory.Span;
// Minimal SFNT header: 4 (sfnt) + 2 (numTables) + 6 (rest) = 12
if (fontData.Length < 12)
{
return false;
}
// Check cache first
if (_tableCache.TryGetValue(tag, out var cached))
{
table = cached;
return true;
}
// Parse table directory
var numTables = BinaryPrimitives.ReadUInt16BigEndian(fontData.Slice(4, 2));
var recordsStart = 12;
var requiredDirectoryBytes = checked(recordsStart + numTables * 16);
if (fontData.Length < requiredDirectoryBytes)
{
return false;
}
for (int i = 0; i < numTables; i++)
{
var entryOffset = recordsStart + i * 16;
var entrySlice = fontData.Slice(entryOffset, 16);
var entryTag = (OpenTypeTag)BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(0, 4));
if (entryTag != tag)
{
continue;
}
var offset = (int)BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(8, 4));
var length = (int)BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(12, 4));
// Bounds checks - ensure values fit within the span
if (offset > fontData.Length || length > fontData.Length)
{
return false;
}
if (offset + length > fontData.Length)
{
return false;
}
if (offset < 0 || length < 0 || offset + length > fontData.Length)
{
return false;
}
table = Memory.Slice(offset, length);
// Acquire write lock to update cache
_lock.EnterWriteLock();
try
{
// Cache the result for faster subsequent lookups
_tableCache[tag] = table;
return true;
}
finally
{
// Release write lock
_lock.ExitWriteLock();
}
}
return false;
}
finally
{
// Release upgradeable read lock
_lock.ExitUpgradeableReadLock();
}
}
/// <summary>
/// Loads font data from the specified stream into unmanaged memory.
/// </summary>
public static UnmanagedFontMemory LoadFromStream(Stream stream)
{
if (stream is null)
{
throw new ArgumentNullException(nameof(stream));
}
if (!stream.CanRead)
{
throw new ArgumentException("Stream is not readable", nameof(stream));
}
if (stream.CanSeek)
{
var length = checked((int)stream.Length);
var ptr = Marshal.AllocHGlobal(length);
var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
var remaining = length;
var offset = 0;
while (remaining > 0)
{
var toRead = Math.Min(buffer.Length, remaining);
var read = stream.Read(buffer, 0, toRead);
if (read == 0)
{
break;
}
Marshal.Copy(buffer, 0, ptr + offset, read);
offset += read;
remaining -= read;
}
return new UnmanagedFontMemory(ptr, offset);
}
catch
{
Marshal.FreeHGlobal(ptr);
throw;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
else
{
using var ms = new MemoryStream();
stream.CopyTo(ms);
var len = checked((int)ms.Length);
var buffer = ms.GetBuffer();
// GetBuffer may return a larger array than the actual data length.
return CreateFromBytes(new ReadOnlySpan<byte>(buffer, 0, len));
}
}
/// <summary>
/// Creates an instance of <see cref="UnmanagedFontMemory"/> from the specified byte data.
/// </summary>
/// <remarks>The method allocates unmanaged memory to store the provided byte data. The caller is
/// responsible for ensuring that the returned <see cref="UnmanagedFontMemory"/> instance is properly disposed
/// to release the allocated memory.</remarks>
/// <param name="data">A read-only span of bytes representing the font data. The span must not be empty.</param>
/// <returns>An instance of <see cref="UnmanagedFontMemory"/> that encapsulates the unmanaged memory containing the font
/// data.</returns>
private static UnmanagedFontMemory CreateFromBytes(ReadOnlySpan<byte> data)
{
var len = data.Length;
var ptr = Marshal.AllocHGlobal(len);
try
{
if (len > 0)
{
unsafe
{
fixed (byte* src = &MemoryMarshal.GetReference(data))
{
Buffer.MemoryCopy(src, (void*)ptr, len, len);
}
}
}
return new UnmanagedFontMemory(ptr, len);
}
catch
{
Marshal.FreeHGlobal(ptr);
throw;
}
}
// Implement MemoryManager<byte> members on the owner
public override Span<byte> GetSpan()
{
_lock.EnterReadLock();
try
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(UnmanagedFontMemory));
}
if (_ptr == IntPtr.Zero || _length <= 0)
{
return Span<byte>.Empty;
}
unsafe
{
return new Span<byte>((void*)_ptr.ToPointer(), _length);
}
}
finally
{
_lock.ExitReadLock();
}
}
public override MemoryHandle Pin(int elementIndex = 0)
{
if (elementIndex < 0)
{
throw new ArgumentOutOfRangeException(nameof(elementIndex));
}
// Increment pin count first to prevent dispose racing with pin.
Interlocked.Increment(ref _pinCount);
// Validate state under lock
_lock.EnterReadLock();
try
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(UnmanagedFontMemory));
}
if (_ptr == IntPtr.Zero || _length == 0)
{
return new MemoryHandle();
}
if (elementIndex > _length)
{
throw new ArgumentOutOfRangeException(nameof(elementIndex));
}
unsafe
{
var p = (byte*)_ptr.ToPointer() + elementIndex;
return new MemoryHandle(p);
}
}
finally
{
_lock.ExitReadLock();
}
}
public override void Unpin()
{
// Decrement pin count
Interlocked.Decrement(ref _pinCount);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
// Explicit dispose: use lock to synchronize with other threads and dispose managed resources.
_lock.EnterWriteLock();
try
{
if (_disposed)
{
return;
}
if (Volatile.Read(ref _pinCount) > 0)
{
throw new InvalidOperationException("Cannot dispose while memory is pinned.");
}
if (_ptr != IntPtr.Zero)
{
Marshal.FreeHGlobal(_ptr);
_ptr = IntPtr.Zero;
}
_length = 0;
_disposed = true;
}
finally
{
_lock.ExitWriteLock();
// Dispose the lock (managed resource) only on explicit dispose.
_lock.Dispose();
}
}
else
{
// Finalizer: do not touch managed objects. Free only unmanaged memory.
var ptr = Interlocked.Exchange(ref _ptr, IntPtr.Zero);
if (ptr != IntPtr.Zero)
{
Marshal.FreeHGlobal(ptr);
}
Interlocked.Exchange(ref _length, 0);
// Mark as disposed to prevent further attempts to use the memory.
_disposed = true;
}
}
}
}

6
src/Avalonia.Base/Media/GlyphMetrics.cs

@ -10,15 +10,15 @@ public readonly record struct GlyphMetrics
/// <summary>
/// Distance from the top extremum of the glyph to the y-origin.
/// </summary>
public int YBearing{ get; init; }
public int YBearing { get; init; }
/// <summary>
/// Distance from the left extremum of the glyph to the right extremum.
/// </summary>
public int Width{ get; init; }
public ushort Width { get; init; }
/// <summary>
/// Distance from the top extremum of the glyph to the bottom extremum.
/// </summary>
public int Height{ get; init; }
public ushort Height { get; init; }
}

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

@ -93,14 +93,17 @@ namespace Avalonia.Media
double fontRenderingEmSize, IGlyphTypeface glyphTypeface)
{
var glyphIndexSpan = ListToSpan(glyphIndices);
var glyphAdvances = glyphTypeface.GetGlyphAdvances(glyphIndexSpan);
var glyphInfos = new GlyphInfo[glyphIndexSpan.Length];
var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
for (var i = 0; i < glyphIndexSpan.Length; ++i)
{
glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale);
var glyphIndex = glyphIndexSpan[i];
var advance = glyphTypeface.GetGlyphAdvance(glyphIndex) * scale;
glyphInfos[i] = new GlyphInfo(glyphIndex, i, advance);
}
return glyphInfos;
@ -205,7 +208,7 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets the scale of the current <see cref="IGlyphTypeface"/>
/// Gets the scale of the current <see cref="IPlatformTypeface"/>
/// </summary>
internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight;
@ -270,7 +273,7 @@ namespace Avalonia.Media
//For in cluster hits we need to move to the start of the next cluster.
if (inClusterHit)
{
for(; glyphIndex < _glyphInfos.Count; glyphIndex++)
for (; glyphIndex < _glyphInfos.Count; glyphIndex++)
{
if (_glyphInfos[glyphIndex].GlyphCluster > characterIndex)
{
@ -367,7 +370,7 @@ namespace Avalonia.Media
characterIndex = glyphInfo.GlyphCluster;
if (currentX + advance > distance)
{
{
break;
}

395
src/Avalonia.Base/Media/GlyphTypeface.cs

@ -0,0 +1,395 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media.Fonts;
using Avalonia.Media.Fonts.Tables;
using Avalonia.Media.Fonts.Tables.Cmap;
using Avalonia.Media.Fonts.Tables.Metrics;
using Avalonia.Media.Fonts.Tables.Name;
using Avalonia.Platform;
namespace Avalonia.Media
{
/// <summary>
/// Represents a glyph typeface, providing access to font metrics, glyph mappings, and other font-related
/// properties.
/// </summary>
/// <remarks>The <see cref="GlyphTypeface"/> class is used to encapsulate font data, including metrics,
/// character-to-glyph mappings, and supported OpenType features. It supports platform-specific typefaces and
/// applies optional font simulations such as bold or oblique. This class is typically used in text rendering and
/// shaping scenarios.</remarks>
internal class GlyphTypeface : IGlyphTypeface
{
private bool _isDisposed;
private readonly NameTable? _nameTable;
private readonly OS2Table? _os2Table;
private readonly IReadOnlyDictionary<int, ushort> _cmapTable;
private readonly HorizontalHeaderTable? _hhTable;
private readonly VerticalHeaderTable? _vhTable;
private readonly HorizontalMetricsTable? _hmTable;
private readonly VerticalMetricsTable? _vmTable;
private IReadOnlyList<OpenTypeTag>? _supportedFeatures;
private ITextShaperTypeface? _textShaperTypeface;
/// <summary>
/// Initializes a new instance of the <see cref="GlyphTypeface"/> class with the specified platform typeface and
/// font simulations.
/// </summary>
/// <remarks>This constructor initializes the glyph typeface by loading various font tables,
/// including OS/2, CMAP, and metrics tables, to calculate font metrics and other properties. It also determines
/// font characteristics such as weight, style, stretch, and family names based on the provided typeface and
/// font simulations.</remarks>
/// <param name="typeface">The platform-specific typeface to be used for this <see cref="GlyphTypeface"/> instance. This parameter
/// cannot be <c>null</c>.</param>
/// <param name="fontSimulations">The font simulations to apply, such as bold or oblique. The default is <see cref="FontSimulations.None"/>.</param>
/// <exception cref="InvalidOperationException">Thrown if required font tables (e.g., 'maxp') cannot be loaded.</exception>
public GlyphTypeface(IPlatformTypeface typeface, FontSimulations fontSimulations = FontSimulations.None)
{
PlatformTypeface = typeface;
_os2Table = OS2Table.Load(this);
_cmapTable = CmapTable.Load(this);
var maxpTable = MaxpTable.Load(this) ?? throw new InvalidOperationException("Could not load the 'maxp' table.");
GlyphCount = maxpTable.NumGlyphs;
_hhTable = HorizontalHeaderTable.Load(this);
if (_hhTable is not null)
{
_hmTable = HorizontalMetricsTable.Load(this, _hhTable.NumberOfHMetrics, GlyphCount);
}
_vhTable = VerticalHeaderTable.Load(this);
if (_vhTable is not null)
{
_vmTable = VerticalMetricsTable.Load(this, _vhTable.NumberOfVMetrics, GlyphCount);
}
var ascent = 0;
var descent = 0;
var lineGap = 0;
if (_os2Table != null && (_os2Table.Selection & OS2Table.FontSelectionFlags.USE_TYPO_METRICS) != 0)
{
ascent = -_os2Table.TypoAscender;
descent = -_os2Table.TypoDescender;
lineGap = _os2Table.TypoLineGap;
}
else
{
if (_hhTable != null)
{
ascent = -_hhTable.Ascender;
descent = -_hhTable.Descender;
lineGap = _hhTable.LineGap;
}
}
if (_os2Table != null && (ascent == 0 || descent == 0))
{
if (_os2Table.TypoAscender != 0 || _os2Table.TypoDescender != 0)
{
ascent = -_os2Table.TypoAscender;
descent = -_os2Table.TypoDescender;
lineGap = _os2Table.TypoLineGap;
}
else
{
ascent = -_os2Table.WinAscent;
descent = _os2Table.WinDescent;
}
}
var headTable = HeadTable.Load(this);
var postTable = PostTable.Load(this);
var isFixedPitch = postTable.IsFixedPitch;
var underlineOffset = postTable.UnderlinePosition;
var underlineSize = postTable.UnderlineThickness;
Metrics = new FontMetrics
{
DesignEmHeight = (short)headTable.UnitsPerEm,
Ascent = ascent,
Descent = descent,
LineGap = lineGap,
UnderlinePosition = -underlineOffset,
UnderlineThickness = underlineSize,
StrikethroughPosition = -_os2Table?.StrikeoutPosition ?? 0,
StrikethroughThickness = _os2Table?.StrikeoutSize ?? 0,
IsFixedPitch = isFixedPitch
};
FontSimulations = fontSimulations;
var fontWeight = _os2Table != null ? (FontWeight)_os2Table.WeightClass : FontWeight.Normal;
Weight = (fontSimulations & FontSimulations.Bold) != 0 ? FontWeight.Bold : fontWeight;
var style = _os2Table != null ? GetFontStyle(_os2Table, headTable, postTable) : FontStyle.Normal;
Style = (fontSimulations & FontSimulations.Oblique) != 0 ? FontStyle.Italic : style;
var stretch = _os2Table != null ? (FontStretch)_os2Table.WidthClass : FontStretch.Normal;
Stretch = stretch;
_nameTable = NameTable.Load(this);
FamilyName = _nameTable?.FontFamilyName((ushort)CultureInfo.InvariantCulture.LCID) ?? "unknown";
TypographicFamilyName = _nameTable?.GetNameById((ushort)CultureInfo.InvariantCulture.LCID, KnownNameIds.TypographicFamilyName) ?? FamilyName;
if (_nameTable != null)
{
var familyNames = new Dictionary<CultureInfo, string>(1);
var faceNames = new Dictionary<CultureInfo, string>(1);
foreach (var nameRecord in _nameTable)
{
if (nameRecord.NameID == KnownNameIds.FontFamilyName)
{
if (nameRecord.Platform != Fonts.Tables.PlatformID.Windows || nameRecord.LanguageID == 0)
{
continue;
}
var culture = GetCulture(nameRecord.LanguageID);
if (!familyNames.ContainsKey(culture))
{
familyNames[culture] = nameRecord.Value;
}
}
if (nameRecord.NameID == KnownNameIds.FontSubfamilyName)
{
if (nameRecord.Platform != Fonts.Tables.PlatformID.Windows || nameRecord.LanguageID == 0)
{
continue;
}
var culture = GetCulture(nameRecord.LanguageID);
if (!faceNames.ContainsKey(culture))
{
faceNames[culture] = nameRecord.Value;
}
}
}
FamilyNames = familyNames;
FaceNames = faceNames;
}
else
{
FamilyNames = new Dictionary<CultureInfo, string> { { CultureInfo.InvariantCulture, FamilyName } };
FaceNames = new Dictionary<CultureInfo, string> { { CultureInfo.InvariantCulture, Weight.ToString() } };
}
static CultureInfo GetCulture(int lcid)
{
if (lcid == ushort.MaxValue)
{
return CultureInfo.InvariantCulture;
}
try
{
return CultureInfo.GetCultureInfo(lcid) ?? CultureInfo.InvariantCulture;
}
catch (CultureNotFoundException)
{
return CultureInfo.InvariantCulture;
}
}
}
public string TypographicFamilyName { get; }
public IReadOnlyDictionary<CultureInfo, string> FamilyNames { get; }
public IReadOnlyDictionary<CultureInfo, string> FaceNames { get; }
public IReadOnlyList<OpenTypeTag> SupportedFeatures
{
get
{
if (_supportedFeatures != null)
{
return _supportedFeatures;
}
var gPosFeatures = FeatureListTable.LoadGPos(this);
var gSubFeatures = FeatureListTable.LoadGSub(this);
var supportedFeatures = new List<OpenTypeTag>(gPosFeatures?.Features.Count ?? 0 + gSubFeatures?.Features.Count ?? 0);
if (gPosFeatures != null)
{
foreach (var gPosFeature in gPosFeatures.Features)
{
if (supportedFeatures.Contains(gPosFeature))
{
continue;
}
supportedFeatures.Add(gPosFeature);
}
}
if (gSubFeatures != null)
{
foreach (var gSubFeature in gSubFeatures.Features)
{
if (supportedFeatures.Contains(gSubFeature))
{
continue;
}
supportedFeatures.Add(gSubFeature);
}
}
_supportedFeatures = supportedFeatures;
return supportedFeatures;
}
}
public FontSimulations FontSimulations { get; }
public int ReplacementCodepoint { get; }
public FontMetrics Metrics { get; }
public uint GlyphCount { get; }
public string FamilyName { get; }
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
public IReadOnlyDictionary<int, ushort> CharacterToGlyphMap => _cmapTable;
public IPlatformTypeface PlatformTypeface { get; }
public ITextShaperTypeface TextShaperTypeface
{
get
{
if (_textShaperTypeface != null)
{
return _textShaperTypeface;
}
var textShaper = AvaloniaLocator.Current.GetRequiredService<ITextShaperImpl>();
_textShaperTypeface = textShaper.CreateTypeface(this);
return _textShaperTypeface;
}
}
private static FontStyle GetFontStyle(OS2Table oS2Table, HeadTable headTable, PostTable postTable)
{
var isItalic = (oS2Table.Selection & OS2Table.FontSelectionFlags.ITALIC) != 0 || (headTable.MacStyle & 0x02) != 0;
var isOblique = (oS2Table.Selection & OS2Table.FontSelectionFlags.OBLIQUE) != 0;
var italicAngle = postTable.ItalicAngle;
if (isOblique)
{
return FontStyle.Oblique;
}
if (Math.Abs(italicAngle) > 0.01f && !isItalic)
{
return FontStyle.Oblique;
}
if (isItalic)
{
return FontStyle.Italic;
}
return FontStyle.Normal;
}
private void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!disposing)
{
return;
}
PlatformTypeface.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public ushort GetGlyphAdvance(ushort glyphId)
{
if (_hmTable is null)
{
return 0;
}
return _hmTable.GetAdvance(glyphId);
}
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = default;
HorizontalGlyphMetric hMetric = default;
VerticalGlyphMetric vMetric = default;
if (_hmTable != null)
{
hMetric = _hmTable.GetMetrics(glyph);
}
if (_vmTable != null)
{
vMetric = _vmTable.GetMetrics(glyph);
}
if (hMetric.Equals(default) && vMetric.Equals(default))
{
return false;
}
metrics = new GlyphMetrics
{
XBearing = hMetric.LeftSideBearing,
YBearing = vMetric.TopSideBearing,
Width = hMetric.AdvanceWidth,
Height = vMetric.AdvanceHeight
};
return true;
}
}
}

169
src/Avalonia.Base/Media/IGlyphTypeface.cs

@ -1,16 +1,74 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media.Fonts;
using Avalonia.Metadata;
namespace Avalonia.Media
{
/// <summary>
/// Represents a typeface that provides access to font-related information and operations, such as glyph metrics,
/// supported OpenType features, and culture-specific names.
/// </summary>
/// <remarks>The <see cref="IGlyphTypeface"/> interface is designed for advanced text rendering and layout
/// scenarios. It provides detailed information about a font's characteristics, including its family name, style,
/// weight, and stretch, as well as mappings between Unicode code points and glyph indices. <para> This interface
/// also supports retrieving culture-specific names for font families and faces, accessing OpenType features, and
/// obtaining glyph metrics for precise text shaping and rendering. </para> <para> Implementations of this interface
/// are expected to be disposable, as they may hold unmanaged resources related to font handling. </para></remarks>
[Unstable]
public interface IGlyphTypeface : IDisposable
{
/// <summary>
/// Gets the family name for the <see cref="IGlyphTypeface"/> object.
/// Gets the family name.
/// </summary>
string FamilyName { get; }
/// <summary>
/// Gets the typographic family name.
/// </summary>
/// <remarks>
/// The typographic family name is an alternate family name that may be used for stylistic or typographic purposes.
/// <para>
/// Example: For the fonts "Inter Light" and "Inter Condensed", the <c>FamilyName</c> values are "Inter Light" and "Inter Condensed" respectively,
/// but both share the same <c>TypographicFamilyName</c> of "Inter".
/// </para>
/// </remarks>
string TypographicFamilyName { get; }
/// <summary>
/// Gets a read-only dictionary that maps culture-specific information to the family name.
/// </summary>
/// <remarks>This property provides localized family names for different cultures. The dictionary is never empty.
/// If a specific culture is not present in the dictionary, the caller may need to handle fallback logic to a default culture
/// or name.</remarks>
IReadOnlyDictionary<CultureInfo, string> FamilyNames { get; }
/// <summary>
/// Gets a read-only list of supported OpenType features.
/// </summary>
IReadOnlyList<OpenTypeTag> SupportedFeatures { get; }
/// <summary>
/// Gets a read-only dictionary that maps culture-specific information to corresponding face names.
/// </summary>
/// <remarks>
/// The dictionary provides a way to retrieve face names localized for specific cultures.
/// If a culture is not present in the dictionary, it indicates that no face name is defined for that
/// culture.
/// <para>
/// Example: For a font family "Arial", common face names include "Regular", "Bold", "Italic", "Bold Italic".
/// The dictionary might contain entries such as:
/// <code>
/// en-US: "Bold Italic"
/// de-DE: "Fett Kursiv"
/// </code>
/// </para>
/// </remarks>
IReadOnlyDictionary<CultureInfo, string> FaceNames { get; }
/// <summary>
/// Gets the designed weight of the font represented by the <see cref="IGlyphTypeface"/> object.
/// </summary>
@ -27,22 +85,45 @@ namespace Avalonia.Media
FontStretch Stretch { get; }
/// <summary>
/// Gets the number of glyphs held by this glyph typeface.
/// Gets the number of glyphs held by this <see cref="IGlyphTypeface"/> object.
/// </summary>
int GlyphCount { get; }
uint GlyphCount { get; }
/// <summary>
/// Gets the font metrics.
/// Gets the algorithmic style simulations applied to <see cref="IGlyphTypeface"/> object.
/// </summary>
FontSimulations FontSimulations { get; }
/// <summary>
/// Gets the font metrics associated with the current font.
/// </summary>
/// <returns>
/// The font metrics.
/// </returns>
FontMetrics Metrics { get; }
/// <summary>
/// Gets the algorithmic style simulations applied to this glyph typeface.
/// Gets the nominal mapping of a Unicode code point to a glyph index as defined by the font 'CMAP' table.
/// </summary>
FontSimulations FontSimulations { get; }
IReadOnlyDictionary<int, ushort> CharacterToGlyphMap { get; }
/// <summary>
/// Gets the glyph typeface associated with the <see cref="IGlyphTypeface"/>.
/// </summary>
IPlatformTypeface PlatformTypeface { get; }
/// <summary>
/// Gets the typeface used for text shaping operations.
/// </summary>
/// <remarks>The typeface is used to determine glyphs and their positioning during text shaping.
/// This property is typically used in scenarios involving advanced text layout or rendering.</remarks>
ITextShaperTypeface TextShaperTypeface { get; }
/// <summary>
/// Returns the glyph advance for the specified glyph.
/// </summary>
/// <param name="glyph">The glyph.</param>
/// <returns>
/// The advance.
/// </returns>
ushort GetGlyphAdvance(ushort glyph);
/// <summary>
/// Tries to get a glyph's metrics in em units.
@ -53,62 +134,48 @@ namespace Avalonia.Media
/// <c>true</c> if an glyph's metrics was found, <c>false</c> otherwise.
/// </returns>
bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics);
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>
/// <remarks>
/// Returns <c>0</c> if a glyph isn't found.
/// </remarks>
/// <param name="codepoint">The codepoint.</param>
/// <returns>
/// A glyph index.
/// </returns>
ushort GetGlyph(uint codepoint);
}
public interface IPlatformTypeface : IFontMemory
{
/// <summary>
/// Tries to get an glyph index for specified codepoint.
/// Gets the designed weight of the font represented by the <see cref="IPlatformTypeface"/> object.
/// </summary>
/// <param name="codepoint">The codepoint.</param>
/// <param name="glyph">A glyph index.</param>
/// <returns>
/// <c>true</c> if an glyph index was found, <c>false</c> otherwise.
/// </returns>
bool TryGetGlyph(uint codepoint, out ushort glyph);
FontWeight Weight { get; }
/// <summary>
/// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as <code>0</code>.
/// Gets the style for the <see cref="IPlatformTypeface"/> object.
/// </summary>
/// <param name="codepoints">The codepoints to map.</param>
/// <returns>
/// An array of glyph indices.
/// </returns>
ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints);
FontStyle Style { get; }
/// <summary>
/// Returns the glyph advance for the specified glyph.
/// Gets the <see cref="FontStretch"/> value for the <see cref="IPlatformTypeface"/> object.
/// </summary>
/// <param name="glyph">The glyph.</param>
/// <returns>
/// The advance.
/// </returns>
int GetGlyphAdvance(ushort glyph);
FontStretch Stretch { get; }
/// <summary>
/// Returns an array of glyph advances in design em size.
/// Returns the font file stream represented by the <see cref="IGlyphTypeface"/>.
/// </summary>
/// <param name="glyphs">The glyph indices.</param>
/// <returns>
/// An array of glyph advances.
/// </returns>
int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs);
/// <param name="stream">The stream.</param>
/// <returns>Returns <c>true</c> if the stream can be obtained, otherwise <c>false</c>.</returns>
bool TryGetStream([NotNullWhen(true)] out Stream? stream);
}
public interface ITextShaperTypeface : IDisposable
{
}
public interface IFontMemory : IDisposable
{
/// <summary>
/// Returns the contents of the table data for the specified tag.
/// Attempts to retrieve the memory block associated with the specified OpenType table tag.
/// </summary>
/// <param name="tag">The table tag to get the data for.</param>
/// <param name="table">The contents of the table data for the specified tag.</param>
/// <returns>Returns <c>true</c> if the content exists, otherwise <c>false</c>.</returns>
bool TryGetTable(uint tag, out byte[] table);
/// <param name="tag">The OpenType table tag identifying the table to retrieve.</param>
/// <param name="table">When this method returns, contains the memory block of the specified table if the operation succeeds;
/// otherwise, contains an empty memory block. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if the memory block for the specified table tag was successfully retrieved;
/// otherwise, <see langword="false"/>.</returns>
bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table);
}
}

39
src/Avalonia.Base/Media/IGlyphTypeface2.cs

@ -1,39 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Avalonia.Media.Fonts;
namespace Avalonia.Media
{
internal interface IGlyphTypeface2 : IGlyphTypeface
{
/// <summary>
/// Returns the font file stream represented by the <see cref="IGlyphTypeface"/> object.
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns>Returns <c>true</c> if the stream can be obtained, otherwise <c>false</c>.</returns>
bool TryGetStream([NotNullWhen(true)] out Stream? stream);
/// <summary>
/// Gets the typographic family name.
/// </summary>
string TypographicFamilyName { get; }
/// <summary>
/// Gets the localized family names.
/// <para>Keys are culture identifiers.</para>
/// </summary>
IReadOnlyDictionary<ushort, string> FamilyNames { get; }
/// <summary>
/// Gets supported font features.
/// </summary>
IReadOnlyList<OpenTypeTag> SupportedFeatures { get; }
/// <summary>
/// Gets the localized face names.
/// <para>Keys are culture identifiers.</para>
/// </summary>
IReadOnlyDictionary<ushort, string> FaceNames { get; }
}
}

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

@ -146,7 +146,7 @@ namespace Avalonia.Media.TextFormatting
//Move forward until we reach the next base character
while (enumerator.MoveNext(out grapheme))
{
if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.CharacterToGlyphMap.TryGetValue(grapheme.FirstCodepoint, out _))
{
break;
}
@ -194,15 +194,15 @@ namespace Avalonia.Media.TextFormatting
if (!currentCodepoint.IsWhiteSpace
&& defaultGlyphTypeface != null
&& defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _))
&& defaultGlyphTypeface.CharacterToGlyphMap.TryGetValue(currentCodepoint, out _))
{
break;
}
//Stop at the first missing glyph
if (!currentCodepoint.IsBreakChar &&
currentCodepoint.GeneralCategory != GeneralCategory.Control &&
!glyphTypeface.TryGetGlyph(currentCodepoint, out _))
if (!currentCodepoint.IsBreakChar &&
currentCodepoint.GeneralCategory != GeneralCategory.Control &&
!glyphTypeface.CharacterToGlyphMap.TryGetValue(currentCodepoint, out _))
{
break;
}

2
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -701,7 +701,7 @@ namespace Avalonia.Media.TextFormatting
var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.CachedGlyphTypeface;
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyph = glyphTypeface.CharacterToGlyphMap[s_empty[0]];
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) };
var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize,

8
src/Avalonia.Base/Platform/IFontManagerImpl.cs

@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Metadata;
namespace Avalonia.Platform
@ -30,12 +29,12 @@ namespace Avalonia.Platform
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStretch">The font stretch.</param>
/// <param name="culture">The culture.</param>
/// <param name="typeface">The matching typeface.</param>
/// <param name="platformTypeface">The matching typeface.</param>
/// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface);
FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, [NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface);
/// <summary>
/// Tries to get a glyph typeface for specified parameters.
@ -61,10 +60,7 @@ namespace Avalonia.Platform
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
}
internal interface IFontManagerImpl2 : IFontManagerImpl
{
/// <summary>
/// Tries to get a list of typefaces for the specified family name.
/// </summary>

4
src/Avalonia.Base/Platform/IGlyphRunImpl.cs

@ -11,10 +11,6 @@ namespace Avalonia.Platform
[Unstable]
public interface IGlyphRunImpl : IDisposable
{
/// <summary>
/// Gets the <see cref="IGlyphTypeface"/> for the <see cref="IGlyphRunImpl"/>.
/// </summary>
IGlyphTypeface GlyphTypeface { get; }
/// <summary>
/// Gets the em size used for rendering the <see cref="IGlyphRunImpl"/>.

11
src/Avalonia.Base/Platform/ITextShaperImpl.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
@ -17,5 +18,13 @@ namespace Avalonia.Platform
/// <param name="options">Text shaper options to customize the shaping process.</param>
/// <returns>A shaped glyph run.</returns>
ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options);
}
/// <summary>
/// Creates a text shaper typeface based on the specified glyph typeface.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface to use as the basis for the text shaper typeface.</param>
/// <returns>An instance of <see cref="ITextShaperTypeface"/> that represents the text shaping functionality for the
/// specified glyph typeface.</returns>
ITextShaperTypeface CreateTypeface(IGlyphTypeface glyphTypeface);
}
}

7
src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs

@ -1,6 +1,5 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Server
{
@ -30,15 +29,15 @@ namespace Avalonia.Rendering.Composition.Server
return maxHeight;
}
public DiagnosticTextRenderer(IGlyphTypeface typeface, double fontRenderingEmSize)
public DiagnosticTextRenderer(IGlyphTypeface glyphTypeface, double fontRenderingEmSize)
{
var chars = new char[LastChar - FirstChar + 1];
for (var c = FirstChar; c <= LastChar; c++)
{
var index = c - FirstChar;
chars[index] = c;
var glyph = typeface.GetGlyph(c);
_runs[index] = new GlyphRun(typeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph });
var glyph = glyphTypeface.CharacterToGlyphMap[c];
_runs[index] = new GlyphRun(glyphTypeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph });
}
}

9
src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs

@ -1,5 +1,4 @@
using Avalonia.Compatibility;
using Avalonia.Controls;
using Avalonia.Logging;
namespace Avalonia
@ -8,6 +7,9 @@ namespace Avalonia
{
public static AppBuilder UsePlatformDetect(this AppBuilder builder)
{
// Always load HarfBuzz on desktop platforms
LoadHarfBuzz(builder);
// We don't have the ability to load every assembly right now, so we are
// stuck with manual configuration here
// Helpers are extracted to separate methods to take the advantage of the fact
@ -20,7 +22,7 @@ namespace Avalonia
LoadWin32(builder);
LoadSkia(builder);
}
else if(OperatingSystemEx.IsMacOS())
else if (OperatingSystemEx.IsMacOS())
{
LoadAvaloniaNative(builder);
LoadSkia(builder);
@ -49,5 +51,8 @@ namespace Avalonia
static void LoadSkia(AppBuilder builder)
=> builder.UseSkia();
static void LoadHarfBuzz(AppBuilder builder)
=> builder.UseHarfBuzz();
}
}

1
src/Avalonia.Desktop/Avalonia.Desktop.csproj

@ -9,6 +9,7 @@
<ProjectReference Include="../../src/Avalonia.Native/Avalonia.Native.csproj" />
<ProjectReference Include="../../packages/Avalonia/Avalonia.csproj" />
<ProjectReference Include="../Avalonia.X11/Avalonia.X11.csproj" />
<ProjectReference Include="..\Harfbuzz\Avalonia.Harfbuzz\Avalonia.Harfbuzz.csproj" />
</ItemGroup>
<Import Project="..\..\build\TrimmingEnable.props" />

22
src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.Harfbuzz.csproj

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks);netstandard2.0</TargetFrameworks>
<IncludeLinuxSkia>true</IncludeLinuxSkia>
<IncludeWasmSkia>true</IncludeWasmSkia>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- No obsolete code usage -->
<WarningsAsErrors>$(WarningsAsErrors);CS0618</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
</Project>

27
src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzApplicationExtensions.cs

@ -0,0 +1,27 @@
using Avalonia.Harfbuzz;
using Avalonia.Platform;
namespace Avalonia
{
/// <summary>
/// Configures the application to use HarfBuzz for text shaping.
/// </summary>
/// <remarks>This method adds a HarfBuzz-based text shaper implementation to the application, enabling
/// advanced text shaping capabilities.</remarks>
public static class HarfBuzzApplicationExtensions
{
/// <summary>
/// Configures the application to use HarfBuzz for text shaping.
/// </summary>
/// <remarks>This method integrates HarfBuzz, a text shaping engine, into the application,
/// enabling advanced text layout and rendering capabilities.</remarks>
/// <param name="builder">The <see cref="AppBuilder"/> instance to configure.</param>
/// <returns>The configured <see cref="AppBuilder"/> instance.</returns>
public static AppBuilder UseHarfBuzz(this AppBuilder builder)
{
return builder.With<ITextShaperImpl>(new HarfBuzzTextShaper());
}
}
}

34
src/Skia/Avalonia.Skia/TextShaperImpl.cs → src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTextShaper.cs

@ -3,6 +3,7 @@ using System.Buffers;
using System.Collections.Concurrent;
using System.Globalization;
using System.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
@ -10,9 +11,9 @@ using HarfBuzzSharp;
using Buffer = HarfBuzzSharp.Buffer;
using GlyphInfo = HarfBuzzSharp.GlyphInfo;
namespace Avalonia.Skia
namespace Avalonia.Harfbuzz
{
internal class TextShaperImpl : ITextShaperImpl
public class HarfBuzzTextShaper : ITextShaperImpl
{
[ThreadStatic]
private static Buffer? s_buffer;
@ -22,7 +23,14 @@ namespace Avalonia.Skia
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
{
var textSpan = text.Span;
var typeface = options.Typeface;
var glyphTypeface = options.Typeface;
if (glyphTypeface.TextShaperTypeface is not HarfBuzzTypeface harfBuzzTypeface)
{
throw new NotSupportedException("The provided GlyphTypeface is not supported by this text shaper.");
}
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
var culture = options.Culture;
@ -45,7 +53,7 @@ namespace Avalonia.Skia
buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture));
var font = ((GlyphTypefaceImpl)typeface).Font;
var font = harfBuzzTypeface.HBFont;
font.Shape(buffer, GetFeatures(options));
@ -60,7 +68,7 @@ namespace Avalonia.Skia
var bufferLength = buffer.Length;
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var shapedBuffer = new ShapedBuffer(text, bufferLength, glyphTypeface, fontRenderingEmSize, bidiLevel);
var glyphInfos = buffer.GetGlyphInfoSpan();
@ -80,11 +88,11 @@ namespace Avalonia.Skia
if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
glyphIndex = glyphTypeface.CharacterToGlyphMap[' '];
glyphAdvance = options.IncrementalTabWidth > 0 ?
options.IncrementalTabWidth :
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
4 * glyphTypeface.GetGlyphAdvance(glyphIndex) * textScale;
}
shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
@ -93,6 +101,12 @@ namespace Avalonia.Skia
return shapedBuffer;
}
public ITextShaperTypeface CreateTypeface(IGlyphTypeface glyphTypeface)
{
return new HarfBuzzTypeface(glyphTypeface);
}
private static void MergeBreakPair(Buffer buffer)
{
var length = buffer.Length;
@ -190,18 +204,18 @@ namespace Avalonia.Skia
}
var features = new Feature[options.FontFeatures.Count];
for (var i = 0; i < options.FontFeatures.Count; i++)
{
var fontFeature = options.FontFeatures[i];
features[i] = new Feature(
Tag.Parse(fontFeature.Tag),
Tag.Parse(fontFeature.Tag),
(uint)fontFeature.Value,
(uint)fontFeature.Start,
(uint)fontFeature.End);
}
return features;
}
}

67
src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTypeface.cs

@ -0,0 +1,67 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Media;
using HarfBuzzSharp;
namespace Avalonia.Harfbuzz
{
internal class HarfBuzzTypeface : ITextShaperTypeface
{
public HarfBuzzTypeface(IGlyphTypeface glyphTypeface)
{
GlyphTypeface = glyphTypeface;
HBFace = new Face(GetTable) { UnitsPerEm = glyphTypeface.Metrics.DesignEmHeight };
HBFont = new Font(HBFace);
HBFont.SetFunctionsOpenType();
}
public IGlyphTypeface GlyphTypeface { get; }
public Face HBFace { get; }
public Font HBFont { get; }
private Blob? GetTable(Face face, Tag tag)
{
if (!GlyphTypeface.PlatformTypeface.TryGetTable((uint)tag, out var table))
{
return null;
}
// If table is backed by managed array, pin it and avoid copy.
if (MemoryMarshal.TryGetArray(table, out var seg))
{
var handle = GCHandle.Alloc(seg.Array!, GCHandleType.Pinned);
var basePtr = handle.AddrOfPinnedObject();
var ptr = IntPtr.Add(basePtr, seg.Offset);
var release = new ReleaseDelegate(() => handle.Free());
return new Blob(ptr, seg.Count, MemoryMode.ReadOnly, release);
}
// Fallback: allocate native memory and copy
var nativePtr = Marshal.AllocHGlobal(table.Length);
unsafe
{
fixed (byte* src = table.Span)
{
System.Buffer.MemoryCopy(src, (void*)nativePtr, table.Length, table.Length);
}
}
var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeHGlobal(nativePtr));
return new Blob(nativePtr, table.Length, MemoryMode.ReadOnly, releaseDelegate);
}
public void Dispose()
{
HBFont.Dispose();
HBFace.Dispose();
}
}
}

1
src/Headless/Avalonia.Headless/Avalonia.Headless.csproj

@ -5,6 +5,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\DevAnalyzers.props" />

7
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -5,11 +5,9 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
namespace Avalonia.Headless
{
@ -19,8 +17,7 @@ namespace Avalonia.Headless
{
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface())
.Bind<IFontManagerImpl>().ToConstant(new HeadlessFontManagerStub())
.Bind<ITextShaperImpl>().ToConstant(new HeadlessTextShaperStub());
.Bind<IFontManagerImpl>().ToConstant(new HeadlessFontManagerStub());
}
public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) => this;

367
src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -3,14 +3,14 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
namespace Avalonia.Headless
@ -66,34 +66,20 @@ namespace Avalonia.Headless
}
}
internal class HeadlessGlyphTypefaceImpl : IGlyphTypeface
internal class HeadlessPlatformTypeface : IPlatformTypeface
{
public HeadlessGlyphTypefaceImpl(string familyName, FontStyle style, FontWeight weight, FontStretch stretch)
{
FamilyName = familyName;
Style = style;
Weight = weight;
Stretch = stretch;
}
private readonly UnmanagedFontMemory _fontMemory;
public FontMetrics Metrics => new FontMetrics
public HeadlessPlatformTypeface(Stream stream)
{
DesignEmHeight = 10,
Ascent = 2,
Descent = 10,
IsFixedPitch = true,
LineGap = 0,
UnderlinePosition = 2,
UnderlineThickness = 1,
StrikethroughPosition = 2,
StrikethroughThickness = 1
};
_fontMemory = UnmanagedFontMemory.LoadFromStream(stream);
public int GlyphCount => 1337;
var dummy = new GlyphTypeface(this, FontSimulations.None);
public FontSimulations FontSimulations => FontSimulations.None;
public string FamilyName { get; }
Weight = dummy.Weight;
Style = dummy.Style;
Stretch = dummy.Stretch;
}
public FontWeight Weight { get; }
@ -103,260 +89,217 @@ namespace Avalonia.Headless
public void Dispose()
{
_fontMemory.Dispose();
}
public ushort GetGlyph(uint codepoint)
public bool TryGetStream([NotNullWhen(true)] out Stream? stream)
{
return (ushort)codepoint;
}
var data = _fontMemory.Memory.Span;
public bool TryGetGlyph(uint codepoint, out ushort glyph)
{
glyph = 8;
stream = new MemoryStream(data.ToArray());
return true;
}
public int GetGlyphAdvance(ushort glyph)
{
return 8;
}
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table) => _fontMemory.TryGetTable(tag, out table);
}
}
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
var advances = new int[glyphs.Length];
internal class HeadlessGlyphTypeface : IGlyphTypeface
{
private readonly IGlyphTypeface _inner;
for (var i = 0; i < advances.Length; i++)
{
advances[i] = 8;
}
public HeadlessGlyphTypeface(IGlyphTypeface inner, string familyName)
{
_inner = inner;
FamilyName = familyName;
}
return advances;
}
public string FamilyName { get; }
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
{
return codepoints.ToArray().Select(x => (ushort)x).ToArray();
}
public string TypographicFamilyName => FamilyName;
public bool TryGetTable(uint tag, out byte[] table)
{
table = null!;
return false;
}
public IReadOnlyDictionary<CultureInfo, string> FamilyNames => _inner.FamilyNames;
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = new GlyphMetrics
{
Width = 10,
Height = 10
};
public IReadOnlyList<OpenTypeTag> SupportedFeatures => _inner.SupportedFeatures;
return true;
}
}
public IReadOnlyDictionary<CultureInfo, string> FaceNames => _inner.FaceNames;
internal class HeadlessTextShaperStub : ITextShaperImpl
{
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
var textSpan = text.Span;
var textStartIndex = TextTestHelper.GetStartCharIndex(text);
for (var i = 0; i < shapedBuffer.Length;)
{
var glyphCluster = i + textStartIndex;
public FontWeight Weight => _inner.Weight;
var codepoint = Codepoint.ReadAt(textSpan, i, out var count);
public FontStyle Style => _inner.Style;
var glyphIndex = typeface.GetGlyph(codepoint);
public FontStretch Stretch => _inner.Stretch;
for (var j = 0; j < count; ++j)
{
shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10);
}
public uint GlyphCount => _inner.GlyphCount;
i += count;
}
public FontSimulations FontSimulations => _inner.FontSimulations;
return shapedBuffer;
}
}
public FontMetrics Metrics => _inner.Metrics;
internal class HeadlessFontManagerStub : IFontManagerImpl
{
private readonly string _defaultFamilyName;
public IReadOnlyDictionary<int, ushort> CharacterToGlyphMap => _inner.CharacterToGlyphMap;
public HeadlessFontManagerStub(string defaultFamilyName = "Default")
{
_defaultFamilyName = defaultFamilyName;
}
public IPlatformTypeface PlatformTypeface => _inner.PlatformTypeface;
public int TryCreateGlyphTypefaceCount { get; private set; }
public ITextShaperTypeface TextShaperTypeface => _inner.TextShaperTypeface;
public string GetDefaultFontFamilyName()
{
return _defaultFamilyName;
}
public void Dispose() => _inner.Dispose();
string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{
return new[] { _defaultFamilyName };
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch,
CultureInfo? culture, out Typeface fontKey)
{
fontKey = new Typeface(_defaultFamilyName);
return false;
}
public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
public ushort GetGlyphAdvance(ushort glyph) => _inner.GetGlyphAdvance(glyph);
TryCreateGlyphTypefaceCount++;
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) => _inner.TryGetGlyphMetrics(glyph, out metrics);
}
if (familyName == "Unknown")
{
return false;
}
internal class HeadlessFontManagerStub : IFontManagerImpl
{
private readonly string _defaultFamilyName = "avares://Avalonia.Fonts.Inter/Assets#Inter";
glyphTypeface = new HeadlessGlyphTypefaceImpl(familyName, style, weight, stretch);
public int TryCreateGlyphTypefaceCount { get; private set; }
return true;
}
public string GetDefaultFontFamilyName() => _defaultFamilyName;
public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface)
{
glyphTypeface = new HeadlessGlyphTypefaceImpl(
FontFamily.DefaultFontFamilyName,
fontSimulations.HasFlag(FontSimulations.Oblique) ? FontStyle.Italic : FontStyle.Normal,
fontSimulations.HasFlag(FontSimulations.Bold) ? FontWeight.Bold : FontWeight.Normal,
FontStretch.Normal);
string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{
return new[] { _defaultFamilyName };
}
TryCreateGlyphTypefaceCount++;
public bool TryMatchCharacter(
int codepoint,
FontStyle fontStyle,
FontWeight fontWeight,
FontStretch fontStretch,
CultureInfo? culture,
out IPlatformTypeface platformTypeface)
{
platformTypeface = null!;
return true;
}
return false;
}
internal class HeadlessFontManagerWithMultipleSystemFontsStub : IFontManagerImpl
public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
private readonly string[] _installedFontFamilyNames;
private readonly string _defaultFamilyName;
glyphTypeface = null;
public HeadlessFontManagerWithMultipleSystemFontsStub(
string[] installedFontFamilyNames,
string defaultFamilyName = "Default")
if (familyName == "MyFont")
{
_installedFontFamilyNames = installedFontFamilyNames;
_defaultFamilyName = defaultFamilyName;
glyphTypeface = new HeadlessGlyphTypeface(Typeface.Default.GlyphTypeface, familyName);
}
public int TryCreateGlyphTypefaceCount { get; private set; }
TryCreateGlyphTypefaceCount++;
public string GetDefaultFontFamilyName()
{
return _defaultFamilyName;
}
return glyphTypeface != null;
}
string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{
return _installedFontFamilyNames;
}
public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface)
{
glyphTypeface = new GlyphTypeface(new HeadlessPlatformTypeface(stream), fontSimulations);
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch,
CultureInfo? culture, out Typeface fontKey)
{
fontKey = new Typeface(_defaultFamilyName);
TryCreateGlyphTypefaceCount++;
return false;
}
return true;
}
public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
throw new NotImplementedException();
}
}
TryCreateGlyphTypefaceCount++;
internal class HeadlessFontManagerWithMultipleSystemFontsStub : IFontManagerImpl
{
private readonly string[] _installedFontFamilyNames;
private readonly string _defaultFamilyName;
if (familyName == "Unknown")
{
return false;
}
public HeadlessFontManagerWithMultipleSystemFontsStub(
string[] installedFontFamilyNames,
string defaultFamilyName = "Default")
{
_installedFontFamilyNames = installedFontFamilyNames;
_defaultFamilyName = defaultFamilyName;
}
glyphTypeface = new HeadlessGlyphTypefaceImpl(familyName, style, weight, stretch);
public int TryCreateGlyphTypefaceCount { get; private set; }
return true;
}
string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{
return _installedFontFamilyNames;
}
public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface)
{
glyphTypeface = new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal);
public string GetDefaultFontFamilyName()
{
return _defaultFamilyName;
}
return true;
}
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
throw new NotImplementedException();
}
internal class HeadlessIconLoaderStub : IPlatformIconLoader
public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
private class IconStub : IWindowIconImpl
{
public void Save(Stream outputStream)
{
throw new NotImplementedException();
}
}
}
public IWindowIconImpl LoadIcon(string fileName)
{
return new IconStub();
}
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
throw new NotImplementedException();
}
public IWindowIconImpl LoadIcon(Stream stream)
{
return new IconStub();
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface)
{
throw new NotImplementedException();
}
}
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
internal class HeadlessIconLoaderStub : IPlatformIconLoader
{
private class IconStub : IWindowIconImpl
{
public void Save(Stream outputStream)
{
return new IconStub();
}
}
public IWindowIconImpl LoadIcon(string fileName)
{
return new IconStub();
}
internal class HeadlessScreensStub : ScreensBase<int, PlatformScreen>
public IWindowIconImpl LoadIcon(Stream stream)
{
protected override IReadOnlyList<int> GetAllScreenKeys() => new[] { 1 };
return new IconStub();
}
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
{
return new IconStub();
}
}
internal class HeadlessScreensStub : ScreensBase<int, PlatformScreen>
{
protected override IReadOnlyList<int> GetAllScreenKeys() => new[] { 1 };
protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key);
protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key);
private class PlatformScreenStub : PlatformScreen
private class PlatformScreenStub : PlatformScreen
{
public PlatformScreenStub(int key) : base(new PlatformHandle((nint)key, nameof(HeadlessScreensStub)))
{
public PlatformScreenStub(int key) : base(new PlatformHandle((nint)key, nameof(HeadlessScreensStub)))
{
Scaling = 1;
Bounds = WorkingArea = new PixelRect(0, 0, 1920, 1280);
IsPrimary = true;
}
Scaling = 1;
Bounds = WorkingArea = new PixelRect(0, 0, 1920, 1280);
IsPrimary = true;
}
}
}
internal static class TextTestHelper
internal static class TextTestHelper
{
public static int GetStartCharIndex(ReadOnlyMemory<char> text)
{
public static int GetStartCharIndex(ReadOnlyMemory<char> text)
{
if (!MemoryMarshal.TryGetString(text, out _, out var start, out _))
throw new InvalidOperationException("text memory should have been a string");
return start;
}
if (!MemoryMarshal.TryGetString(text, out _, out var start, out _))
throw new InvalidOperationException("text memory should have been a string");
return start;
}
}

24
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -9,7 +9,7 @@ using SkiaSharp;
namespace Avalonia.Skia
{
internal class FontManagerImpl : IFontManagerImpl, IFontManagerImpl2
internal class FontManagerImpl : IFontManagerImpl
{
private SKFontManager _skFontManager = SKFontManager.Default;
@ -30,8 +30,13 @@ namespace Avalonia.Skia
[ThreadStatic] private static string[]? t_languageTagBuffer;
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface fontKey)
public bool TryMatchCharacter(
int codepoint,
FontStyle fontStyle,
FontWeight fontWeight,
FontStretch fontStretch,
CultureInfo? culture,
[NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface)
{
SKFontStyle skFontStyle;
@ -63,17 +68,12 @@ namespace Avalonia.Skia
if (skTypeface != null)
{
// ToDo: create glyph typeface here to get the correct style/weight/stretch
fontKey = new Typeface(
skTypeface.FamilyName,
skTypeface.FontStyle.Slant.ToAvalonia(),
(FontWeight)skTypeface.FontStyle.Weight,
(FontStretch)skTypeface.FontStyle.Width);
platformTypeface = new SkiaTypeface(skTypeface, FontSimulations.None);
return true;
}
fontKey = default;
platformTypeface = null;
return false;
}
@ -104,7 +104,7 @@ namespace Avalonia.Skia
fontSimulations |= FontSimulations.Oblique;
}
glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);
glyphTypeface = new GlyphTypeface(new SkiaTypeface(skTypeface, fontSimulations), fontSimulations);
return true;
}
@ -115,7 +115,7 @@ namespace Avalonia.Skia
if (skTypeface != null)
{
glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);
glyphTypeface = new GlyphTypeface(new SkiaTypeface(skTypeface, fontSimulations), fontSimulations);
return true;
}

6
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@ -11,7 +11,7 @@ namespace Avalonia.Skia
{
internal class GlyphRunImpl : IGlyphRunImpl
{
private readonly GlyphTypefaceImpl _glyphTypefaceImpl;
private readonly SkiaTypeface _glyphTypefaceImpl;
private readonly ushort[] _glyphIndices;
private readonly SKPoint[] _glyphPositions;
@ -36,7 +36,7 @@ namespace Avalonia.Skia
throw new ArgumentNullException(nameof(glyphInfos));
}
_glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
_glyphTypefaceImpl = (SkiaTypeface)glyphTypeface.PlatformTypeface;
FontRenderingEmSize = fontRenderingEmSize;
var count = glyphInfos.Count;
@ -86,8 +86,6 @@ namespace Avalonia.Skia
Bounds = runBounds.Translate(new Vector(baselineOrigin.X, baselineOrigin.Y));
}
public IGlyphTypeface GlyphTypeface => _glyphTypefaceImpl;
public double FontRenderingEmSize { get; }
public Point BaselineOrigin { get; }

393
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -1,393 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Media.Fonts.Tables;
using Avalonia.Media.Fonts.Tables.Name;
using HarfBuzzSharp;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class GlyphTypefaceImpl : IGlyphTypeface2
{
private bool _isDisposed;
private readonly NameTable? _nameTable;
private readonly OS2Table? _os2Table;
private readonly HorizontalHeadTable? _hhTable;
private IReadOnlyList<OpenTypeTag>? _supportedFeatures;
public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations)
{
SKTypeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
Face = new Face(GetTable) { UnitsPerEm = typeface.UnitsPerEm };
Font = new Font(Face);
Font.SetFunctionsOpenType();
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlineOffset);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineSize);
_os2Table = OS2Table.Load(this);
_hhTable = HorizontalHeadTable.Load(this);
var ascent = 0;
var descent = 0;
var lineGap = 0;
if (_os2Table != null && (_os2Table.FontStyle & OS2Table.FontStyleSelection.USE_TYPO_METRICS) != 0)
{
ascent = -_os2Table.TypoAscender;
descent = -_os2Table.TypoDescender;
lineGap = _os2Table.TypoLineGap;
}
else
{
if (_hhTable != null)
{
ascent = -_hhTable.Ascender;
descent = -_hhTable.Descender;
lineGap = _hhTable.LineGap;
}
}
if (_os2Table != null && (ascent == 0 || descent == 0))
{
if (_os2Table.TypoAscender != 0 || _os2Table.TypoDescender != 0)
{
ascent = -_os2Table.TypoAscender;
descent = -_os2Table.TypoDescender;
lineGap = _os2Table.TypoLineGap;
}
else
{
ascent = -_os2Table.WinAscent;
descent = _os2Table.WinDescent;
}
}
Metrics = new FontMetrics
{
DesignEmHeight = (short)Face.UnitsPerEm,
Ascent = ascent,
Descent = descent,
LineGap = lineGap,
UnderlinePosition = -underlineOffset,
UnderlineThickness = underlineSize,
StrikethroughPosition = -_os2Table?.StrikeoutPosition ?? 0,
StrikethroughThickness = _os2Table?.StrikeoutSize ?? 0,
IsFixedPitch = typeface.IsFixedPitch
};
GlyphCount = typeface.GlyphCount;
FontSimulations = fontSimulations;
var fontWeight = _os2Table != null ? (FontWeight)_os2Table.WeightClass : FontWeight.Normal;
Weight = (fontSimulations & FontSimulations.Bold) != 0 ? FontWeight.Bold : fontWeight;
var style = _os2Table != null ? GetFontStyle(_os2Table.FontStyle) : FontStyle.Normal;
if (typeface.FontStyle.Slant == SKFontStyleSlant.Oblique)
{
style = FontStyle.Oblique;
}
Style = (fontSimulations & FontSimulations.Oblique) != 0 ? FontStyle.Italic : style;
var stretch = _os2Table != null ? (FontStretch)_os2Table.WidthClass : FontStretch.Normal;
Stretch = stretch;
_nameTable = NameTable.Load(this);
//Rely on Skia if no name table is present
FamilyName = _nameTable?.FontFamilyName((ushort)CultureInfo.InvariantCulture.LCID) ?? typeface.FamilyName;
TypographicFamilyName = _nameTable?.GetNameById((ushort)CultureInfo.InvariantCulture.LCID, KnownNameIds.TypographicFamilyName) ?? FamilyName;
if(_nameTable != null)
{
var familyNames = new Dictionary<ushort, string>(1);
var faceNames = new Dictionary<ushort, string>(1);
foreach (var nameRecord in _nameTable)
{
if(nameRecord.NameID == KnownNameIds.FontFamilyName)
{
if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0)
{
continue;
}
if (!familyNames.ContainsKey(nameRecord.LanguageID))
{
familyNames[nameRecord.LanguageID] = nameRecord.Value;
}
}
if(nameRecord.NameID == KnownNameIds.FontSubfamilyName)
{
if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0)
{
continue;
}
if (!faceNames.ContainsKey(nameRecord.LanguageID))
{
faceNames[nameRecord.LanguageID] = nameRecord.Value;
}
}
}
FamilyNames = familyNames;
FaceNames = faceNames;
}
else
{
FamilyNames = new Dictionary<ushort, string> { { (ushort)CultureInfo.InvariantCulture.LCID, FamilyName } };
FaceNames = new Dictionary<ushort, string> { { (ushort)CultureInfo.InvariantCulture.LCID, Weight.ToString() } };
}
}
public string TypographicFamilyName { get; }
public IReadOnlyDictionary<ushort, string> FamilyNames { get; }
public IReadOnlyDictionary<ushort, string> FaceNames { get; }
public IReadOnlyList<OpenTypeTag> SupportedFeatures
{
get
{
if (_supportedFeatures != null)
{
return _supportedFeatures;
}
var gPosFeatures = FeatureListTable.LoadGPos(this);
var gSubFeatures = FeatureListTable.LoadGSub(this);
var supportedFeatures = new List<OpenTypeTag>(gPosFeatures?.Features.Count ?? 0 + gSubFeatures?.Features.Count ?? 0);
if (gPosFeatures != null)
{
foreach (var gPosFeature in gPosFeatures.Features)
{
if (supportedFeatures.Contains(gPosFeature))
{
continue;
}
supportedFeatures.Add(gPosFeature);
}
}
if (gSubFeatures != null)
{
foreach (var gSubFeature in gSubFeatures.Features)
{
if (supportedFeatures.Contains(gSubFeature))
{
continue;
}
supportedFeatures.Add(gSubFeature);
}
}
_supportedFeatures = supportedFeatures;
return supportedFeatures;
}
}
public SKTypeface SKTypeface { get; }
public Face Face { get; }
public Font Font { get; }
public FontSimulations FontSimulations { get; }
public int ReplacementCodepoint { get; }
public FontMetrics Metrics { get; }
public int GlyphCount { get; }
public string FamilyName { get; }
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = default;
if (!Font.TryGetGlyphExtents(glyph, out var extents))
{
return false;
}
metrics = new GlyphMetrics
{
XBearing = extents.XBearing,
YBearing = extents.YBearing,
Width = extents.Width,
Height = extents.Height
};
return true;
}
/// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint)
{
if (Font.TryGetGlyph(codepoint, out var glyph))
{
return (ushort)glyph;
}
return 0;
}
public bool TryGetGlyph(uint codepoint, out ushort glyph)
{
glyph = GetGlyph(codepoint);
return glyph != 0;
}
/// <inheritdoc cref="IGlyphTypeface"/>
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
{
var glyphs = new ushort[codepoints.Length];
for (var i = 0; i < codepoints.Length; i++)
{
if (Font.TryGetGlyph(codepoints[i], out var glyph))
{
glyphs[i] = (ushort)glyph;
}
}
return glyphs;
}
/// <inheritdoc cref="IGlyphTypeface"/>
public int GetGlyphAdvance(ushort glyph)
{
return Font.GetHorizontalGlyphAdvance(glyph);
}
/// <inheritdoc cref="IGlyphTypeface"/>
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
var glyphIndices = new uint[glyphs.Length];
for (var i = 0; i < glyphs.Length; i++)
{
glyphIndices[i] = glyphs[i];
}
return Font.GetHorizontalGlyphAdvances(glyphIndices);
}
private static FontStyle GetFontStyle(OS2Table.FontStyleSelection styleSelection)
{
if ((styleSelection & OS2Table.FontStyleSelection.ITALIC) != 0)
{
return FontStyle.Italic;
}
if ((styleSelection & OS2Table.FontStyleSelection.OBLIQUE) != 0)
{
return FontStyle.Oblique;
}
return FontStyle.Normal;
}
private Blob? GetTable(Face face, Tag tag)
{
var size = SKTypeface.GetTableSize(tag);
var data = Marshal.AllocCoTaskMem(size);
var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data));
return SKTypeface.TryGetTableData(tag, 0, size, data) ?
new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null;
}
public SKFont CreateSKFont(float size)
=> new(SKTypeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f)
{
LinearMetrics = true,
Embolden = (FontSimulations & FontSimulations.Bold) != 0
};
private void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!disposing)
{
return;
}
Font.Dispose();
Face.Dispose();
SKTypeface.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public bool TryGetTable(uint tag, out byte[] table)
{
return SKTypeface.TryGetTableData(tag, out table);
}
public bool TryGetStream([NotNullWhen(true)] out Stream? stream)
{
try
{
var asset = SKTypeface.OpenStream();
var size = asset.Length;
var buffer = new byte[size];
asset.Read(buffer, size);
stream = new MemoryStream(buffer);
return true;
}
catch
{
stream = null;
return false;
}
}
}
}

10
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -2,15 +2,15 @@ using System;
using System.Collections.Generic;
using System.IO;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Avalonia.Metal;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Media.Imaging;
using Avalonia.Skia.Metal;
using Avalonia.Skia.Vulkan;
using Avalonia.Vulkan;
using SkiaSharp;
using Avalonia.Media.TextFormatting;
using Avalonia.Metal;
using Avalonia.Skia.Metal;
namespace Avalonia.Skia
{
@ -81,7 +81,7 @@ namespace Avalonia.Skia
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface)
if (glyphRun.GlyphTypeface.PlatformTypeface is not SkiaTypeface glyphTypeface)
{
throw new InvalidOperationException("PlatformImpl can't be null.");
}

3
src/Skia/Avalonia.Skia/SkiaPlatform.cs

@ -21,8 +21,7 @@ namespace Avalonia.Skia
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(renderInterface)
.Bind<IFontManagerImpl>().ToConstant(new FontManagerImpl())
.Bind<ITextShaperImpl>().ToConstant(new TextShaperImpl());
.Bind<IFontManagerImpl>().ToConstant(new FontManagerImpl());
}
/// <summary>

81
src/Skia/Avalonia.Skia/SkiaTypeface.cs

@ -0,0 +1,81 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class SkiaTypeface : IPlatformTypeface
{
public SkiaTypeface(SKTypeface typeface, FontSimulations fontSimulations)
{
SKTypeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
FontSimulations = fontSimulations;
Weight = (FontWeight)typeface.FontWeight;
Style = typeface.FontStyle.Slant.ToAvalonia();
Stretch = (FontStretch)typeface.FontWidth;
}
public SKTypeface SKTypeface { get; }
public FontSimulations FontSimulations { get; }
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
public SKFont CreateSKFont(float size)
{
return new(SKTypeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f)
{
LinearMetrics = true,
Embolden = (FontSimulations & FontSimulations.Bold) != 0
};
}
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table)
{
table = default;
if (SKTypeface.TryGetTableData(tag, out var data))
{
table = data;
return true;
}
return false;
}
public bool TryGetStream([NotNullWhen(true)] out Stream? stream)
{
try
{
var asset = SKTypeface.OpenStream();
var size = asset.Length;
var buffer = new byte[size];
asset.Read(buffer, size);
stream = new MemoryStream(buffer);
return true;
}
catch
{
stream = null;
return false;
}
}
public void Dispose()
{
SKTypeface.Dispose();
}
}
}

7
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -9,8 +9,8 @@ using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
using GlyphRun = Avalonia.Media.GlyphRun;
using SharpDX.Mathematics.Interop;
using GlyphRun = Avalonia.Media.GlyphRun;
namespace Avalonia
{
@ -111,8 +111,7 @@ namespace Avalonia.Direct2D1
InitializeDirect2D();
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(s_instance)
.Bind<IFontManagerImpl>().ToConstant(new FontManagerImpl())
.Bind<ITextShaperImpl>().ToConstant(new TextShaperImpl());
.Bind<IFontManagerImpl>().ToConstant(new FontManagerImpl());
SharpDX.Configuration.EnableReleaseOnFinalizer = true;
}
@ -192,7 +191,7 @@ namespace Avalonia.Direct2D1
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface)
if (glyphRun.GlyphTypeface.PlatformTypeface is not DWriteTypeface glyphTypeface)
{
throw new InvalidOperationException("PlatformImpl can't be null.");
}

111
src/Windows/Avalonia.Direct2D1/Media/DWriteTypeface.cs

@ -0,0 +1,111 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using HarfBuzzSharp;
using SharpDX.DirectWrite;
namespace Avalonia.Direct2D1.Media
{
internal class DWriteTypeface : IPlatformTypeface
{
private bool _isDisposed;
public DWriteTypeface(SharpDX.DirectWrite.Font font)
{
DWFont = font;
FontFace = new FontFace(DWFont).QueryInterface<FontFace1>();
Weight = (Avalonia.Media.FontWeight)DWFont.Weight;
Style = (Avalonia.Media.FontStyle)DWFont.Style;
Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch;
}
private static uint SwapBytes(uint x)
{
x = (x >> 16) | (x << 16);
return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8);
}
public SharpDX.DirectWrite.Font DWFont { get; }
public FontFace1 FontFace { get; }
public Face Face { get; }
public HarfBuzzSharp.Font Font { get; }
public Avalonia.Media.FontWeight Weight { get; }
public Avalonia.Media.FontStyle Style { get; }
public Avalonia.Media.FontStretch Stretch { get; }
private void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!disposing)
{
return;
}
Font?.Dispose();
Face?.Dispose();
FontFace?.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table)
{
table = default;
var dwTag = (int)SwapBytes((uint)tag);
if (FontFace.TryGetFontTable(dwTag, out var tableData, out _))
{
table = tableData.ToArray();
return true;
}
return false;
}
public bool TryGetStream([NotNullWhen(true)] out Stream stream)
{
stream = default;
var files = FontFace.GetFiles();
if (files.Length > 0)
{
var file = files[0];
var referenceKey = file.GetReferenceKey();
stream = referenceKey.ToDataStream();
return true;
}
return false;
}
}
}

17
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
@ -33,7 +35,7 @@ namespace Avalonia.Direct2D1.Media
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, out Typeface typeface)
FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, out IPlatformTypeface typeface)
{
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@ -51,7 +53,7 @@ namespace Avalonia.Direct2D1.Media
var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
typeface = new Typeface(fontFamilyName, fontStyle, fontWeight, fontStretch);
typeface = new DWriteTypeface(font);
return true;
}
@ -78,7 +80,7 @@ namespace Avalonia.Direct2D1.Media
(SharpDX.DirectWrite.FontStretch)stretch,
(SharpDX.DirectWrite.FontStyle)style);
glyphTypeface = new GlyphTypefaceImpl(font);
glyphTypeface = new GlyphTypeface(new DWriteTypeface(font), FontSimulations.None);
return true;
}
@ -102,7 +104,7 @@ namespace Avalonia.Direct2D1.Media
{
var font = fontFamily.GetFont(0);
glyphTypeface = new GlyphTypefaceImpl(font);
glyphTypeface = new GlyphTypeface(new DWriteTypeface(font), FontSimulations.None);
return true;
}
@ -112,5 +114,10 @@ namespace Avalonia.Direct2D1.Media
return false;
}
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface> familyTypefaces)
{
throw new NotSupportedException();
}
}
}

10
src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs

@ -11,7 +11,7 @@ namespace Avalonia.Direct2D1.Media
{
internal class GlyphRunImpl : IGlyphRunImpl
{
private readonly GlyphTypefaceImpl _glyphTypefaceImpl;
private readonly DWriteTypeface _glyphTypefaceImpl;
private readonly short[] _glyphIndices;
private readonly float[] _glyphAdvances;
@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1.Media
public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
{
_glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
_glyphTypefaceImpl = (DWriteTypeface)glyphTypeface.PlatformTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BaselineOrigin = baselineOrigin;
@ -64,14 +64,14 @@ namespace Avalonia.Direct2D1.Media
AscenderOffset = (float)y
};
if (_glyphTypefaceImpl.TryGetGlyphMetrics(glyphInfos[i].GlyphIndex, out var metrics))
if (glyphTypeface.TryGetGlyphMetrics(glyphInfos[i].GlyphIndex, out var metrics))
{
// Found metrics with negative height, prefer to adjust it to positive.
var ybearing = metrics.YBearing;
var height = metrics.Height;
if (height < 0)
{
height = -height;
height = (ushort)-height;
}
// Not entirely sure about why we need to do this, but it seems to work
@ -111,8 +111,6 @@ namespace Avalonia.Direct2D1.Media
}
}
public IGlyphTypeface GlyphTypeface => _glyphTypefaceImpl;
public double FontRenderingEmSize { get; }
public Point BaselineOrigin { get; }

216
src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs

@ -1,216 +0,0 @@
using System;
using Avalonia.Media;
using HarfBuzzSharp;
using SharpDX.DirectWrite;
using FontMetrics = Avalonia.Media.FontMetrics;
using FontSimulations = Avalonia.Media.FontSimulations;
using GlyphMetrics = Avalonia.Media.GlyphMetrics;
namespace Avalonia.Direct2D1.Media
{
internal class GlyphTypefaceImpl : IGlyphTypeface
{
private bool _isDisposed;
public GlyphTypefaceImpl(SharpDX.DirectWrite.Font font)
{
DWFont = font;
FontFace = new FontFace(DWFont).QueryInterface<FontFace1>();
Face = new Face(GetTable);
Font = new HarfBuzzSharp.Font(Face);
Font.SetFunctionsOpenType();
Font.GetScale(out var xScale, out _);
if (!Font.TryGetHorizontalFontExtents(out var fontExtents))
{
Font.TryGetVerticalFontExtents(out fontExtents);
}
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness);
Metrics = new FontMetrics
{
DesignEmHeight = (short)xScale,
Ascent = -fontExtents.Ascender,
Descent = -fontExtents.Descender,
LineGap = fontExtents.LineGap,
UnderlinePosition = underlinePosition,
UnderlineThickness = underlineThickness,
StrikethroughPosition = strikethroughPosition,
StrikethroughThickness = strikethroughThickness,
IsFixedPitch = FontFace.IsMonospacedFont
};
FamilyName = DWFont.FontFamily.FamilyNames.GetString(0);
Weight = (Avalonia.Media.FontWeight)DWFont.Weight;
Style = (Avalonia.Media.FontStyle)DWFont.Style;
Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch;
}
private Blob GetTable(Face face, Tag tag)
{
var dwTag = (int)SwapBytes(tag);
if (FontFace.TryGetFontTable(dwTag, out var tableData, out _))
{
return new Blob(tableData.Pointer, tableData.Size, MemoryMode.ReadOnly, () => { });
}
return null;
}
private static uint SwapBytes(uint x)
{
x = (x >> 16) | (x << 16);
return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8);
}
public SharpDX.DirectWrite.Font DWFont { get; }
public FontFace1 FontFace { get; }
public Face Face { get; }
public HarfBuzzSharp.Font Font { get; }
public FontMetrics Metrics { get; }
public int GlyphCount { get; set; }
public FontSimulations FontSimulations => FontSimulations.None;
public string FamilyName { get; }
public Avalonia.Media.FontWeight Weight { get; }
public Avalonia.Media.FontStyle Style { get; }
public Avalonia.Media.FontStretch Stretch { get; }
/// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint)
{
if (Font.TryGetGlyph(codepoint, out var glyph))
{
return (ushort)glyph;
}
return 0;
}
public bool TryGetGlyph(uint codepoint, out ushort glyph)
{
glyph = GetGlyph(codepoint);
return glyph != 0;
}
/// <inheritdoc cref="IGlyphTypeface"/>
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
{
var glyphs = new ushort[codepoints.Length];
for (var i = 0; i < codepoints.Length; i++)
{
if (Font.TryGetGlyph(codepoints[i], out var glyph))
{
glyphs[i] = (ushort)glyph;
}
}
return glyphs;
}
/// <inheritdoc cref="IGlyphTypeface"/>
public int GetGlyphAdvance(ushort glyph)
{
return Font.GetHorizontalGlyphAdvance(glyph);
}
/// <inheritdoc cref="IGlyphTypeface"/>
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
var glyphIndices = new uint[glyphs.Length];
for (var i = 0; i < glyphs.Length; i++)
{
glyphIndices[i] = glyphs[i];
}
return Font.GetHorizontalGlyphAdvances(glyphIndices);
}
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = default;
if (!Font.TryGetGlyphExtents(glyph, out var extents))
{
return false;
}
metrics = new GlyphMetrics
{
XBearing = extents.XBearing,
YBearing = extents.YBearing,
Width = extents.Width,
Height = extents.Height
};
return true;
}
private void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!disposing)
{
return;
}
Font?.Dispose();
Face?.Dispose();
FontFace?.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public bool TryGetTable(uint tag, out byte[] table)
{
table = null;
var blob = Face.ReferenceTable(tag);
if (blob.Length > 0)
{
table = blob.AsSpan().ToArray();
return true;
}
return false;
}
}
}

204
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@ -1,204 +0,0 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Globalization;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using HarfBuzzSharp;
using Buffer = HarfBuzzSharp.Buffer;
using GlyphInfo = HarfBuzzSharp.GlyphInfo;
namespace Avalonia.Direct2D1.Media
{
internal class TextShaperImpl : ITextShaperImpl
{
private static readonly ConcurrentDictionary<int, Language> s_cachedLanguage = new();
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
{
var textSpan = text.Span;
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
var culture = options.Culture;
using (var buffer = new Buffer())
{
// HarfBuzz needs the surrounding characters to correctly shape the text
var containingText = GetContainingMemory(text, out var start, out var length).Span;
buffer.AddUtf16(containingText, start, length);
MergeBreakPair(buffer);
buffer.GuessSegmentProperties();
buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft;
var usedCulture = culture ?? CultureInfo.CurrentCulture;
buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture));
var font = ((GlyphTypefaceImpl)typeface).Font;
font.Shape(buffer, GetFeatures(options));
if (buffer.Direction == Direction.RightToLeft)
{
buffer.Reverse();
}
font.GetScale(out var scaleX, out _);
var textScale = fontRenderingEmSize / scaleX;
var bufferLength = buffer.Length;
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var glyphInfos = buffer.GetGlyphInfoSpan();
var glyphPositions = buffer.GetGlyphPositionSpan();
for (var i = 0; i < bufferLength; i++)
{
var sourceInfo = glyphInfos[i];
var glyphIndex = (ushort)sourceInfo.Codepoint;
var glyphCluster = (int)(sourceInfo.Cluster);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing;
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
glyphAdvance = options.IncrementalTabWidth > 0 ?
options.IncrementalTabWidth :
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
}
shapedBuffer[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
}
return shapedBuffer;
}
}
private static void MergeBreakPair(Buffer buffer)
{
var length = buffer.Length;
var glyphInfos = buffer.GetGlyphInfoSpan();
var second = glyphInfos[length - 1];
if (!new Codepoint(second.Codepoint).IsBreakChar)
{
return;
}
if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n')
{
var first = glyphInfos[length - 2];
first.Codepoint = '\u200C';
second.Codepoint = '\u200C';
second.Cluster = first.Cluster;
unsafe
{
fixed (GlyphInfo* p = &glyphInfos[length - 2])
{
*p = first;
}
fixed (GlyphInfo* p = &glyphInfos[length - 1])
{
*p = second;
}
}
}
else
{
second.Codepoint = '\u200C';
unsafe
{
fixed (GlyphInfo* p = &glyphInfos[length - 1])
{
*p = second;
}
}
}
}
private static Vector GetGlyphOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale)
{
var position = glyphPositions[index];
var offsetX = position.XOffset * textScale;
var offsetY = -position.YOffset * textScale;
return new Vector(offsetX, offsetY);
}
private static double GetGlyphAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale)
{
// Depends on direction of layout
// glyphPositions[index].YAdvance * textScale;
return glyphPositions[index].XAdvance * textScale;
}
private static ReadOnlyMemory<char> GetContainingMemory(ReadOnlyMemory<char> memory, out int start, out int length)
{
if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length))
{
return containingString.AsMemory();
}
if (MemoryMarshal.TryGetArray(memory, out var segment))
{
start = segment.Offset;
length = segment.Count;
return segment.Array.AsMemory();
}
if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager<char> memoryManager, out start, out length))
{
return memoryManager.Memory;
}
// should never happen
throw new InvalidOperationException("Memory not backed by string, array or manager");
}
private static Feature[] GetFeatures(TextShaperOptions options)
{
if (options.FontFeatures is null || options.FontFeatures.Count == 0)
{
return Array.Empty<Feature>();
}
var features = new Feature[options.FontFeatures.Count];
for (var i = 0; i < options.FontFeatures.Count; i++)
{
var fontFeature = options.FontFeatures[i];
features[i] = new Feature(
Tag.Parse(fontFeature.Tag),
(uint)fontFeature.Value,
(uint)fontFeature.Start,
(uint)fontFeature.End);
}
return features;
}
}
}

3
tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs

@ -1,5 +1,4 @@
using System;
using Avalonia.Headless;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
@ -70,7 +69,7 @@ namespace Avalonia.Base.UnitTests.Media
{
AvaloniaLocator.CurrentMutable.Bind<FontManagerOptions>().ToConstant(options);
FontManager.Current.TryMatchCharacter(1, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal,
FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal,
FontFamily.Default, null, out var typeface);
Assert.Equal("MyFont", typeface.FontFamily.Name);

185
tests/Avalonia.Base.UnitTests/Media/Fonts/UnmanagedFontMemoryTests.cs

@ -0,0 +1,185 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Media.Fonts;
using Xunit;
namespace Avalonia.Base.UnitTests.Media.Fonts
{
public class UnmanagedFontMemoryTests
{
private static byte[] BuildFont(OpenTypeTag tag, byte[] tableData)
{
const int recordsStart = 12;
const int numTables = 1;
var directoryBytes = recordsStart + numTables * 16; // 12 + 16 = 28
var offset = directoryBytes;
var result = new byte[offset + tableData.Length];
// Simple SFNT header (version 0x00010000)
result[0] = 0;
result[1] = 1;
result[2] = 0;
result[3] = 0;
// numTables (big-endian)
result[4] = 0;
result[5] = 1;
// rest of header (6 bytes) left as zero
// Table record at offset 12
uint v = tag;
result[12] = (byte)(v >> 24);
result[13] = (byte)(v >> 16);
result[14] = (byte)(v >> 8);
result[15] = (byte)v;
// checksum (4 bytes) left as zero
// offset (big-endian) at bytes 20..23
result[20] = (byte)(offset >> 24);
result[21] = (byte)(offset >> 16);
result[22] = (byte)(offset >> 8);
result[23] = (byte)offset;
// length (big-endian) at bytes 24..27
var len = tableData.Length;
result[24] = (byte)(len >> 24);
result[25] = (byte)(len >> 16);
result[26] = (byte)(len >> 8);
result[27] = (byte)len;
Buffer.BlockCopy(tableData, 0, result, offset, len);
return result;
}
[Fact]
public unsafe void TryGetTable_ReturnsTableData_WhenExists()
{
var tag = OpenTypeTag.Parse("test");
var data = new byte[] { 1, 2, 3, 4, 5 };
var font = BuildFont(tag, data);
using var ms = new MemoryStream(font);
using var mem = UnmanagedFontMemory.LoadFromStream(ms);
Assert.True(mem.TryGetTable(tag, out var table));
Assert.Equal(data, table.ToArray());
// Second call should also succeed (cache path)
Assert.True(mem.TryGetTable(tag, out var table2));
Assert.Equal(table.Length, table2.Length);
// Ensure both ReadOnlyMemory instances reference the same underlying memory
ref byte r1 = ref MemoryMarshal.GetReference(table.Span);
ref byte r2 = ref MemoryMarshal.GetReference(table2.Span);
fixed (byte* p1 = &r1)
fixed (byte* p2 = &r2)
{
Assert.Equal((IntPtr)p1, (IntPtr)p2);
}
}
[Fact]
public void TryGetTable_ReturnsFalse_ForUnknownTag()
{
var tag = OpenTypeTag.Parse("TEST");
var other = OpenTypeTag.Parse("OTHR");
var data = new byte[] { 9, 8, 7 };
var font = BuildFont(tag, data);
using var ms = new MemoryStream(font);
using var mem = UnmanagedFontMemory.LoadFromStream(ms);
Assert.False(mem.TryGetTable(other, out _));
}
[Fact]
public void TryGetTable_ReturnsFalse_ForInvalidFont()
{
// Too short to be a valid SFNT
var shortData = new byte[8];
using var ms = new MemoryStream(shortData);
using var mem = UnmanagedFontMemory.LoadFromStream(ms);
Assert.False(mem.TryGetTable(OpenTypeTag.Parse("test"), out _));
}
[Fact]
public void GetSpan_ReturnsUnderlyingData()
{
var tag = OpenTypeTag.Parse("span");
var tableData = Enumerable.Range(0, 64).Select(i => (byte)i).ToArray();
var font = BuildFont(tag, tableData);
using var ms = new MemoryStream(font);
using var mem = UnmanagedFontMemory.LoadFromStream(ms);
var span = mem.GetSpan();
Assert.Equal(font.Length, span.Length);
Assert.Equal(font, span.ToArray());
}
[Fact]
public void Pin_IncrementsPinCount_And_Dispose_Throws_WhenPinned()
{
var tag = OpenTypeTag.Parse("pin ");
var data = new byte[] { 1, 2, 3 };
var font = BuildFont(tag, data);
using var ms = new MemoryStream(font);
UnmanagedFontMemory mem = UnmanagedFontMemory.LoadFromStream(ms);
UnmanagedFontMemory? fresh = null;
try
{
var handle = mem.Pin();
try
{
// Attempting to dispose while pinned should throw
Assert.Throws<InvalidOperationException>(() => mem.Dispose());
}
finally
{
// Release the pin via the handle. After the failed Dispose the original
// instance may be in an invalid state, so prefer releasing the pin
// through the handle rather than calling methods on the possibly corrupted instance.
try
{
handle.Dispose();
}
catch { }
}
// After the exception the original instance may be unusable; construct a new instance
// for further operations and assertions.
fresh = UnmanagedFontMemory.LoadFromStream(new MemoryStream(font));
// Now disposing the fresh instance should not throw
fresh.Dispose();
}
finally
{
// Ensure final cleanup if something went wrong
try
{
mem.Dispose();
}
catch { }
if (fresh != null)
{
try
{
fresh.Dispose();
}
catch { }
}
}
}
}
}

4
tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs

@ -180,9 +180,7 @@ namespace Avalonia.Base.UnitTests.Media
glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]);
}
return new GlyphRun(
new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal,
FontStretch.Normal), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel);
return new GlyphRun(Typeface.Default.GlyphTypeface, 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel);
}
private static IDisposable Start()

131
tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs

@ -0,0 +1,131 @@
using System;
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using Xunit;
namespace Avalonia.Base.UnitTests.Media
{
public class GlyphTypefaceTests
{
private static string s_InterFontUri = "resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests";
[Fact]
public void Should_Load_Inter_Font()
{
var assetLoader = new StandardAssetLoader();
using var stream = assetLoader.Open(new Uri(s_InterFontUri));
var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
Assert.Equal("Inter", typeface.FamilyName);
}
[Fact]
public void Should_Have_CharacterToGlyphMap_For_Common_Characters()
{
var assetLoader = new StandardAssetLoader();
using var stream = assetLoader.Open(new Uri(s_InterFontUri));
var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
var map = typeface.CharacterToGlyphMap;
Assert.NotNull(map);
Assert.True(map.ContainsKey('A'));
Assert.True(map['A'] != 0);
Assert.True(map.ContainsKey('a'));
Assert.True(map['a'] != 0);
Assert.True(map.ContainsKey(' '));
Assert.True(map[' '] != 0);
}
[Fact]
public void GetGlyphAdvance_Should_Return_Advance_For_GlyphId()
{
var assetLoader = new StandardAssetLoader();
using var stream = assetLoader.Open(new Uri(s_InterFontUri));
var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
var map = typeface.CharacterToGlyphMap;
Assert.True(map.ContainsKey('A'));
var glyphId = map['A'];
// Ensure metrics are available for this glyph
Assert.True(typeface.TryGetGlyphMetrics(glyphId, out var metrics));
var advance = typeface.GetGlyphAdvance(glyphId);
// Advance returned by GetGlyphAdvance should match the metrics width
Assert.Equal(metrics.Width, advance);
}
private class CustomPlatformTypeface : IPlatformTypeface
{
private readonly UnmanagedFontMemory _fontMemory;
public CustomPlatformTypeface(Stream stream)
{
_fontMemory = UnmanagedFontMemory.LoadFromStream(stream);
}
public FontWeight Weight => FontWeight.Normal;
public FontStyle Style => FontStyle.Normal;
public FontStretch Stretch => FontStretch.Normal;
public void Dispose()
{
_fontMemory.Dispose();
}
public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream)
{
var memory = _fontMemory.Memory;
var handle = memory.Pin(); // MemoryHandle merken
stream = new PinnedUnmanagedMemoryStream(handle, memory.Length);
return true;
}
private sealed class PinnedUnmanagedMemoryStream : UnmanagedMemoryStream
{
private MemoryHandle _handle;
public unsafe PinnedUnmanagedMemoryStream(MemoryHandle handle, long length)
: base((byte*)handle.Pointer, length)
{
_handle = handle;
}
protected override void Dispose(bool disposing)
{
try
{
base.Dispose(disposing);
}
finally
{
_handle.Dispose();
}
}
}
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table) => _fontMemory.TryGetTable(tag, out table);
}
}
}

1
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -10,6 +10,7 @@
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Harfbuzz\Avalonia.Harfbuzz\Avalonia.Harfbuzz.csproj" />
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>

3
tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs

@ -1,6 +1,7 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Harfbuzz;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Skia;
@ -35,7 +36,7 @@ public class HugeTextLayout : IDisposable
if (s_useSkia)
{
testServices = testServices.With(
textShaperImpl: new TextShaperImpl(),
textShaperImpl: new HarfBuzzTextShaper(),
fontManagerImpl: new FontManagerImpl());
}

32
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@ -1,18 +1,18 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Xunit;
using System.Collections.ObjectModel;
using System.Reactive.Subjects;
using Avalonia.Headless;
using Avalonia.Harfbuzz;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
@ -371,7 +371,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(textbox.Text, control.Text);
});
}
[Fact]
public void Custom_TextSelector()
{
@ -388,7 +388,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString()));
});
}
[Fact]
public void Custom_ItemSelector()
{
@ -405,7 +405,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(control.Text, control.ItemSelector(input, selectedItem));
});
}
[Fact]
public void Text_Validation()
{
@ -420,7 +420,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true);
});
}
[Fact]
public void Text_Validation_TextBox_Errors_Binding()
{
@ -429,20 +429,20 @@ namespace Avalonia.Controls.UnitTests
// simulate the TemplateBinding that would be used within the AutoCompleteBox control theme for the inner PART_TextBox
// DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
textbox.Bind(DataValidationErrors.ErrorsProperty, control.GetBindingObservable(DataValidationErrors.ErrorsProperty));
var exception = new InvalidCastException("failed validation");
var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
control.Bind(AutoCompleteBox.TextProperty, textObservable);
Dispatcher.UIThread.RunJobs();
Assert.True(DataValidationErrors.GetHasErrors(control));
Assert.Equal([exception], DataValidationErrors.GetErrors(control));
Assert.True(DataValidationErrors.GetHasErrors(textbox));
Assert.Equal([exception], DataValidationErrors.GetErrors(textbox));
});
}
[Fact]
public void SelectedItem_Validation()
{
@ -1197,7 +1197,7 @@ namespace Avalonia.Controls.UnitTests
AutoCompleteBox control = CreateControl();
control.ItemsSource = CreateSimpleStringArray();
TextBox textBox = GetTextBox(control);
var window = new Window {Content = control};
var window = new Window { Content = control };
window.ApplyStyling();
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
@ -1265,7 +1265,7 @@ namespace Avalonia.Controls.UnitTests
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),
standardCursorFactory: Mock.Of<ICursorFactory>(),
textShaperImpl: new HeadlessTextShaperStub(),
textShaperImpl: new HarfBuzzTextShaper(),
fontManagerImpl: new HeadlessFontManagerStub());
private class TestContextMenu : ContextMenu

3
tests/Avalonia.Controls.UnitTests/DatePickerTests.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Harfbuzz;
using Avalonia.Headless;
using Avalonia.Platform;
using Avalonia.Threading;
@ -282,7 +283,7 @@ namespace Avalonia.Controls.UnitTests
private static TestServices Services => TestServices.MockThreadingInterface.With(
fontManagerImpl: new HeadlessFontManagerStub(),
standardCursorFactory: Mock.Of<ICursorFactory>(),
textShaperImpl: new HeadlessTextShaperStub(),
textShaperImpl: new HarfBuzzTextShaper(),
renderInterface: new HeadlessPlatformRenderInterface());
private static IControlTemplate CreateTemplate()

3
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -9,6 +9,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Harfbuzz;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Layout;
@ -1238,7 +1239,7 @@ namespace Avalonia.Controls.UnitTests
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),
renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub()));
textShaperImpl: new HarfBuzzTextShaper()));
}
private class ItemsControlWithContainer : ItemsControl

19
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -13,7 +13,6 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
@ -775,9 +774,9 @@ namespace Avalonia.Controls.UnitTests
{
Template = ListBoxTemplate(),
ItemsSource = items,
ItemsPanel = new FuncTemplate<Panel>(() => new VirtualizingStackPanel
ItemsPanel = new FuncTemplate<Panel>(() => new VirtualizingStackPanel
{
Orientation = Orientation.Horizontal
Orientation = Orientation.Horizontal
}),
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
SelectedIndex = 0,
@ -1111,8 +1110,8 @@ namespace Avalonia.Controls.UnitTests
Items = { "Foo", "Bar", "Baz" },
};
var button = new Button
{
var button = new Button
{
Content = "Button",
[DockPanel.DockProperty] = Dock.Top,
};
@ -1213,10 +1212,10 @@ namespace Avalonia.Controls.UnitTests
var panel = Assert.IsType<VirtualizingStackPanel>(target.ItemsPanelRoot);
Assert.Equal(0, panel.FirstRealizedIndex);
Assert.Equal(9, panel.LastRealizedIndex);
Assert.Equal(6, panel.LastRealizedIndex);
Assert.Equal(
Enumerable.Range(0, 10).Select(x => $"Item{x}"),
Enumerable.Range(0, 7).Select(x => $"Item{x}"),
data.GetRealizedItems());
}
@ -1347,9 +1346,9 @@ namespace Avalonia.Controls.UnitTests
{
private readonly List<string> _inner = new(Enumerable.Repeat<string>(null, 100));
public object this[int index]
{
get => _inner[index] = $"Item{index}";
public object this[int index]
{
get => _inner[index] = $"Item{index}";
set => throw new NotSupportedException();
}

20
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@ -6,6 +6,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Harfbuzz;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Input.Platform;
@ -136,7 +137,7 @@ namespace Avalonia.Controls.UnitTests
Template = CreateTemplate(),
Text = "1234"
};
target.ApplyTemplate();
RaiseKeyEvent(target, Key.A, KeyModifiers.Control);
@ -191,7 +192,7 @@ namespace Avalonia.Controls.UnitTests
Text = "First Second Third Fourth",
CaretIndex = 5
};
textBox.ApplyTemplate();
// (First| Second Third Fourth)
@ -233,7 +234,7 @@ namespace Avalonia.Controls.UnitTests
Text = "First Second Third Fourth",
CaretIndex = 19
};
textBox.ApplyTemplate();
// (First Second Third |Fourth)
@ -336,7 +337,7 @@ namespace Avalonia.Controls.UnitTests
Template = CreateTemplate(),
AcceptsReturn = true
};
target.ApplyTemplate();
RaiseKeyEvent(target, Key.Enter, 0);
@ -453,7 +454,7 @@ namespace Avalonia.Controls.UnitTests
AcceptsReturn = true,
NewLine = "Test"
};
target.ApplyTemplate();
RaiseKeyEvent(target, Key.Enter, 0);
@ -896,7 +897,8 @@ namespace Avalonia.Controls.UnitTests
};
var impl = CreateMockTopLevelImpl();
var topLevel = new TestTopLevel(impl.Object) {
var topLevel = new TestTopLevel(impl.Object)
{
Template = CreateTopLevelTemplate(),
Content = target
};
@ -926,13 +928,13 @@ namespace Avalonia.Controls.UnitTests
inputManager: new InputManager(),
renderInterface: new HeadlessPlatformRenderInterface(),
fontManagerImpl: new HeadlessFontManagerStub(),
textShaperImpl: new HeadlessTextShaperStub(),
textShaperImpl: new HarfBuzzTextShaper(),
standardCursorFactory: Mock.Of<ICursorFactory>());
private static TestServices Services => TestServices.MockThreadingInterface.With(
renderInterface: new HeadlessPlatformRenderInterface(),
standardCursorFactory: Mock.Of<ICursorFactory>(),
textShaperImpl: new HeadlessTextShaperStub(),
standardCursorFactory: Mock.Of<ICursorFactory>(),
textShaperImpl: new HarfBuzzTextShaper(),
fontManagerImpl: new HeadlessFontManagerStub());
private static IControlTemplate CreateTemplate()

11
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Harfbuzz;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Layout;
@ -1030,8 +1031,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
// Issue #11119
using var app = Start();
var items = Enumerable.Range(0, 100).Select(x => new TestContainer
{
var items = Enumerable.Range(0, 100).Select(x => new TestContainer
{
Content = $"Item {x}",
Height = 100,
}).ToList();
@ -1092,7 +1093,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
// Create a SelectingItemsControl that creates containers that raise IsSelectedChanged,
// with a virtualizing stack panel.
var target = CreateTarget<TestSelectorWithContainers>(
itemsSource: items,
itemsSource: items,
virtualizing: true);
target.AutoScrollToSelectedItem = false;
@ -1186,7 +1187,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
bool virtualizing = false)
{
return CreateTarget<TestSelector>(
dataContext: dataContext,
dataContext: dataContext,
items: items,
itemsSource: itemsSource,
itemContainerTheme: itemContainerTheme,
@ -1351,7 +1352,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),
renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub()));
textShaperImpl: new HarfBuzzTextShaper()));
}
private class TestSelector : SelectingItemsControl

28
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -1,12 +1,10 @@
using System;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls.UnitTests
{
@ -171,7 +169,7 @@ namespace Avalonia.Controls.UnitTests
{
var target = new TextBlock();
target.Inlines.Add(new TextBox { Text = "Hello"});
target.Inlines.Add(new TextBox { Text = "Hello" });
target.Measure(Size.Infinity);
@ -287,7 +285,7 @@ namespace Avalonia.Controls.UnitTests
var span = new Span { Inlines = new InlineCollection { new Run { Text = "World" } } };
var inlines = new InlineCollection{ new Run{Text = "Hello "}, span };
var inlines = new InlineCollection { new Run { Text = "Hello " }, span };
target.Inlines = inlines;
@ -372,13 +370,13 @@ namespace Avalonia.Controls.UnitTests
target.Arrange(new Rect(target.DesiredSize));
Assert.True(button.IsMeasureValid);
Assert.Equal(80, button.DesiredSize.Width);
Assert.Equal(58, button.DesiredSize.Width);
target.Arrange(new Rect(new Size(200, 50)));
Assert.True(button.IsArrangeValid);
Assert.Equal(60, button.Bounds.Left);
Assert.Equal(43, button.Bounds.Left);
}
}
@ -427,7 +425,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, target.Inlines.Count);
}
}
[Fact]
public void Setting_TextDecorations_Should_Update_Inlines()
{
@ -448,7 +446,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(underline, target.Inlines[0].TextDecorations);
}
}
[Fact]
public void TextBlock_TextLines_Should_Be_Empty()
{
@ -492,7 +490,7 @@ namespace Avalonia.Controls.UnitTests
target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Assert.Equal(target.DesiredSize, new Size(40, 10));
Assert.Equal(target.DesiredSize, new Size(28, 15));
}
[Fact]
@ -504,7 +502,7 @@ namespace Avalonia.Controls.UnitTests
target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Assert.Equal(target.DesiredSize, new Size(44, 14));
Assert.Equal(target.DesiredSize, new Size(32, 19));
}
[Fact]
@ -516,7 +514,7 @@ namespace Avalonia.Controls.UnitTests
target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Assert.Equal(target.DesiredSize, new Size(40, 9.6));
Assert.Equal(target.DesiredSize, new Size(27.954545454545453, 14.522727272727273));
}
[Fact]
@ -529,7 +527,7 @@ namespace Avalonia.Controls.UnitTests
target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
target.Arrange(new Rect(default, target.DesiredSize));
Assert.Equal(target.Bounds, new Rect(0, 0, 40, 9.6));
Assert.Equal(target.Bounds, new Rect(0, 0, 27.954545454545453, 14.522727272727273));
}
[Fact]
@ -541,7 +539,7 @@ namespace Avalonia.Controls.UnitTests
target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Assert.Equal(target.DesiredSize, new Size(44.5, 14.1));
Assert.Equal(target.DesiredSize, new Size(32.45454545454545, 19.022727272727273));
}
[Fact]
@ -554,7 +552,7 @@ namespace Avalonia.Controls.UnitTests
target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
target.Arrange(new Rect(default, target.DesiredSize));
Assert.Equal(target.Bounds, new Rect(0, 0, 44.5, 14.1));
Assert.Equal(target.Bounds, new Rect(0, 0, 32.45454545454545, 19.022727272727273));
}
private class TestTextBlock : TextBlock

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

@ -8,6 +8,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Harfbuzz;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Input.Platform;
@ -2134,13 +2135,13 @@ namespace Avalonia.Controls.UnitTests
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),
standardCursorFactory: Mock.Of<ICursorFactory>(),
textShaperImpl: new HeadlessTextShaperStub(),
textShaperImpl: new HarfBuzzTextShaper(),
fontManagerImpl: new HeadlessFontManagerStub());
private static TestServices Services => TestServices.MockThreadingInterface.With(
standardCursorFactory: Mock.Of<ICursorFactory>(),
renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub(),
textShaperImpl: new HarfBuzzTextShaper(),
fontManagerImpl: new HeadlessFontManagerStub());
internal static IControlTemplate CreateTemplate()

3
tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs

@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Harfbuzz;
using Avalonia.Headless;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.MarkupExtensions;
@ -149,7 +150,7 @@ namespace Avalonia.Controls.UnitTests
private static TestServices Services => TestServices.MockThreadingInterface.With(
standardCursorFactory: Mock.Of<ICursorFactory>(),
textShaperImpl: new HeadlessTextShaperStub(),
textShaperImpl: new HarfBuzzTextShaper(),
fontManagerImpl: new HeadlessFontManagerStub());
private static IControlTemplate CreateTemplate()

3
tests/Avalonia.Controls.UnitTests/TimePickerTests.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Harfbuzz;
using Avalonia.Headless;
using Avalonia.Platform;
using Avalonia.Threading;
@ -312,7 +313,7 @@ namespace Avalonia.Controls.UnitTests
private static TestServices Services => TestServices.MockThreadingInterface.With(
fontManagerImpl: new HeadlessFontManagerStub(),
standardCursorFactory: Mock.Of<ICursorFactory>(),
textShaperImpl: new HeadlessTextShaperStub(),
textShaperImpl: new HarfBuzzTextShaper(),
renderInterface: new HeadlessPlatformRenderInterface());
private static IControlTemplate CreateTemplate(bool includePopup = false)

3
tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Harfbuzz;
using Avalonia.Headless;
using Avalonia.Layout;
using Avalonia.UnitTests;
@ -325,7 +326,7 @@ namespace Avalonia.Controls.UnitTests
TestServices.MockThreadingInterface.With(
fontManagerImpl: new HeadlessFontManagerStub(),
renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub()));
textShaperImpl: new HarfBuzzTextShaper()));
}
private static (TransitioningContentControl, TestTransition) CreateTarget(object content)

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

@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Harfbuzz;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Input.Platform;
@ -1464,7 +1465,7 @@ namespace Avalonia.Controls.UnitTests
var target = CreateTarget(
data: data,
expandAll: false,
itemContainerTheme: itemTheme,
itemContainerTheme: itemTheme,
multiSelect: true);
var rootContainer = Assert.IsType<TreeViewItem>(target.ContainerFromIndex(0));
@ -1841,7 +1842,7 @@ namespace Avalonia.Controls.UnitTests
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),
renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub()));
textShaperImpl: new HarfBuzzTextShaper()));
}
private class Node : NotifyingBase

1
tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj

@ -25,6 +25,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Harfbuzz\Avalonia.Harfbuzz\Avalonia.Harfbuzz.csproj" />
<ProjectReference Include="..\..\src\Headless\Avalonia.Headless.NUnit\Avalonia.Headless.NUnit.csproj" />
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>

4
tests/Avalonia.Headless.UnitTests/TestApplication.cs

@ -1,5 +1,4 @@
using Avalonia.Headless.UnitTests;
using Avalonia.Themes.Simple;
using Avalonia.Themes.Simple;
namespace Avalonia.Headless.UnitTests;
@ -11,6 +10,7 @@ public class TestApplication : Application
}
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<TestApplication>()
.UseHarfBuzz()
.UseSkia()
.UseHeadless(new AvaloniaHeadlessPlatformOptions
{

1
tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj

@ -18,6 +18,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Harfbuzz\Avalonia.Harfbuzz\Avalonia.Harfbuzz.csproj" />
<ProjectReference Include="..\..\src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj" />
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>

1
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -12,6 +12,7 @@
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />

3
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs

@ -134,10 +134,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public object Foo { get; } = null;
}
private static IDisposable StyledWindow(params (string, string)[] assets)
private static IDisposable StyledWindow()
{
var services = TestServices.StyledWindow.With(
assetLoader: new MockAssetLoader(assets),
theme: () => new Styles
{
WindowStyle(),

3
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@ -960,10 +960,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
window.Resources.Clear();
}
private IDisposable StyledWindow(params (string, string)[] assets)
private IDisposable StyledWindow()
{
var services = TestServices.StyledWindow.With(
assetLoader: new MockAssetLoader(assets),
theme: () => new Styles
{
WindowStyle(),

1
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs

@ -583,7 +583,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
private static IDisposable StyledWindow(params (string, string)[] assets)
{
var services = TestServices.StyledWindow.With(
assetLoader: new MockAssetLoader(assets),
theme: () => new Styles
{
WindowStyle(),

BIN
tests/Avalonia.RenderTests/Assets/NotoSansTamil-Regular.ttf

Binary file not shown.

6
tests/Avalonia.RenderTests/Media/GlyphRunTests.cs

@ -140,7 +140,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface;
var glyphIndices = new[] { glyphTypeface.GetGlyph('A'), glyphTypeface.GetGlyph('B'), glyphTypeface.GetGlyph('C') };
var glyphIndices = new[] { glyphTypeface.CharacterToGlyphMap['A'], glyphTypeface.CharacterToGlyphMap['B'], glyphTypeface.CharacterToGlyphMap['C'] };
var characters = new[] { 'A', 'B', 'C' };
@ -165,7 +165,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface;
var glyphIndices = new[] { glyphTypeface.GetGlyph('A'), glyphTypeface.GetGlyph('B'), glyphTypeface.GetGlyph('C') };
var glyphIndices = new[] { glyphTypeface.CharacterToGlyphMap['A'], glyphTypeface.CharacterToGlyphMap['B'], glyphTypeface.CharacterToGlyphMap['C'] };
var characters = new[] { 'A', 'B', 'C' };
@ -188,7 +188,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface;
var glyphIndices = new[] { glyphTypeface.GetGlyph('A'), glyphTypeface.GetGlyph('B'), glyphTypeface.GetGlyph('C') };
var glyphIndices = new[] { glyphTypeface.CharacterToGlyphMap['A'], glyphTypeface.CharacterToGlyphMap['B'], glyphTypeface.CharacterToGlyphMap['C'] };
var scale = 100.0 / glyphTypeface.Metrics.DesignEmHeight;

3
tests/Avalonia.RenderTests/TestRenderHelper.cs

@ -21,6 +21,8 @@ using Avalonia.UnitTests;
using Avalonia.Utilities;
using SixLabors.ImageSharp.PixelFormats;
using Image = SixLabors.ImageSharp.Image;
using Avalonia.Harfbuzz;
#if AVALONIA_SKIA
using Avalonia.Skia;
#else
@ -50,6 +52,7 @@ static class TestRenderHelper
.ToConstant(s_dispatcherImpl);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(new StandardAssetLoader());
AvaloniaLocator.CurrentMutable.Bind<ITextShaperImpl>().ToConstant(new HarfBuzzTextShaper());
}

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

@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SkiaSharp;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace Avalonia.Skia.UnitTests.Media
{
@ -39,28 +40,37 @@ namespace Avalonia.Skia.UnitTests.Media
_isInitialized = true;
}
return _customFonts.Select(x=> x.Name).ToArray();
return _customFonts.Select(x => x.Name).ToArray();
}
private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,
CultureInfo culture, out Typeface typeface)
CultureInfo culture, out IPlatformTypeface typeface)
{
if (!_isInitialized)
{
_customFonts.Initialize(this);
}
if(_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out typeface))
if (_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out var match))
{
typeface = match.GlyphTypeface.PlatformTypeface;
return true;
}
var fallback = SKFontManager.Default.MatchCharacter(null, (SKFontStyleWeight)fontWeight,
(SKFontStyleWidth)fontStretch, (SKFontStyleSlant)fontStyle, _bcp47, codepoint);
typeface = new Typeface(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight);
if (fallback == null)
{
typeface = null;
return false;
}
typeface = new SkiaTypeface(fallback, FontSimulations.None);
return true;
}
@ -81,7 +91,7 @@ namespace Avalonia.Skia.UnitTests.Media
var skTypeface = SKTypeface.FromFamilyName(familyName,
(SKFontStyleWeight)weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)style);
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
glyphTypeface = new GlyphTypeface(new SkiaTypeface(skTypeface, FontSimulations.None), FontSimulations.None);
return true;
}
@ -90,7 +100,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var skTypeface = SKTypeface.FromStream(stream);
glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);
glyphTypeface = new GlyphTypeface(new SkiaTypeface(skTypeface, FontSimulations.None), fontSimulations);
return true;
}
@ -99,5 +109,10 @@ namespace Avalonia.Skia.UnitTests.Media
{
_customFonts.Dispose();
}
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface> familyTypefaces)
{
throw new NotImplementedException();
}
}
}

6
tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs

@ -89,11 +89,7 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.Equal("Manrope Light", glyphTypeface.FamilyName);
Assert.True(glyphTypeface is IGlyphTypeface2);
var glyphTypeface2 = (IGlyphTypeface2)glyphTypeface;
Assert.Equal("Manrope", glyphTypeface2.TypographicFamilyName);
Assert.Equal("Manrope", glyphTypeface.TypographicFamilyName);
}
}

2
tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs

@ -164,7 +164,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
foreach (var ignorable in _ignorables)
{
if (glyphTypeface.FamilyName == ignorable.Name || glyphTypeface is IGlyphTypeface2 glyphTypeface2 && glyphTypeface2.TypographicFamilyName == ignorable.Name)
if (glyphTypeface.FamilyName == ignorable.Name || glyphTypeface.TypographicFamilyName == ignorable.Name)
{
return false;
}

8
tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

@ -101,12 +101,14 @@ namespace Avalonia.Skia.UnitTests.Media
{
Assert.True(FontManager.Current.TryGetGlyphTypeface(Typeface.Default, out _));
var countBefore = fontManagerImpl.TryCreateGlyphTypefaceCount;
for (int i = 0; i < 10; i++)
{
FontManager.Current.TryGetGlyphTypeface(new Typeface("Unknown"), out _);
}
Assert.Equal(fontManagerImpl.TryCreateGlyphTypefaceCount, 2);
Assert.Equal(countBefore + 1, fontManagerImpl.TryCreateGlyphTypefaceCount);
}
}
@ -330,7 +332,7 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.Equal("Inter", glyphTypeface.FamilyName);
var features = ((IGlyphTypeface2)glyphTypeface).SupportedFeatures;
var features = glyphTypeface.SupportedFeatures;
Assert.NotEmpty(features);
}
@ -448,7 +450,7 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.Equal(FontStyle.Normal, regularTypeface.Style);
Assert.NotEqual(((GlyphTypefaceImpl)italicTypeface.GlyphTypeface).SKTypeface, ((GlyphTypefaceImpl)regularTypeface.GlyphTypeface).SKTypeface);
Assert.NotEqual(((SkiaTypeface)italicTypeface.GlyphTypeface.PlatformTypeface).SKTypeface, ((SkiaTypeface)regularTypeface.GlyphTypeface.PlatformTypeface).SKTypeface);
}
}
}

1
tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs

@ -474,7 +474,6 @@ namespace Avalonia.Skia.UnitTests.Media
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(),
textShaperImpl: new TextShaperImpl(),
fontManagerImpl: new CustomFontManagerImpl()));
return disposable;

113
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/Tables/CmapTableTests.cs

@ -0,0 +1,113 @@
using System;
using System.Buffers.Binary;
using Avalonia.Media.Fonts.Tables.Cmap;
using Xunit;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting.Tables
{
public class CmapTableTests
{
[Fact]
public void BuildFormat4Subtable_Should_Map_Range()
{
// Build a subtable mapping U+0030–U+0039 (digits 0–9) to glyphs 1–10
byte[] subtable = CmapTestHelper.BuildFormat4Subtable(0x0030, 0x0039, 1);
var cmap = new CmapFormat4Table(subtable);
for (int i = 0; i < 10; i++)
{
int cp = 0x30 + i;
ushort glyph = cmap[cp];
var expectedGlyph = (ushort)(i + 1);
Assert.Equal(expectedGlyph, glyph);
}
// Outside range should map to 0
Assert.Equal((ushort)0, cmap[0x0041]); // 'A'
}
}
public static class CmapTestHelper
{
/// <summary>
/// Builds a Format 4 subtable for a TrueType font's 'cmap' table, which maps a range of character codes to
/// glyph indices.
/// </summary>
/// <remarks>The Format 4 subtable is used in TrueType fonts to define mappings from character
/// codes to glyph indices for a contiguous range of character codes. This method generates a minimal Format 4
/// subtable with one segment for the specified range and a sentinel segment, as required by the TrueType
/// specification. <para> The generated subtable includes the necessary header fields, segment arrays, and delta
/// values to ensure that the specified range of character codes maps correctly to the corresponding glyph
/// indices. </para> <exception cref="ArgumentException"> Thrown if <paramref name="endCode"/> is less than
/// <paramref name="startCode"/>. </exception></remarks>
/// <param name="startCode">The starting character code of the range to map.</param>
/// <param name="endCode">The ending character code of the range to map.</param>
/// <param name="firstGlyphId">The glyph index corresponding to the <paramref name="startCode"/>. Subsequent character codes in the range
/// will map to consecutive glyph indices.</param>
/// <returns>A byte array representing the Format 4 subtable, which can be embedded in a TrueType font's 'cmap' table.</returns>
public static byte[] BuildFormat4Subtable(ushort startCode, ushort endCode, ushort firstGlyphId = 1)
{
if (endCode < startCode)
throw new ArgumentException("endCode must be >= startCode");
// We will build exactly one real segment + sentinel
ushort segCount = 2; // one real + one sentinel
ushort segCountX2 = (ushort)(segCount * 2);
// Correct search parameters (searchRange = 2 * (2^floor(log2(segCount))))
int highestPowerOfTwo = 1;
while (highestPowerOfTwo * 2 <= segCount)
highestPowerOfTwo *= 2;
ushort searchRange = (ushort)(2 * highestPowerOfTwo);
ushort entrySelector = (ushort)(Math.Log(highestPowerOfTwo, 2));
ushort rangeShift = (ushort)(segCountX2 - searchRange);
// idDelta so that startCode maps to firstGlyphId
short idDelta = (short)(firstGlyphId - startCode);
// Calculate length: header (14) + endCode(segCount*2) + reservedPad(2) + startCode(segCount*2)
// + idDelta(segCount*2) + idRangeOffset(segCount*2) + (no glyphIdArray)
int headerSize = 14;
int segArraysSize = segCount * 2 /*endCode*/ + 2 /*reservedPad*/ + segCount * 2 /*startCode*/ + segCount * 2 /*idDelta*/ + segCount * 2 /*idRangeOffset*/;
int length = headerSize + segArraysSize;
var buffer = new byte[length];
int pos = 0;
void WriteUInt16(ushort v)
{ BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(pos, 2), v); pos += 2; }
void WriteInt16(short v)
{ BinaryPrimitives.WriteInt16BigEndian(buffer.AsSpan(pos, 2), v); pos += 2; }
// Header
WriteUInt16(4); // format
WriteUInt16((ushort)length); // length
WriteUInt16(0); // language
WriteUInt16(segCountX2);
WriteUInt16(searchRange);
WriteUInt16(entrySelector);
WriteUInt16(rangeShift);
// endCode[] (one real segment then sentinel)
WriteUInt16(endCode);
WriteUInt16(0xFFFF);
WriteUInt16(0); // reservedPad
// startCode[]
WriteUInt16(startCode);
WriteUInt16(0xFFFF);
// idDelta[]
WriteInt16(idDelta);
WriteInt16(1); // sentinel delta (commonly 1)
// idRangeOffset[]
WriteUInt16(0);
WriteUInt16(0);
return buffer;
}
}
}

5
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -475,7 +475,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var formatter = new TextFormatterImpl();
var glyph = typeface.GlyphTypeface.GetGlyph('a');
var glyph = typeface.GlyphTypeface.CharacterToGlyphMap['a'];
var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) *
(12.0 / typeface.GlyphTypeface.Metrics.DesignEmHeight);
@ -1253,8 +1253,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(),
textShaperImpl: new TextShaperImpl()));
.With(renderInterface: new PlatformRenderInterface()));
AvaloniaLocator.CurrentMutable
.Bind<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl()));

10
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -566,7 +566,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(7, textRun.Length);
var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint);
var replacementGlyph = Typeface.Default.GlyphTypeface.CharacterToGlyphMap[Codepoint.ReplacementCodepoint];
foreach (var glyphInfo in textRun.GlyphRun.GlyphInfos)
{
@ -1189,7 +1189,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
using (Start())
{
var typeFace = new Typeface("Courier New");
const string monospaceFont = "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono";
var typeFace = new Typeface(monospaceFont);
var glyphTypeface = typeFace.GlyphTypeface;
var textLayout0 = new TextLayout("aaaa", typeFace, 12.0, Brushes.White);
Assert.Equal(textLayout0.WidthIncludingTrailingWhitespace, textLayout0.Width);
@ -1217,7 +1222,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null),
textShaperImpl: new TextShaperImpl(),
fontManagerImpl: new CustomFontManagerImpl()));
return disposable;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save