Browse Source

Universal GlyphTypeface implementation (#19852)

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

* Revert changes

* Fix Android

* Make the test happy

* Fix build

* Update baseline

* Fix naming

* Fix headless

* Move interfaces to dedicated files
Make GlyphTypeface.GlyphCount an integer

* Fix GlyphCount

* Make IGlyphTypeface NotClientImplementable

* Make sure we cache platform typefaces by their desired name, style, weight and stretch

* Update baseline

* Only use IGlyphTypeface

* Fix Android

* Try to clear the buffer before we encode somethimg

* Add needed test font

* Add more unit tests

* Reduce allocations

* Remove Direct2D1 test files

* More tests

* More complete table implementations

* More adjustments

* Use batch APIs

* Handle invalid timestamps

* Update baseline

* Introduce a CharacterToGlyphMap struct for faster access

* Remove AggressiveInlining

* Remove AggressiveInlining

* Make the head table optional for legacy fonts

* Remove Load method. Fix TextBlockTests

* Fix nullables

* Remove redundant folder

* Update Api baseline

* revert diff helper changes

* revert changes

* Use bare minimum font for Headless platform and introduce a test font manager that uses the Inter font for testing.

* Add missing font file for Headless platform

---------

Co-authored-by: Gillibald <[email protected]>
Co-authored-by: Julien Lebosquain <[email protected]>
Benedikt Stebner 4 days ago
parent
commit
e8424283fe
100 changed files with 6828 additions and 2049 deletions
  1. 11 2
      Avalonia.sln
  2. 2 2
      api/Avalonia.Win32.Interoperability.nupkg.xml
  3. 390 252
      api/Avalonia.nupkg.xml
  4. 1 1
      samples/ControlCatalog.Android/MainActivity.cs
  5. 1 3
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  6. 4 4
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  7. 2 2
      samples/TextTestApp/MainWindow.axaml.cs
  8. 1 0
      src/Android/Avalonia.Android/AndroidPlatform.cs
  9. 1 0
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  10. 14 15
      src/Avalonia.Base/Media/FontManager.cs
  11. 1 1
      src/Avalonia.Base/Media/FontMetrics.cs
  12. 98 85
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  13. 8 0
      src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs
  14. 50 0
      src/Avalonia.Base/Media/Fonts/FontCollectionKeyExtensions.cs
  15. 13 8
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  16. 4 4
      src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs
  17. 47 29
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  18. 109 200
      src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs
  19. 148 0
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs
  20. 34 0
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapEncoding.cs
  21. 16 0
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat.cs
  22. 258 0
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs
  23. 454 0
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat4Table.cs
  24. 42 0
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapSubtableEntry.cs
  25. 166 0
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs
  26. 52 0
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRange.cs
  27. 71 0
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs
  28. 38 26
      src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs
  29. 75 0
      src/Avalonia.Base/Media/Fonts/Tables/FontVersion.cs
  30. 322 0
      src/Avalonia.Base/Media/Fonts/Tables/HeadTable.cs
  31. 87 40
      src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeaderTable.cs
  32. 138 0
      src/Avalonia.Base/Media/Fonts/Tables/MaxpTable.cs
  33. 26 0
      src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalGlyphMetric.cs
  34. 227 0
      src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalMetricsTable.cs
  35. 24 0
      src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalGlyphMetric.cs
  36. 227 0
      src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalMetricsTable.cs
  37. 30 19
      src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs
  38. 56 46
      src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs
  39. 199 345
      src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs
  40. 248 0
      src/Avalonia.Base/Media/Fonts/Tables/Panose.cs
  41. 1 1
      src/Avalonia.Base/Media/Fonts/Tables/PlatformID.cs
  42. 46 0
      src/Avalonia.Base/Media/Fonts/Tables/PostTable.cs
  43. 0 38
      src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs
  44. 127 0
      src/Avalonia.Base/Media/Fonts/Tables/VerticalHeaderTable.cs
  45. 356 0
      src/Avalonia.Base/Media/Fonts/UnmanagedFontMemory.cs
  46. 3 3
      src/Avalonia.Base/Media/GlyphMetrics.cs
  47. 18 12
      src/Avalonia.Base/Media/GlyphRun.cs
  48. 655 0
      src/Avalonia.Base/Media/GlyphTypeface.cs
  49. 20 0
      src/Avalonia.Base/Media/IFontMemory.cs
  50. 0 115
      src/Avalonia.Base/Media/IGlyphTypeface.cs
  51. 0 39
      src/Avalonia.Base/Media/IGlyphTypeface2.cs
  52. 46 0
      src/Avalonia.Base/Media/IPlatformTypeface.cs
  53. 11 0
      src/Avalonia.Base/Media/ITextShaperTypeface.cs
  54. 3 3
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  55. 7 7
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  56. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  57. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs
  58. 2 2
      src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs
  59. 4 4
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  60. 1 1
      src/Avalonia.Base/Media/Typeface.cs
  61. 6 25
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  62. 0 4
      src/Avalonia.Base/Platform/IGlyphRunImpl.cs
  63. 1 1
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  64. 11 2
      src/Avalonia.Base/Platform/ITextShaperImpl.cs
  65. 6 0
      src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs
  66. 3 4
      src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs
  67. 7 2
      src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs
  68. 1 0
      src/Avalonia.Desktop/Avalonia.Desktop.csproj
  69. 22 0
      src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.HarfBuzz.csproj
  70. 27 0
      src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzApplicationExtensions.cs
  71. 33 12
      src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTextShaper.cs
  72. 64 0
      src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTypeface.cs
  73. 6 0
      src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
  74. BIN
      src/Headless/Avalonia.Headless/BareMinimum.ttf
  75. 5 8
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  76. 157 232
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  77. 11 32
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  78. 3 5
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  79. 0 385
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  80. 6 6
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  81. 1 2
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  82. 83 0
      src/Skia/Avalonia.Skia/SkiaTypeface.cs
  83. 1 0
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  84. 1 0
      src/iOS/Avalonia.iOS/Platform.cs
  85. 2 4
      tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
  86. 256 0
      tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/HeadTableTests.cs
  87. 233 0
      tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/MaxpTableTests.cs
  88. 233 0
      tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/OS2TableTests.cs
  89. 185 0
      tests/Avalonia.Base.UnitTests/Media/Fonts/UnmanagedFontMemoryTests.cs
  90. 1 3
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  91. 446 0
      tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs
  92. 1 0
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  93. 2 1
      tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
  94. 4 3
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  95. 1 0
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  96. 2 1
      tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
  97. 2 0
      tests/Avalonia.Controls.UnitTests/GridTests.cs
  98. 2 1
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  99. 2 2
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  100. 4 3
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

+ 11 - 2
Avalonia.sln

@@ -34,8 +34,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE
 		src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
 		src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
 		src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
 		src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
 		src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.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\StreamCompatibilityExtensions.cs = src\Shared\StreamCompatibilityExtensions.cs
+		src\Shared\StringCompatibilityExtensions.cs = src\Shared\StringCompatibilityExtensions.cs
 	EndProjectSection
 	EndProjectSection
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Markup\Avalonia.Markup\Avalonia.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Markup\Avalonia.Markup\Avalonia.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}"
@@ -80,6 +80,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia", "src\Skia\A
 EndProject
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
 	ProjectSection(SolutionItems) = preProject
 	ProjectSection(SolutionItems) = preProject
+		build\AnalyzerProject.targets = build\AnalyzerProject.targets
 		build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props
 		build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props
 		build\Base.props = build\Base.props
 		build\Base.props = build\Base.props
 		build\Binding.props = build\Binding.props
 		build\Binding.props = build\Binding.props
@@ -107,7 +108,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
 		build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
 		build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
 		build\WarnAsErrors.props = build\WarnAsErrors.props
 		build\WarnAsErrors.props = build\WarnAsErrors.props
 		build\XUnit.props = build\XUnit.props
 		build\XUnit.props = build\XUnit.props
-		build\AnalyzerProject.targets = build\AnalyzerProject.targets
 	EndProjectSection
 	EndProjectSection
 EndProject
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"
@@ -275,6 +275,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.MacCatalyst"
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.tvOS", "samples\ControlCatalog.tvOS\ControlCatalog.tvOS.csproj", "{14342787-B4EF-4076-8C91-BA6C523DE8DF}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.tvOS", "samples\ControlCatalog.tvOS\ControlCatalog.tvOS.csproj", "{14342787-B4EF-4076-8C91-BA6C523DE8DF}"
 EndProject
 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
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.PerAssembly.UnitTests", "tests\Avalonia.Headless.NUnit.PerAssembly.UnitTests\Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj", "{A175EFAE-476C-4DAA-87D5-742C18CFCC27}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.PerAssembly.UnitTests", "tests\Avalonia.Headless.NUnit.PerAssembly.UnitTests\Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj", "{A175EFAE-476C-4DAA-87D5-742C18CFCC27}"
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.PerTest.UnitTests", "tests\Avalonia.Headless.NUnit.PerTest.UnitTests\Avalonia.Headless.NUnit.PerTest.UnitTests.csproj", "{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.PerTest.UnitTests", "tests\Avalonia.Headless.NUnit.PerTest.UnitTests\Avalonia.Headless.NUnit.PerTest.UnitTests.csproj", "{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}"
@@ -643,6 +647,10 @@ Global
 		{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{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.ActiveCfg = Release|Any CPU
 		{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Release|Any CPU.Build.0 = 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
 		{A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -750,6 +758,7 @@ Global
 		{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{DE3C28DD-B602-4750-831D-345102A54CA0} = {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}
 		{14342787-B4EF-4076-8C91-BA6C523DE8DF} = {9B9E3891-2366-4253-A952-D08BCEB71098}
+		{E2BFA463-6402-4EF8-8945-FD9A10A914D1} = {7670D720-6E84-4AFC-8331-A5C399481905}
 		{A175EFAE-476C-4DAA-87D5-742C18CFCC27} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{A175EFAE-476C-4DAA-87D5-742C18CFCC27} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{342D2657-2F84-493C-B74B-9D2CAE5D9DAB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{342D2657-2F84-493C-B74B-9D2CAE5D9DAB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}

+ 2 - 2
api/Avalonia.Win32.Interoperability.nupkg.xml

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
 <!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
 <Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Suppression>
   <Suppression>
@@ -37,4 +37,4 @@
     <Left>baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll</Left>
     <Left>baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll</Left>
     <Right>current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll</Right>
     <Right>current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll</Right>
   </Suppression>
   </Suppression>
-</Suppressions>
+</Suppressions>

File diff suppressed because it is too large
+ 390 - 252
api/Avalonia.nupkg.xml


+ 1 - 1
samples/ControlCatalog.Android/MainActivity.cs

@@ -10,7 +10,7 @@ using static Android.Content.Intent;
 
 
 namespace ControlCatalog.Android
 namespace ControlCatalog.Android
 {
 {
-    [Activity(Name = "com.Avalonia.ControlCatalog.MainActivity", Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, Exported = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
+    [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, Exported = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
     // CategoryLeanbackLauncher is required for Android TV.
     // CategoryLeanbackLauncher is required for Android TV.
     [IntentFilter(new[] { ActionView }, Categories = new[] { CategoryDefault, CategoryLeanbackLauncher })]
     [IntentFilter(new[] { ActionView }, Categories = new[] { CategoryDefault, CategoryLeanbackLauncher })]
     public class MainActivity : AvaloniaMainActivity
     public class MainActivity : AvaloniaMainActivity

+ 1 - 3
samples/RenderDemo/Pages/CustomSkiaPage.cs

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

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

@@ -22,7 +22,7 @@ namespace RenderDemo.Pages
 
 
     public class GlyphRunControl : Control
     public class GlyphRunControl : Control
     {
     {
-        private IGlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
+        private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
         private readonly Random _rand = new Random();
         private readonly Random _rand = new Random();
         private ushort[] _glyphIndices = new ushort[1];
         private ushort[] _glyphIndices = new ushort[1];
         private char[] _characters = new char[1];
         private char[] _characters = new char[1];
@@ -69,7 +69,7 @@ namespace RenderDemo.Pages
 
 
             _fontSize += _direction;
             _fontSize += _direction;
 
 
-            _glyphIndices[0] = _glyphTypeface.GetGlyph(c);
+            _glyphIndices[0] = _glyphTypeface.CharacterToGlyphMap[c];
 
 
             _characters[0] = c;
             _characters[0] = c;
 
 
@@ -81,7 +81,7 @@ namespace RenderDemo.Pages
 
 
     public class GlyphRunGeometryControl : Control
     public class GlyphRunGeometryControl : Control
     {
     {
-        private IGlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
+        private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
         private readonly Random _rand = new Random();
         private readonly Random _rand = new Random();
         private ushort[] _glyphIndices = new ushort[1];
         private ushort[] _glyphIndices = new ushort[1];
         private char[] _characters = new char[1];
         private char[] _characters = new char[1];
@@ -128,7 +128,7 @@ namespace RenderDemo.Pages
 
 
             _fontSize += _direction;
             _fontSize += _direction;
 
 
-            _glyphIndices[0] = _glyphTypeface.GetGlyph(c);
+            _glyphIndices[0] = _glyphTypeface.CharacterToGlyphMap[c];
 
 
             _characters[0] = c;
             _characters[0] = c;
 
 

+ 2 - 2
samples/TextTestApp/MainWindow.axaml.cs

@@ -223,12 +223,12 @@ namespace TextTestApp
             }
             }
         }
         }
 
 
-        private IImage CreateGlyphDrawing(IGlyphTypeface glyphTypeface, double emSize, GlyphInfo info)
+        private IImage CreateGlyphDrawing(GlyphTypeface glyphTypeface, double emSize, GlyphInfo info)
         {
         {
             return new DrawingImage { Drawing = new GeometryDrawing { Brush = Brushes.Black, Geometry = GetGlyphOutline(glyphTypeface, emSize, info) } };
             return new DrawingImage { Drawing = new GeometryDrawing { Brush = Brushes.Black, Geometry = GetGlyphOutline(glyphTypeface, emSize, info) } };
         }
         }
 
 
-        private Geometry GetGlyphOutline(IGlyphTypeface typeface, double emSize, GlyphInfo info)
+        private Geometry GetGlyphOutline(GlyphTypeface typeface, double emSize, GlyphInfo info)
         {
         {
             // substitute for GlyphTypeface.GetGlyphOutline
             // substitute for GlyphTypeface.GetGlyphOutline
             return new GlyphRun(typeface, emSize, new[] { '\0' }, [info]).BuildGeometry();
             return new GlyphRun(typeface, emSize, new[] { '\0' }, [info]).BuildGeometry();

+ 1 - 0
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -24,6 +24,7 @@ namespace Avalonia
             return builder
             return builder
                 .UseAndroidRuntimePlatformSubsystem()
                 .UseAndroidRuntimePlatformSubsystem()
                 .UseWindowingSubsystem(() => AndroidPlatform.Initialize(), "Android")
                 .UseWindowingSubsystem(() => AndroidPlatform.Initialize(), "Android")
+                .UseHarfBuzz()
                 .UseSkia();
                 .UseSkia();
         }
         }
     }
     }

+ 1 - 0
src/Android/Avalonia.Android/Avalonia.Android.csproj

@@ -15,6 +15,7 @@
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
     <ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
+    <ProjectReference Include="..\..\HarfBuzz\Avalonia.HarfBuzz\Avalonia.HarfBuzz.csproj" />
     <ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
     <ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 

+ 14 - 15
src/Avalonia.Base/Media/FontManager.cs

@@ -98,7 +98,7 @@ namespace Avalonia.Media
         /// <returns>
         /// <returns>
         ///     <c>True</c>, if the <see cref="FontManager"/> could create the glyph typeface, <c>False</c> otherwise.
         ///     <c>True</c>, if the <see cref="FontManager"/> could create the glyph typeface, <c>False</c> otherwise.
         /// </returns>
         /// </returns>
-        public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
         {
         {
             glyphTypeface = null;
             glyphTypeface = null;
 
 
@@ -109,7 +109,7 @@ namespace Avalonia.Media
                 return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
                 return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
             }
             }
 
 
-            
+
             if (fontFamily.Key != null)
             if (fontFamily.Key != null)
             {
             {
                 if (fontFamily.Key is CompositeFontFamilyKey compositeKey)
                 if (fontFamily.Key is CompositeFontFamilyKey compositeKey)
@@ -187,7 +187,7 @@ namespace Avalonia.Media
             }
             }
         }
         }
 
 
-        private bool TryGetGlyphTypefaceByKeyAndName(Typeface typeface, FontFamilyKey key, string familyName, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        private bool TryGetGlyphTypefaceByKeyAndName(Typeface typeface, FontFamilyKey key, string familyName, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
         {
         {
             var source = key.Source.EnsureAbsolute(key.BaseUri);
             var source = key.Source.EnsureAbsolute(key.BaseUri);
 
 
@@ -271,7 +271,7 @@ namespace Avalonia.Media
                     {
                     {
                         typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
                         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.TryGetGlyph(codepoint, out _))
                         {
                         {
                             return true;
                             return true;
                         }
                         }
@@ -300,6 +300,11 @@ namespace Avalonia.Media
                             fontCollection.TryGetGlyphTypeface(familyName, fontStyle, fontWeight, fontStretch, out _) &&
                             fontCollection.TryGetGlyphTypeface(familyName, fontStyle, fontWeight, fontStretch, out _) &&
                             fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
                             fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
                         {
                         {
+                            if (typeface.FontFamily.Name == DefaultFontFamily.Name && i + 1 < compositeKey.Keys.Count)
+                            {
+                                continue;
+                            }
+
                             return true;
                             return true;
                         }
                         }
                     }
                     }
@@ -328,24 +333,18 @@ namespace Avalonia.Media
 
 
             if (key == null)
             if (key == null)
             {
             {
-                if (SystemFonts is IFontCollection2 fontCollection2)
+                if (SystemFonts.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces))
                 {
                 {
-                    if (fontCollection2.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces))
-                    {
-                        return familyTypefaces;
-                    }
+                    return familyTypefaces;
                 }
                 }
             }
             }
             else
             else
             {
             {
                 var source = key.Source.EnsureAbsolute(key.BaseUri);
                 var source = key.Source.EnsureAbsolute(key.BaseUri);
 
 
-                if (TryGetFontCollection(source, out var fontCollection) && fontCollection is IFontCollection2 fontCollection2)
+                if (TryGetFontCollection(source, out var fontCollection) && fontCollection.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces))
                 {
                 {
-                    if (fontCollection2.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces))
-                    {
-                        return familyTypefaces;
-                    }
+                    return familyTypefaces;
                 }
                 }
             }
             }
 
 
@@ -374,7 +373,7 @@ namespace Avalonia.Media
                         fontCollection = new EmbeddedFontCollection(source, source);
                         fontCollection = new EmbeddedFontCollection(source, source);
                     }
                     }
                 }
                 }
-                
+
                 if (fontCollection != null)
                 if (fontCollection != null)
                 {
                 {
                     return _fontCollections.TryAdd(fontCollection.Key, fontCollection);
                     return _fontCollections.TryAdd(fontCollection.Key, fontCollection);

+ 1 - 1
src/Avalonia.Base/Media/FontMetrics.cs

@@ -8,7 +8,7 @@
         /// <summary>
         /// <summary>
         ///     Gets the font design units per em.
         ///     Gets the font design units per em.
         /// </summary>
         /// </summary>
-        public short DesignEmHeight { get; init; }
+        public ushort DesignEmHeight { get; init; }
 
 
         /// <summary>
         /// <summary>
         ///     A <see cref="bool"/> value indicating whether all glyphs in the font have the same advancement. 
         ///     A <see cref="bool"/> value indicating whether all glyphs in the font have the same advancement. 

+ 98 - 85
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@@ -9,13 +9,13 @@ using Avalonia.Platform;
 
 
 namespace Avalonia.Media.Fonts
 namespace Avalonia.Media.Fonts
 {
 {
-    public abstract class FontCollectionBase : IFontCollection2
+    public abstract class FontCollectionBase : IFontCollection
     {
     {
         private static readonly Comparer<FontFamily> FontFamilyNameComparer =
         private static readonly Comparer<FontFamily> FontFamilyNameComparer =
             Comparer<FontFamily>.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
             Comparer<FontFamily>.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
 
 
         // Make this internal for testing purposes
         // Make this internal for testing purposes
-        internal readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> _glyphTypefaceCache = new();
+        internal readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, GlyphTypeface?>> _glyphTypefaceCache = new();
 
 
         private readonly object _fontFamiliesLock = new();
         private readonly object _fontFamiliesLock = new();
         private volatile FontFamily[] _fontFamilies = Array.Empty<FontFamily>();
         private volatile FontFamily[] _fontFamilies = Array.Empty<FontFamily>();
@@ -39,12 +39,14 @@ namespace Avalonia.Media.Fonts
         {
         {
             match = default;
             match = default;
 
 
+            var key = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch };
+
             //If a font family is defined we try to find a match inside that family first
             //If a font family is defined we try to find a match inside that family first
             if (familyName != null && _glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
             if (familyName != null && _glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
             {
             {
-                if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
+                if (TryGetNearestMatch(glyphTypefaces, key, out var glyphTypeface))
                 {
                 {
-                    if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+                    if (glyphTypeface.CharacterToGlyphMap.TryGetGlyph(codepoint, out _))
                     {
                     {
                         match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), style, weight, stretch);
                         match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), style, weight, stretch);
 
 
@@ -64,15 +66,17 @@ namespace Avalonia.Media.Fonts
 
 
                 glyphTypefaces = pair.Value;
                 glyphTypefaces = pair.Value;
 
 
-                if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
+                if (TryGetNearestMatch(glyphTypefaces, key, out var glyphTypeface))
                 {
                 {
-                    if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+                    if (glyphTypeface.CharacterToGlyphMap.TryGetGlyph(codepoint, out _))
                     {
                     {
+                        var platformTypeface = glyphTypeface.PlatformTypeface;
+
                         // Found a match
                         // Found a match
-                        match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), 
-                            glyphTypeface.Style, 
-                            glyphTypeface.Weight, 
-                            glyphTypeface.Stretch);
+                        match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName),
+                            platformTypeface.Style,
+                            platformTypeface.Weight,
+                            platformTypeface.Stretch);
 
 
                         return true;
                         return true;
                     }
                     }
@@ -83,11 +87,11 @@ namespace Avalonia.Media.Fonts
         }
         }
 
 
         public virtual bool TryCreateSyntheticGlyphTypeface(
         public virtual bool TryCreateSyntheticGlyphTypeface(
-            IGlyphTypeface glyphTypeface,
+            GlyphTypeface glyphTypeface,
             FontStyle style,
             FontStyle style,
             FontWeight weight,
             FontWeight weight,
             FontStretch stretch,
             FontStretch stretch,
-            [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
+            [NotNullWhen(true)] out GlyphTypeface? syntheticGlyphTypeface)
         {
         {
             syntheticGlyphTypeface = null;
             syntheticGlyphTypeface = null;
 
 
@@ -99,44 +103,40 @@ namespace Avalonia.Media.Fonts
 
 
             var key = new FontCollectionKey(style, weight, stretch);
             var key = new FontCollectionKey(style, weight, stretch);
 
 
-            var currentKey =
-                new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
-
+            var currentKey = glyphTypeface.ToFontCollectionKey();
+                
             if (currentKey == key)
             if (currentKey == key)
             {
             {
                 return false;
                 return false;
             }
             }
 
 
-            if (glyphTypeface is not IGlyphTypeface2 glyphTypeface2)
-            {
-                return false;
-            }
-
             var fontSimulations = FontSimulations.None;
             var fontSimulations = FontSimulations.None;
 
 
-            if (style != FontStyle.Normal && glyphTypeface2.Style != style)
+            if (style != FontStyle.Normal && glyphTypeface.Style != style)
             {
             {
                 fontSimulations |= FontSimulations.Oblique;
                 fontSimulations |= FontSimulations.Oblique;
             }
             }
 
 
-            if ((int)weight >= 600 && glyphTypeface2.Weight < weight)
+            if ((int)weight >= 600 && glyphTypeface.Weight < weight)
             {
             {
                 fontSimulations |= FontSimulations.Bold;
                 fontSimulations |= FontSimulations.Bold;
             }
             }
 
 
-            if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream))
+            if (fontSimulations != FontSimulations.None && glyphTypeface.PlatformTypeface.TryGetStream(out var stream))
             {
             {
                 using (stream)
                 using (stream)
                 {
                 {
-                    if (_fontManagerImpl.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface))
+                    if (_fontManagerImpl.TryCreateGlyphTypeface(stream, fontSimulations, out var platformTypeface))
                     {
                     {
+                        syntheticGlyphTypeface = new GlyphTypeface(platformTypeface, fontSimulations);
+
                         //Add the TypographicFamilyName to the cache
                         //Add the TypographicFamilyName to the cache
-                        if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
+                        if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName))
                         {
                         {
-                            TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, syntheticGlyphTypeface);
+                            TryAddGlyphTypeface(glyphTypeface.TypographicFamilyName, key, syntheticGlyphTypeface);
                         }
                         }
 
 
-                        foreach (var kvp in glyphTypeface2.FamilyNames)
+                        foreach (var kvp in glyphTypeface.FamilyNames)
                         {
                         {
                             TryAddGlyphTypeface(kvp.Value, key, syntheticGlyphTypeface);
                             TryAddGlyphTypeface(kvp.Value, key, syntheticGlyphTypeface);
                         }
                         }
@@ -154,17 +154,11 @@ namespace Avalonia.Media.Fonts
         public IEnumerator<FontFamily> GetEnumerator() => ((IEnumerable<FontFamily>)_fontFamilies).GetEnumerator();
         public IEnumerator<FontFamily> GetEnumerator() => ((IEnumerable<FontFamily>)_fontFamilies).GetEnumerator();
 
 
         public virtual bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
         public virtual bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
-                    FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+                    FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
         {
         {
             var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
             var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
 
 
-            style = typeface.Style;
-
-            weight = typeface.Weight;
-
-            stretch = typeface.Stretch;
-
-            var key = new FontCollectionKey(style, weight, stretch);
+            var key = typeface.ToFontCollectionKey();
 
 
             return TryGetGlyphTypeface(familyName, key, out glyphTypeface);
             return TryGetGlyphTypeface(familyName, key, out glyphTypeface);
         }
         }
@@ -195,7 +189,7 @@ namespace Avalonia.Media.Fonts
             return false;
             return false;
         }
         }
 
 
-        public bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        public bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
         {
         {
             if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
             if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
             {
             {
@@ -210,52 +204,59 @@ namespace Avalonia.Media.Fonts
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Attempts to add the specified <see cref="IGlyphTypeface"/> to the font collection.
+        /// Attempts to add the specified <see cref="GlyphTypeface"/> to the font collection.
         /// </summary>
         /// </summary>
-        /// <remarks>This method checks the <see cref="IGlyphTypeface.FamilyName"/> and, if applicable,
-        /// the typographic family name and other family names provided by the <see cref="IGlyphTypeface2"/> interface.
+        /// <remarks>This method checks the <see cref="GlyphTypeface.FamilyName"/> and, if applicable,
+        /// the typographic family name and other family names provided by the <see cref="GlyphTypeface"/> interface.
         /// If any of these names can be associated with the glyph typeface, the typeface is added to the collection.
         /// If any of these names can be associated with the glyph typeface, the typeface is added to the collection.
         /// The method ensures that duplicate entries are not added.</remarks>
         /// The method ensures that duplicate entries are not added.</remarks>
         /// <param name="glyphTypeface">The glyph typeface to add. Must not be <see langword="null"/> and must have a non-empty <see
         /// <param name="glyphTypeface">The glyph typeface to add. Must not be <see langword="null"/> and must have a non-empty <see
-        /// cref="IGlyphTypeface.FamilyName"/>.</param>
+        /// cref="GlyphTypeface.FamilyName"/>.</param>
         /// <returns><see langword="true"/> if the glyph typeface was successfully added to the collection; otherwise, <see
         /// <returns><see langword="true"/> if the glyph typeface was successfully added to the collection; otherwise, <see
         /// langword="false"/>.</returns>
         /// langword="false"/>.</returns>
-        public bool TryAddGlyphTypeface(IGlyphTypeface glyphTypeface)
+        public bool TryAddGlyphTypeface(GlyphTypeface glyphTypeface)
+        {
+            var key = glyphTypeface.ToFontCollectionKey();
+
+            return TryAddGlyphTypeface(glyphTypeface, key);
+        }
+
+        /// <summary>
+        /// Attempts to add the specified glyph typeface to the collection using the provided key.
+        /// </summary>
+        /// <remarks>The method adds the glyph typeface using both its typographic family name and all
+        /// available family names. If the glyph typeface or its family name is invalid, the method returns false and
+        /// does not add the typeface.</remarks>
+        /// <param name="glyphTypeface">The glyph typeface to add. Cannot be null, and its FamilyName property must not be null or empty.</param>
+        /// <param name="key">The key that identifies the font collection to which the glyph typeface will be added.</param>
+        /// <returns>true if the glyph typeface was successfully added to the collection; otherwise, false.</returns>
+        public bool TryAddGlyphTypeface(GlyphTypeface glyphTypeface, FontCollectionKey key)
         {
         {
             if (glyphTypeface == null || string.IsNullOrEmpty(glyphTypeface.FamilyName))
             if (glyphTypeface == null || string.IsNullOrEmpty(glyphTypeface.FamilyName))
             {
             {
                 return false;
                 return false;
             }
             }
 
 
-            var key = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
+            var result = false;
 
 
-            if (glyphTypeface is IGlyphTypeface2 glyphTypeface2)
+            //Add the TypographicFamilyName to the cache
+            if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName))
             {
             {
-                var result = false;
-
-                //Add the TypographicFamilyName to the cache
-                if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
+                if (TryAddGlyphTypeface(glyphTypeface.TypographicFamilyName, key, glyphTypeface))
                 {
                 {
-                    if (TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, glyphTypeface))
-                    {
-                        result = true;
-                    }
+                    result = true;
                 }
                 }
+            }
 
 
-                foreach (var kvp in glyphTypeface2.FamilyNames)
+            foreach (var kvp in glyphTypeface.FamilyNames)
+            {
+                if (TryAddGlyphTypeface(kvp.Value, key, glyphTypeface))
                 {
                 {
-                    if (TryAddGlyphTypeface(kvp.Value, key, glyphTypeface))
-                    {
-                        result = true;
-                    }
+                    result = true;
                 }
                 }
-
-                return result;
-            }
-            else
-            {
-                return TryAddGlyphTypeface(glyphTypeface.FamilyName, key, glyphTypeface);
             }
             }
+
+            return result;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -265,17 +266,21 @@ namespace Avalonia.Media.Fonts
         /// If successful, it adds the created glyph typeface to the collection.</remarks>
         /// If successful, it adds the created glyph typeface to the collection.</remarks>
         /// <param name="stream">The font stream containing the font data. The stream must be readable and positioned at the beginning of the
         /// <param name="stream">The font stream containing the font data. The stream must be readable and positioned at the beginning of the
         /// font data.</param>
         /// font data.</param>
-        /// <param name="glyphTypeface">When this method returns, contains the created <see cref="IGlyphTypeface"/> instance if the operation
+        /// <param name="glyphTypeface">When this method returns, contains the created <see cref="GlyphTypeface"/> instance if the operation
         /// succeeds; otherwise, <see langword="null"/>.</param>
         /// succeeds; otherwise, <see langword="null"/>.</param>
         /// <returns><see langword="true"/> if the glyph typeface was successfully created and added; otherwise, <see
         /// <returns><see langword="true"/> if the glyph typeface was successfully created and added; otherwise, <see
         /// langword="false"/>.</returns>
         /// langword="false"/>.</returns>
-        public bool TryAddGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        public bool TryAddGlyphTypeface(Stream stream, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
         {
         {
-            if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out glyphTypeface))
+            glyphTypeface = null;
+
+            if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface))
             {
             {
                 return false;
                 return false;
             }
             }
 
 
+            glyphTypeface = new GlyphTypeface(platformTypeface);
+
             return TryAddGlyphTypeface(glyphTypeface);
             return TryAddGlyphTypeface(glyphTypeface);
         }
         }
 
 
@@ -310,17 +315,19 @@ namespace Avalonia.Media.Fonts
                         {
                         {
                             var stream = _assetLoader.Open(fontAsset);
                             var stream = _assetLoader.Open(fontAsset);
 
 
-                            if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
+                            if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface))
                             {
                             {
                                 continue;
                                 continue;
                             }
                             }
 
 
-                            var key = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
+                            var glyphTypeface = new GlyphTypeface(platformTypeface);
+
+                            var key = glyphTypeface.ToFontCollectionKey();
 
 
                             //Add TypographicFamilyName to the cache
                             //Add TypographicFamilyName to the cache
-                            if (glyphTypeface is IGlyphTypeface2 glyphTypeface2 && !string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
+                            if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName))
                             {
                             {
-                                if (TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, glyphTypeface))
+                                if (TryAddGlyphTypeface(glyphTypeface.TypographicFamilyName, key, glyphTypeface))
                                 {
                                 {
                                     result = true;
                                     result = true;
                                 }
                                 }
@@ -346,8 +353,10 @@ namespace Avalonia.Media.Fonts
 
 
                             using var stream = File.OpenRead(source.LocalPath);
                             using var stream = File.OpenRead(source.LocalPath);
 
 
-                            if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
+                            if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface))
                             {
                             {
+                                var glyphTypeface = new GlyphTypeface(platformTypeface);
+
                                 if (TryAddGlyphTypeface(glyphTypeface))
                                 if (TryAddGlyphTypeface(glyphTypeface))
                                 {
                                 {
                                     result = true;
                                     result = true;
@@ -368,8 +377,10 @@ namespace Avalonia.Media.Fonts
                                 {
                                 {
                                     using var stream = File.OpenRead(file);
                                     using var stream = File.OpenRead(file);
 
 
-                                    if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
+                                    if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface))
                                     {
                                     {
+                                        var glyphTypeface = new GlyphTypeface(platformTypeface);
+
                                         if (TryAddGlyphTypeface(glyphTypeface))
                                         if (TryAddGlyphTypeface(glyphTypeface))
                                         {
                                         {
                                             result = true;
                                             result = true;
@@ -445,10 +456,10 @@ namespace Avalonia.Media.Fonts
         /// find the best match based on the provided <paramref name="key"/>.</remarks>
         /// find the best match based on the provided <paramref name="key"/>.</remarks>
         /// <param name="familyName">The name of the font family to search for. This parameter is case-insensitive.</param>
         /// <param name="familyName">The name of the font family to search for. This parameter is case-insensitive.</param>
         /// <param name="key">The key representing the desired font collection attributes.</param>
         /// <param name="key">The key representing the desired font collection attributes.</param>
-        /// <param name="glyphTypeface">When this method returns, contains the matching <see cref="IGlyphTypeface"/> if a match is found; otherwise,
+        /// <param name="glyphTypeface">When this method returns, contains the matching <see cref="GlyphTypeface"/> if a match is found; otherwise,
         /// <see langword="null"/>.</param>
         /// <see langword="null"/>.</param>
         /// <returns><see langword="true"/> if a matching glyph typeface is found; otherwise, <see langword="false"/>.</returns>
         /// <returns><see langword="true"/> if a matching glyph typeface is found; otherwise, <see langword="false"/>.</returns>
-        protected bool TryGetGlyphTypeface(string familyName, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        protected bool TryGetGlyphTypeface(string familyName, FontCollectionKey key, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
         {
         {
             glyphTypeface = null;
             glyphTypeface = null;
 
 
@@ -461,7 +472,7 @@ namespace Avalonia.Media.Fonts
 
 
                 if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
                 if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
                 {
                 {
-                    var matchedKey = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
+                    var matchedKey = glyphTypeface.ToFontCollectionKey();
 
 
                     if (matchedKey != key)
                     if (matchedKey != key)
                     {
                     {
@@ -550,19 +561,21 @@ namespace Avalonia.Media.Fonts
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Attempts to retrieve the nearest matching <see cref="IGlyphTypeface"/> for the specified font key from the
+        /// Attempts to retrieve the nearest matching <see cref="GlyphTypeface"/> for the specified font key from the
         /// provided collection of glyph typefaces.
         /// provided collection of glyph typefaces.
         /// </summary>
         /// </summary>
         /// <remarks>This method attempts to find the best match for the specified font key by considering
         /// <remarks>This method attempts to find the best match for the specified font key by considering
-        /// various fallback strategies, such as normalizing the font style, stretch, and weight. If no suitable match is found, the method will return the first available non-null <see cref="IGlyphTypeface"/> from the
+        /// various fallback strategies, such as normalizing the font style, stretch, and weight. 
+        /// If no suitable match is found, the method will return the first available non-null <see cref="GlyphTypeface"/> from the
         /// collection, if any.</remarks>
         /// collection, if any.</remarks>
         /// <param name="glyphTypefaces">A collection of glyph typefaces, indexed by <see cref="FontCollectionKey"/>.</param>
         /// <param name="glyphTypefaces">A collection of glyph typefaces, indexed by <see cref="FontCollectionKey"/>.</param>
         /// <param name="key">The <see cref="FontCollectionKey"/> representing the desired font attributes.</param>
         /// <param name="key">The <see cref="FontCollectionKey"/> representing the desired font attributes.</param>
-        /// <param name="glyphTypeface">When this method returns, contains the <see cref="IGlyphTypeface"/> that most closely matches the specified
+        /// <param name="glyphTypeface">When this method returns, contains the <see cref="GlyphTypeface"/> that most closely matches the specified
         /// key, if a match is found; otherwise, <see langword="null"/>.</param>
         /// key, if a match is found; otherwise, <see langword="null"/>.</param>
-        /// <returns><see langword="true"/> if a matching <see cref="IGlyphTypeface"/> is found; otherwise, <see
+        /// <returns><see langword="true"/> if a matching <see cref="GlyphTypeface"/> is found; otherwise, <see
         /// langword="false"/>.</returns>
         /// langword="false"/>.</returns>
-        protected bool TryGetNearestMatch(IDictionary<FontCollectionKey, IGlyphTypeface?> glyphTypefaces, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        protected bool TryGetNearestMatch(IDictionary<FontCollectionKey, GlyphTypeface?> glyphTypefaces, 
+            FontCollectionKey key, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
         {
         {
             if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
             if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
             {
             {
@@ -627,7 +640,7 @@ namespace Avalonia.Media.Fonts
         /// <param name="glyphTypeface">The glyph typeface to add to the cache. Can be null.</param>
         /// <param name="glyphTypeface">The glyph typeface to add to the cache. Can be null.</param>
         /// <returns><see langword="true"/> if the glyph typeface was successfully added to the cache; otherwise, <see
         /// <returns><see langword="true"/> if the glyph typeface was successfully added to the cache; otherwise, <see
         /// langword="false"/>.</returns>
         /// langword="false"/>.</returns>
-        protected bool TryAddGlyphTypeface(string familyName, FontCollectionKey key, IGlyphTypeface? glyphTypeface)
+        protected bool TryAddGlyphTypeface(string familyName, FontCollectionKey key, GlyphTypeface? glyphTypeface)
         {
         {
             if (string.IsNullOrEmpty(familyName))
             if (string.IsNullOrEmpty(familyName))
             {
             {
@@ -651,7 +664,7 @@ namespace Avalonia.Media.Fonts
             }
             }
 
 
             // Family doesn't exist yet. Create a new dictionary instance and try to install it.
             // Family doesn't exist yet. Create a new dictionary instance and try to install it.
-            var newDict = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
+            var newDict = new ConcurrentDictionary<FontCollectionKey, GlyphTypeface?>();
 
 
             // GetOrAdd will return the instance that ended up in the dictionary. If it's our
             // GetOrAdd will return the instance that ended up in the dictionary. If it's our
             // newDict instance then we won the race to add the family and should publish it.
             // newDict instance then we won the race to add the family and should publish it.
@@ -693,9 +706,9 @@ namespace Avalonia.Media.Fonts
         /// null.</param>
         /// null.</param>
         /// <returns>true if a suitable fallback glyph typeface is found; otherwise, false.</returns>
         /// <returns>true if a suitable fallback glyph typeface is found; otherwise, false.</returns>
         private static bool TryFindStretchFallback(
         private static bool TryFindStretchFallback(
-           IDictionary<FontCollectionKey, IGlyphTypeface?> glyphTypefaces,
+           IDictionary<FontCollectionKey, GlyphTypeface?> glyphTypefaces,
            FontCollectionKey key,
            FontCollectionKey key,
-           [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+           [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
         {
         {
             glyphTypeface = null;
             glyphTypeface = null;
 
 
@@ -740,9 +753,9 @@ namespace Avalonia.Media.Fonts
         /// null.</param>
         /// null.</param>
         /// <returns>true if a fallback glyph typeface matching the requested weight is found; otherwise, false.</returns>
         /// <returns>true if a fallback glyph typeface matching the requested weight is found; otherwise, false.</returns>
         private static bool TryFindWeightFallback(
         private static bool TryFindWeightFallback(
-            IDictionary<FontCollectionKey, IGlyphTypeface?> glyphTypefaces,
+            IDictionary<FontCollectionKey, GlyphTypeface?> glyphTypefaces,
             FontCollectionKey key,
             FontCollectionKey key,
-            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+            [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
         {
         {
             glyphTypeface = null;
             glyphTypeface = null;
             var weight = (int)key.Weight;
             var weight = (int)key.Weight;

+ 8 - 0
src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs

@@ -1,4 +1,12 @@
 namespace Avalonia.Media.Fonts
 namespace Avalonia.Media.Fonts
 {
 {
+    /// <summary>
+    /// Represents a unique key for identifying a font inside a font collection based on style, weight, and stretch attributes.
+    /// </summary>
+    /// <remarks>Use this key to efficiently look up or group fonts in a collection by their style, weight,
+    /// and stretch characteristics.</remarks>
+    /// <param name="Style">The font style to use when constructing the key.</param>
+    /// <param name="Weight">The font weight to use when constructing the key.</param>
+    /// <param name="Stretch">The font stretch to use when constructing the key.</param>
     public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch);
     public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch);
 }
 }

+ 50 - 0
src/Avalonia.Base/Media/Fonts/FontCollectionKeyExtensions.cs

@@ -0,0 +1,50 @@
+using System;
+using Avalonia.Platform;
+
+namespace Avalonia.Media.Fonts
+{
+    internal static class FontCollectionKeyExtensions
+    {
+        /// <summary>
+        /// Creates a new FontCollectionKey based on the style, weight, and stretch of the specified Typeface.
+        /// </summary>
+        /// <param name="typeface">The Typeface from which to extract style, weight, and stretch information. Cannot be null.</param>
+        /// <returns>A FontCollectionKey representing the style, weight, and stretch of the specified Typeface.</returns>
+        public static FontCollectionKey ToFontCollectionKey(this Typeface typeface)
+        {
+            return new FontCollectionKey(typeface.Style, typeface.Weight, typeface.Stretch);
+        }
+
+        /// <summary>
+        /// Creates a new FontCollectionKey based on the style, weight, and stretch of the specified GlyphTypeface.
+        /// </summary>
+        /// <param name="glyphTypeface">The GlyphTypeface instance from which to extract style, weight, and stretch information. Cannot be null.</param>
+        /// <returns>A FontCollectionKey representing the style, weight, and stretch of the specified glyph typeface.</returns>
+        /// <exception cref="ArgumentNullException">Thrown if glyphTypeface is null.</exception>
+        public static FontCollectionKey ToFontCollectionKey(this GlyphTypeface glyphTypeface)
+        {
+            if (glyphTypeface == null)
+            {
+                throw new ArgumentNullException(nameof(glyphTypeface));
+            }
+
+            return new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
+        }
+
+        /// <summary>
+        /// Creates a new FontCollectionKey based on the style, weight, and stretch of the specified platform typeface.
+        /// </summary>
+        /// <param name="platformTypeface">The platform typeface from which to extract style, weight, and stretch information. Cannot be null.</param>
+        /// <returns>A FontCollectionKey representing the style, weight, and stretch of the specified platform typeface.</returns>
+        /// <exception cref="ArgumentNullException">Thrown if platformTypeface is null.</exception>
+        public static FontCollectionKey ToFontCollectionKey(this IPlatformTypeface platformTypeface)
+        {
+            if (platformTypeface == null)
+            {
+                throw new ArgumentNullException(nameof(platformTypeface));
+            }
+
+            return new FontCollectionKey(platformTypeface.Style, platformTypeface.Weight, platformTypeface.Stretch);
+        }
+    }
+}

+ 13 - 8
src/Avalonia.Base/Media/Fonts/IFontCollection.cs

@@ -5,6 +5,12 @@ using System.Globalization;
 
 
 namespace Avalonia.Media.Fonts
 namespace Avalonia.Media.Fonts
 {
 {
+    /// <summary>
+    /// Represents a collection of font families and provides methods for querying and managing font typefaces
+    /// within the collection.
+    /// </summary>
+    /// <remarks>Implementations of this interface allow applications to retrieve font families, match
+    /// characters to typefaces, and obtain glyph typefaces based on specific font properties.</remarks>
     public interface IFontCollection : IReadOnlyList<FontFamily>, IDisposable
     public interface IFontCollection : IReadOnlyList<FontFamily>, IDisposable
     {
     {
         /// <summary>
         /// <summary>
@@ -22,7 +28,7 @@ namespace Avalonia.Media.Fonts
         /// <param name="glyphTypeface">The glyph typeface.</param>
         /// <param name="glyphTypeface">The glyph typeface.</param>
         /// <returns>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns>
         /// <returns>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns>
         bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
         bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
-            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
+            FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface);
 
 
         /// <summary>
         /// <summary>
         ///     Tries to match a specified character to a <see cref="Typeface"/> that supports specified font properties.
         ///     Tries to match a specified character to a <see cref="Typeface"/> that supports specified font properties.
@@ -39,17 +45,14 @@ namespace Avalonia.Media.Fonts
         /// </returns>
         /// </returns>
         bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
         bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
             FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface);
             FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface);
-    }
 
 
-    internal interface IFontCollection2 : IFontCollection
-    {
         /// <summary>
         /// <summary>
         /// Tries to get a list of typefaces for the specified family name.
         /// Tries to get a list of typefaces for the specified family name.
         /// </summary>
         /// </summary>
         /// <param name="familyName">The family name.</param>
         /// <param name="familyName">The family name.</param>
         /// <param name="familyTypefaces">The list of typefaces.</param>
         /// <param name="familyTypefaces">The list of typefaces.</param>
         /// <returns>
         /// <returns>
-        ///     <c>True</c>, if the <see cref="IFontCollection2"/> could get the list of typefaces, <c>False</c> otherwise.
+        ///     <c>True</c>, if the <see cref="IFontCollection"/> could get the list of typefaces, <c>False</c> otherwise.
         /// </returns>
         /// </returns>
         bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces);
         bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces);
 
 
@@ -62,7 +65,8 @@ namespace Avalonia.Media.Fonts
         /// <param name="stretch">The font stretch.</param>
         /// <param name="stretch">The font stretch.</param>
         /// <param name="syntheticGlyphTypeface"></param>
         /// <param name="syntheticGlyphTypeface"></param>
         /// <returns>Returns <c>true</c> if a synthetic glyph typface can be created; otherwise, <c>false</c></returns>
         /// <returns>Returns <c>true</c> if a synthetic glyph typface can be created; otherwise, <c>false</c></returns>
-        bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface);
+        bool TryCreateSyntheticGlyphTypeface(GlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch,
+            [NotNullWhen(true)] out GlyphTypeface? syntheticGlyphTypeface);
 
 
         /// <summary>
         /// <summary>
         /// Attempts to retrieve the glyph typeface that most closely matches the specified font family name, style,
         /// Attempts to retrieve the glyph typeface that most closely matches the specified font family name, style,
@@ -77,9 +81,10 @@ namespace Avalonia.Media.Fonts
         /// <param name="style">The desired font style.</param>
         /// <param name="style">The desired font style.</param>
         /// <param name="weight">The desired font weight.</param>
         /// <param name="weight">The desired font weight.</param>
         /// <param name="stretch">The desired font stretch.</param>
         /// <param name="stretch">The desired font stretch.</param>
-        /// <param name="glyphTypeface">When this method returns, contains the <see cref="IGlyphTypeface"/> that most closely matches the specified
+        /// <param name="glyphTypeface">When this method returns, contains the <see cref="GlyphTypeface"/> that most closely matches the specified
         /// parameters, if a match is found; otherwise, <see langword="null"/>. This parameter is passed uninitialized.</param>
         /// parameters, if a match is found; otherwise, <see langword="null"/>. This parameter is passed uninitialized.</param>
         /// <returns><see langword="true"/> if a matching glyph typeface is found; otherwise, <see langword="false"/>.</returns>
         /// <returns><see langword="true"/> if a matching glyph typeface is found; otherwise, <see langword="false"/>.</returns>
-        bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
+        bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, 
+            [NotNullWhen(true)] out GlyphTypeface? glyphTypeface);
     }
     }
 }
 }

+ 4 - 4
src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs

@@ -2,11 +2,11 @@
 
 
 namespace Avalonia.Media.Fonts
 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);
-        public static readonly OpenTypeTag MaxSigned = new OpenTypeTag((byte)sbyte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
+        internal static readonly OpenTypeTag None = new OpenTypeTag(0, 0, 0, 0);
+        internal static readonly OpenTypeTag Max = new OpenTypeTag(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
+        internal static readonly OpenTypeTag MaxSigned = new OpenTypeTag((byte)sbyte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
 
 
         private readonly uint _value;
         private readonly uint _value;
 
 

+ 47 - 29
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@@ -26,7 +26,7 @@ namespace Avalonia.Media.Fonts
         public override Uri Key => FontManager.SystemFontsKey;
         public override Uri Key => FontManager.SystemFontsKey;
 
 
         public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
         public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
-            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+            FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
         {
         {
             var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
             var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
 
 
@@ -35,13 +35,7 @@ namespace Avalonia.Media.Fonts
                 return true;
                 return true;
             }
             }
 
 
-            style = typeface.Style;
-
-            weight = typeface.Weight;
-
-            stretch = typeface.Stretch;
-
-            var key = new FontCollectionKey(style, weight, stretch);
+            var key = typeface.ToFontCollectionKey();
 
 
             //Check cache first to avoid unnecessary calls to the font manager
             //Check cache first to avoid unnecessary calls to the font manager
             if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface))
             if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface))
@@ -50,7 +44,7 @@ namespace Avalonia.Media.Fonts
             }
             }
 
 
             //Try to create the glyph typeface via system font manager
             //Try to create the glyph typeface via system font manager
-            if (!_platformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
+            if (!_platformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out var platformTypeface))
             {
             {
                 //Add null to cache to avoid future calls
                 //Add null to cache to avoid future calls
                 TryAddGlyphTypeface(familyName, key, null);
                 TryAddGlyphTypeface(familyName, key, null);
@@ -58,9 +52,23 @@ namespace Avalonia.Media.Fonts
                 return false;
                 return false;
             }
             }
 
 
+            glyphTypeface = new GlyphTypeface(platformTypeface);
+
+            //Add to cache with platform typeface family name first
+            TryAddGlyphTypeface(platformTypeface.FamilyName, key, glyphTypeface);
+
             //Add to cache
             //Add to cache
             if (!TryAddGlyphTypeface(glyphTypeface))
             if (!TryAddGlyphTypeface(glyphTypeface))
             {
             {
+                // Another thread may have added an entry for this key while we were creating the glyph typeface.
+                // Re-check the cache and yield the existing glyph typeface if present.
+                if (_glyphTypefaceCache.TryGetValue(familyName, out var existingMap) && existingMap.TryGetValue(key, out var existingTypeface) && existingTypeface != null)
+                {
+                    glyphTypeface = existingTypeface;
+
+                    return true;
+                }
+
                 return false;
                 return false;
             }
             }
 
 
@@ -70,14 +78,7 @@ namespace Avalonia.Media.Fonts
 
 
         public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
         public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
         {
         {
-            familyTypefaces = null;
-
-            if (_platformImpl is IFontManagerImpl2 fontManagerImpl2)
-            {
-                return fontManagerImpl2.TryGetFamilyTypefaces(familyName, out familyTypefaces);
-            }
-
-            return false;
+            return _platformImpl.TryGetFamilyTypefaces(familyName, out familyTypefaces);
         }
         }
 
 
         public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName,
         public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName,
@@ -85,10 +86,9 @@ namespace Avalonia.Media.Fonts
         {
         {
             var requestedKey = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch };
             var requestedKey = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch };
 
 
-            //TODO12: Think about removing familyName parameter
             if (base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match))
             if (base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match))
             {
             {
-                var matchKey = new FontCollectionKey { Style = match.Style, Weight = match.Weight, Stretch = match.Stretch };
+                var matchKey = match.ToFontCollectionKey();
 
 
                 if (requestedKey == matchKey)
                 if (requestedKey == matchKey)
                 {
                 {
@@ -96,25 +96,43 @@ namespace Avalonia.Media.Fonts
                 }
                 }
             }
             }
 
 
-            if (_platformImpl is IFontManagerImpl2 fontManagerImpl2)
+            if (_platformImpl.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out var platformTypeface))
             {
             {
-                if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out var glyphTypeface))
+                // Construct the resulting Typeface
+                match = new Typeface(platformTypeface.FamilyName, platformTypeface.Style, platformTypeface.Weight,
+                       platformTypeface.Stretch);
+
+                // Compute the key for cache lookup this can be different from the requested key
+                var key = match.ToFontCollectionKey();
+
+                // Check cache first: if an entry exists and is non-null, match succeeded and we can return true.
+                if (_glyphTypefaceCache.TryGetValue(platformTypeface.FamilyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out var existing))
                 {
                 {
-                    match = new Typeface(glyphTypeface.FamilyName, glyphTypeface.Style, glyphTypeface.Weight,
-                       glyphTypeface.Stretch);
+                    return existing != null;
+                }
 
 
-                    // Add to cache if not already present
-                    TryAddGlyphTypeface(glyphTypeface);
+                // Not in cache yet: create glyph typeface and try to add it.
+                var glyphTypeface = new GlyphTypeface(platformTypeface);
 
 
+                // Try adding with the platform typeface family name first.
+                TryAddGlyphTypeface(platformTypeface.FamilyName, key, glyphTypeface);
+
+                // Try adding the glyph typeface with the matched key.
+                if (TryAddGlyphTypeface(glyphTypeface, key))
+                {
                     return true;
                     return true;
                 }
                 }
 
 
+                // TryAddGlyphTypeface failed: another thread may have added an entry. Re-check the cache.
+                if (_glyphTypefaceCache.TryGetValue(platformTypeface.FamilyName, out glyphTypefaces) && glyphTypefaces.TryGetValue(key, out existing))
+                {
+                    return existing != null;
+                }
+
                 return false;
                 return false;
             }
             }
-            else
-            {
-                return _platformImpl.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match);
-            }
+
+            return false;
         }
         }
     }
     }
 }
 }

+ 109 - 200
src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs

@@ -5,104 +5,92 @@
 using System;
 using System;
 using System.Buffers.Binary;
 using System.Buffers.Binary;
 using System.Diagnostics;
 using System.Diagnostics;
-using System.IO;
 using System.Runtime.CompilerServices;
 using System.Runtime.CompilerServices;
 using System.Text;
 using System.Text;
 
 
 namespace Avalonia.Media.Fonts.Tables
 namespace Avalonia.Media.Fonts.Tables
 {
 {
     /// <summary>
     /// <summary>
-    /// BinaryReader using big-endian encoding.
+    /// BinaryReader using big-endian encoding for ReadOnlySpan&lt;byte&gt;.
     /// </summary>
     /// </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>
         /// <summary>
         /// Initializes a new instance of the <see cref="BigEndianBinaryReader" /> class.
         /// 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>
         /// </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>
         /// <summary>
-        /// Gets the underlying stream of the EndianBinaryReader.
+        /// Gets the current position in the span.
         /// </summary>
         /// </summary>
-        public Stream BaseStream { get; }
+        public readonly int Position => _position;
 
 
         /// <summary>
         /// <summary>
-        /// Seeks within the stream.
+        /// Seeks within the span.
         /// </summary>
         /// </summary>
         /// <param name="offset">Offset to seek to.</param>
         /// <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()
         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()
         public sbyte ReadSByte()
         {
         {
-            ReadInternal(_buffer, 1);
-            return unchecked((sbyte)_buffer[0]);
+            EnsureAvailable(1);
+
+            return unchecked((sbyte)_span[_position++]);
         }
         }
 
 
         public float ReadF2dot14()
         public float ReadF2dot14()
         {
         {
             const float f2Dot14ToFloat = 16384.0f;
             const float f2Dot14ToFloat = 16384.0f;
+
             return ReadInt16() / f2Dot14ToFloat;
             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()
         public short ReadInt16()
         {
         {
-            ReadInternal(_buffer, 2);
+            EnsureAvailable(2);
+
+            short value = BinaryPrimitives.ReadInt16BigEndian(_span.Slice(_position, 2));
+
+            _position += 2;
 
 
-            return BinaryPrimitives.ReadInt16BigEndian(_buffer);
+            return value;
         }
         }
 
 
         public TEnum ReadInt16<TEnum>()
         public TEnum ReadInt16<TEnum>()
             where TEnum : struct, Enum
             where TEnum : struct, Enum
         {
         {
             TryConvert(ReadUInt16(), out TEnum value);
             TryConvert(ReadUInt16(), out TEnum value);
+
             return value;
             return value;
         }
         }
 
 
@@ -112,77 +100,75 @@ namespace Avalonia.Media.Fonts.Tables
 
 
         public ushort ReadUFWORD() => ReadUInt16();
         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()
         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;
+        }
+
+        public FontVersion ReadVersion16Dot16()
+        {
+            EnsureAvailable(4);
+
+            uint value = BinaryPrimitives.ReadUInt32BigEndian(_span.Slice(_position, 4));
+
+            _position += 4;
+
+            return new FontVersion(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()
         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()
         public long ReadInt64()
         {
         {
-            ReadInternal(_buffer, 8);
+            EnsureAvailable(8);
+
+            long value = BinaryPrimitives.ReadInt64BigEndian(_span.Slice(_position, 8));
+
+            _position += 8;
 
 
-            return BinaryPrimitives.ReadInt64BigEndian(_buffer);
+            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()
         public ushort ReadUInt16()
         {
         {
-            ReadInternal(_buffer, 2);
+            EnsureAvailable(2);
 
 
-            return BinaryPrimitives.ReadUInt16BigEndian(_buffer);
+            ushort value = BinaryPrimitives.ReadUInt16BigEndian(_span.Slice(_position, 2));
+
+            _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 ushort ReadOffset16() => ReadUInt16();
 
 
         public TEnum ReadUInt16<TEnum>()
         public TEnum ReadUInt16<TEnum>()
             where TEnum : struct, Enum
             where TEnum : struct, Enum
         {
         {
             TryConvert(ReadUInt16(), out TEnum value);
             TryConvert(ReadUInt16(), out TEnum value);
+
             return 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)
         public ushort[] ReadUInt16Array(int length)
         {
         {
             ushort[] data = new ushort[length];
             ushort[] data = new ushort[length];
+
             for (int i = 0; i < length; i++)
             for (int i = 0; i < length; i++)
             {
             {
                 data[i] = ReadUInt16();
                 data[i] = ReadUInt16();
@@ -191,10 +177,6 @@ namespace Avalonia.Media.Fonts.Tables
             return data;
             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)
         public void ReadUInt16Array(Span<ushort> buffer)
         {
         {
             for (int i = 0; i < buffer.Length; i++)
             for (int i = 0; i < buffer.Length; i++)
@@ -203,16 +185,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)
         public uint[] ReadUInt32Array(int length)
         {
         {
             uint[] data = new uint[length];
             uint[] data = new uint[length];
+
             for (int i = 0; i < length; i++)
             for (int i = 0; i < length; i++)
             {
             {
                 data[i] = ReadUInt32();
                 data[i] = ReadUInt32();
@@ -225,21 +201,15 @@ namespace Avalonia.Media.Fonts.Tables
         {
         {
             byte[] data = new byte[length];
             byte[] data = new byte[length];
 
 
-            ReadInternal(data, length);
+            ReadBytesInternal(data, length);
 
 
             return data;
             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)
         public short[] ReadInt16Array(int length)
         {
         {
             short[] data = new short[length];
             short[] data = new short[length];
+
             for (int i = 0; i < length; i++)
             for (int i = 0; i < length; i++)
             {
             {
                 data[i] = ReadInt16();
                 data[i] = ReadInt16();
@@ -248,10 +218,6 @@ namespace Avalonia.Media.Fonts.Tables
             return data;
             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)
         public void ReadInt16Array(Span<short> buffer)
         {
         {
             for (int i = 0; i < buffer.Length; i++)
             for (int i = 0; i < buffer.Length; i++)
@@ -260,110 +226,66 @@ 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()
         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()
         public int ReadUInt24()
         {
         {
             byte highByte = ReadByte();
             byte highByte = ReadByte();
+
             return (highByte << 16) | ReadUInt16();
             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()
         public uint ReadUInt32()
         {
         {
-            ReadInternal(_buffer, 4);
+            EnsureAvailable(4);
+
+            uint value = BinaryPrimitives.ReadUInt32BigEndian(_span.Slice(_position, 4));
 
 
-            return BinaryPrimitives.ReadUInt32BigEndian(_buffer);
+            _position += 4;
+
+            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();
         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)
         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;
             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)
         public string ReadString(int bytesToRead, Encoding encoding)
         {
         {
-            byte[] data = new byte[bytesToRead];
-            ReadInternal(data, bytesToRead);
-            return encoding.GetString(data, 0, data.Length);
+            EnsureAvailable(bytesToRead);
+
+            string result = encoding.GetString(_span.Slice(_position, bytesToRead));
+
+            _position += bytesToRead;
+
+            return result;
         }
         }
 
 
-        /// <summary>
-        /// Reads the uint32 string.
-        /// </summary>
-        /// <returns>a 4 character long UTF8 encoded string.</returns>
         public string ReadTag()
         public string ReadTag()
         {
         {
-            ReadInternal(_buffer, 4);
+            EnsureAvailable(4);
 
 
-            return Encoding.UTF8.GetString(_buffer, 0, 4);
+            string tag = Encoding.UTF8.GetString(_span.Slice(_position, 4));
+
+            _position += 4;
+
+            return tag;
         }
         }
 
 
-        /// <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)
         public int ReadOffset(int size)
             => size switch
             => size switch
             {
             {
@@ -374,33 +296,20 @@ namespace Avalonia.Media.Fonts.Tables
                 _ => throw new InvalidOperationException(),
                 _ => 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.");
             }
             }
         }
         }
 
 

+ 148 - 0
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs

@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace Avalonia.Media.Fonts.Tables.Cmap
+{
+    /// <summary>
+    /// Provides a read-only mapping from Unicode code points to glyph identifiers for a font's character map (cmap)
+    /// table.
+    /// </summary>
+    /// <remarks>This struct enables efficient lookup of glyph IDs corresponding to Unicode code points,
+    /// supporting both Format 4 (BMP) and Format 12 (Unicode full repertoire) cmap subtables. 
+    /// </remarks>
+#pragma warning disable CA1815 // Override equals not needed for readonly struct
+    public readonly struct CharacterToGlyphMap
+#pragma warning restore CA1815 // Override equals not needed for readonly struct
+    {
+        private readonly CmapFormat _format;
+        private readonly CmapFormat4Table? _format4;
+        private readonly CmapFormat12Table? _format12;
+
+        /// <summary>
+        /// Initializes a new instance of the CharacterToGlyphMap class using the specified Format 4 cmap table.
+        /// </summary>
+        /// <param name="table">The Format 4 cmap table that provides character-to-glyph mapping data. Cannot be null.</param>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal CharacterToGlyphMap(CmapFormat4Table table)
+        {
+            _format = CmapFormat.Format4;
+            _format4 = table;
+            _format12 = null;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the CharacterToGlyphMap class using the specified Format 12 character-to-glyph
+        /// mapping table.
+        /// </summary>
+        /// <param name="table">The Format 12 cmap table that defines the mapping from Unicode code points to glyph indices. Cannot be null.</param>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal CharacterToGlyphMap(CmapFormat12Table table)
+        {
+            _format = CmapFormat.Format12;
+            _format12 = table;
+            _format4 = null;
+        }
+
+        /// <summary>
+        /// Gets the glyph index associated with the specified Unicode code point.
+        /// </summary>
+        /// <param name="codePoint">The Unicode code point for which to retrieve the glyph index.</param>
+        /// <returns>The glyph index corresponding to the specified code point.</returns>
+        public ushort this[int codePoint]
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get => GetGlyph(codePoint);
+        }
+
+        /// <summary>
+        /// Retrieves the glyph index that corresponds to the specified Unicode code point.
+        /// </summary>
+        /// <param name="codePoint">The Unicode code point for which to obtain the glyph index.</param>
+        /// <returns>The glyph index associated with the specified code point. Returns 0 if the code point is not mapped to any
+        /// glyph.</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public ushort GetGlyph(int codePoint)
+        {
+            return _format switch
+            {
+                CmapFormat.Format4 => _format4!.GetGlyph(codePoint),
+                CmapFormat.Format12 => _format12!.GetGlyph(codePoint),
+                _ => 0
+            };
+        }
+
+        /// <summary>
+        /// Determines whether the character map contains a glyph for the specified Unicode code point.
+        /// </summary>
+        /// <param name="codePoint">The Unicode code point to check for the presence of a corresponding glyph.</param>
+        /// <returns>true if a glyph exists for the specified code point; otherwise, false.</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public bool ContainsGlyph(int codePoint)
+        {
+            return _format switch
+            {
+                CmapFormat.Format4 => _format4!.ContainsGlyph(codePoint),
+                CmapFormat.Format12 => _format12!.ContainsGlyph(codePoint),
+                _ => false
+            };
+        }
+
+        /// <summary>
+        /// Maps a sequence of Unicode code points to their corresponding glyph IDs using the current character mapping
+        /// format.
+        /// </summary>
+        /// <remarks>If the current character mapping format is not supported, all entries in <paramref
+        /// name="glyphIds"/> are set to zero. The mapping is performed in place, and the method does not allocate
+        /// additional memory.</remarks>
+        /// <param name="codePoints">A read-only span of Unicode code points to be mapped to glyph IDs.</param>
+        /// <param name="glyphIds">A span in which the resulting glyph IDs are written. Must be at least as long as <paramref
+        /// name="codePoints"/>.</param>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public void GetGlyphs(ReadOnlySpan<int> codePoints, Span<ushort> glyphIds)
+        {
+            switch (_format)
+            {
+                case CmapFormat.Format4:
+                    _format4!.GetGlyphs(codePoints, glyphIds);
+                    return;
+                case CmapFormat.Format12:
+                    _format12!.GetGlyphs(codePoints, glyphIds);
+                    return;
+                default:
+                    glyphIds.Clear();
+                    return;
+            }
+        }
+        
+
+        /// <summary>
+        /// Attempts to retrieve the glyph identifier corresponding to the specified Unicode code point.
+        /// </summary>
+        /// <param name="codePoint">The Unicode code point for which to obtain the glyph identifier.</param>
+        /// <param name="glyphId">When this method returns, contains the glyph identifier associated with the specified code point, if found;
+        /// otherwise, zero. This parameter is passed uninitialized.</param>
+        /// <returns>true if a glyph identifier was found for the specified code point; otherwise, false.</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)] 
+        public bool TryGetGlyph(int codePoint, out ushort glyphId) 
+        { 
+            switch (_format) 
+            { 
+                case CmapFormat.Format4: return _format4!.TryGetGlyph(codePoint, out glyphId); 
+                case CmapFormat.Format12: return _format12!.TryGetGlyph(codePoint, out glyphId);
+                default: glyphId = 0; return false; 
+            } 
+        }
+
+        /// <summary>
+        /// Returns an enumerator that iterates through all code point ranges mapped by this instance.
+        /// </summary>
+        /// <returns>A <see cref="CodepointRangeEnumerator"/> that can be used to enumerate the mapped code point ranges.</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)] 
+        public CodepointRangeEnumerator GetMappedRanges() 
+        { 
+            return new CodepointRangeEnumerator(_format, _format4, _format12);
+        }
+    }
+}

+ 34 - 0
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 - 0
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
+    }
+}

+ 258 - 0
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs

@@ -0,0 +1,258 @@
+using System;
+using System.Buffers.Binary;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Media.Fonts.Tables.Cmap
+{
+    internal sealed class CmapFormat12Table
+    {
+        private readonly ReadOnlyMemory<byte> _table;
+        private readonly int _groupCount;
+        private readonly ReadOnlyMemory<byte> _groups;
+
+        /// <summary>
+        /// Gets the language code for the cmap subtable.
+        /// For non-language-specific tables, this value is 0.
+        /// </summary>
+        public uint Language { get; }
+
+        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);
+
+            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);
+        }
+
+        /// <summary>
+        /// Retrieves the glyph index corresponding to the specified Unicode code point.
+        /// </summary>
+        /// <param name="codePoint">The Unicode code point for which to obtain the glyph index. Must be a valid code point supported by the
+        /// font.</param>
+        /// <returns>The glyph index as an unsigned 16-bit integer for the specified code point.</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public ushort GetGlyph(int codePoint) => this[codePoint];
+
+        /// <summary>
+        /// Determines whether the specified Unicode code point is present in the glyph set.
+        /// </summary>
+        /// <param name="codePoint">The Unicode code point to check for presence in the glyph set. Must be a valid integer representing a
+        /// Unicode character.</param>
+        /// <returns>true if the glyph set contains the specified code point; otherwise, false.</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public bool ContainsGlyph(int codePoint)
+        {
+            return FindGroupIndex(codePoint) >= 0;
+        }
+
+        /// <summary>
+        /// Maps multiple Unicode code points to glyph indices in a single operation.
+        /// </summary>
+        /// <param name="codePoints">Read-only span of code points to map.</param>
+        /// <param name="glyphIds">Output span to write glyph IDs. Must be at least as long as <paramref name="codePoints"/>.</param>
+        /// <remarks>
+        /// This method is significantly more efficient than calling the indexer multiple times as it:
+        /// - Reuses span references (no repeated memory access)
+        /// - Caches group data for sequential lookups
+        /// - Optimizes for locality of code points (common in text runs)
+        /// Format 12 is commonly used for fonts with large character sets (CJK, emoji, etc.)
+        /// This is the preferred method for batch character-to-glyph mapping in text shaping.
+        /// </remarks>
+        public void GetGlyphs(ReadOnlySpan<int> codePoints, Span<ushort> glyphIds)
+        {
+            if (glyphIds.Length < codePoints.Length)
+            {
+                throw new ArgumentException("Output span must be at least as long as input span", nameof(glyphIds));
+            }
+
+            var groups = _groups.Span;
+
+            // Track last group for locality optimization
+            int lastGroup = -1;
+            uint lastStart = 0;
+            uint lastEnd = 0;
+            uint lastStartGlyph = 0;
+
+            for (int i = 0; i < codePoints.Length; i++)
+            {
+                int codePoint = codePoints[i];
+
+                // Optimization: check if codepoint is in the same group as previous
+                if (lastGroup >= 0 && codePoint >= lastStart && codePoint <= lastEnd)
+                {
+                    glyphIds[i] = (ushort)(lastStartGlyph + (codePoint - lastStart));
+                    continue;
+                }
+
+                // Binary search for group
+                int groupIndex = FindGroupIndexOptimized(codePoint, groups);
+
+                if (groupIndex < 0)
+                {
+                    glyphIds[i] = 0;
+                    lastGroup = -1;
+
+                    continue;
+                }
+
+                // Cache group data
+                lastGroup = groupIndex;
+                lastStart = ReadUInt32BE(groups, groupIndex, 0);
+                lastEnd = ReadUInt32BE(groups, groupIndex, 4);
+                lastStartGlyph = ReadUInt32BE(groups, groupIndex, 8);
+
+                glyphIds[i] = (ushort)(lastStartGlyph + (codePoint - lastStart));
+            }
+        }
+
+        public bool TryGetGlyph(int codePoint, out ushort glyphId)
+        {
+            int groupIndex = FindGroupIndex(codePoint);
+
+            if (groupIndex < 0)
+            {
+                glyphId = 0;
+                return false;
+            }
+
+            var groups = _groups.Span;
+
+            uint start = ReadUInt32BE(groups, groupIndex, 0);
+            uint startGlyph = ReadUInt32BE(groups, groupIndex, 8);
+
+            glyphId = (ushort)(startGlyph + (codePoint - start));
+
+            return glyphId != 0;
+        }
+
+        internal bool TryGetRange(int index, out CodepointRange range)
+        {
+            if ((uint)index >= (uint)_groupCount)
+            {
+                range = default;
+
+                return false;
+            }
+
+            var groups = _groups.Span;
+
+            int start = (int)ReadUInt32BE(groups, index, 0);
+            int end = (int)ReadUInt32BE(groups, index, 4);
+
+            range = new CodepointRange(start, end);
+
+            return true;
+        }
+
+        public ushort this[int codePoint]
+        {
+            get
+            {
+                int groupIndex = FindGroupIndex(codePoint);
+
+                if (groupIndex < 0)
+                {
+                    return 0;
+                }
+
+                var groups = _groups.Span;
+
+                uint start = ReadUInt32BE(groups, groupIndex, 0);
+                uint startGlyph = ReadUInt32BE(groups, groupIndex, 8);
+
+                // Calculate glyph index
+                return (ushort)(startGlyph + (codePoint - start));
+            }
+        }
+
+        // Optimized binary search that works directly with cached span
+        private int FindGroupIndexOptimized(int codePoint, ReadOnlySpan<byte> groups)
+        {
+            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;
+                }
+            }
+
+            return -1;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static uint ReadUInt32BE(ReadOnlySpan<byte> span, int groupIndex, int fieldOffset)
+        {
+            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;
+
+            var groups = _groups.Span;
+
+            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;
+        }
+    }
+}

+ 454 - 0
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat4Table.cs

@@ -0,0 +1,454 @@
+using System;
+using System.Buffers.Binary;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Media.Fonts.Tables.Cmap
+{
+    internal sealed class CmapFormat4Table
+    {
+        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;
+
+        /// <summary>
+        /// Gets the language code for the cmap subtable.
+        /// For non-language-specific tables, this value is 0.
+        /// </summary>
+        public ushort Language { get; }
+
+        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);
+
+            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 (warn in debug builds instead of asserting)
+#if DEBUG
+            var expectedSearchRange = (ushort)(2 * (1 << (int)Math.Floor(Math.Log(_segCount, 2))));
+            if (searchRange != expectedSearchRange)
+            {
+                Debug.WriteLine($"CMAP format 4: unexpected searchRange {searchRange}, expected {expectedSearchRange} for segCount {_segCount}.");
+            }
+
+            var expectedEntrySelector = (ushort)Math.Floor(Math.Log(_segCount, 2));
+            if (entrySelector != expectedEntrySelector)
+            {
+                Debug.WriteLine($"CMAP format 4: unexpected entrySelector {entrySelector}, expected {expectedEntrySelector} for segCount {_segCount}.");
+            }
+
+            var expectedRangeShift = (ushort)(segCountX2 - searchRange);
+            if (rangeShift != expectedRangeShift)
+            {
+                Debug.WriteLine($"CMAP format 4: unexpected rangeShift {rangeShift}, expected {expectedRangeShift} for segCountX2 {segCountX2} and searchRange {searchRange}.");
+            }
+#endif
+
+            // 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);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public ushort GetGlyph(int codePoint) => this[codePoint];
+
+        public bool ContainsGlyph(int codePoint)
+        {
+            int seg = FindSegmentIndex(codePoint);
+
+            if ((uint)seg >= (uint)_segCount)
+            {
+                return false;
+            }
+
+            ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets.Span, seg);
+            ushort idDelta = ReadUInt16BE(_idDeltas.Span, seg);
+
+            if (idRangeOffset == 0)
+            {
+                // Always maps to something (possibly .notdef via delta)
+                return ((codePoint + idDelta) & 0xFFFF) != 0;
+            }
+
+            int start = ReadUInt16BE(_startCodes.Span, seg);
+            int ro = idRangeOffset >> 1;
+            int idx = (codePoint - start) + ro - (_segCount - seg);
+
+            if ((uint)idx >= (uint)(_glyphIdArray.Length >> 1))
+            {
+                return false;
+            }
+
+            ushort glyphId = ReadUInt16BE(_glyphIdArray.Span, idx);
+
+            return glyphId != 0;
+        }
+
+        /// <summary>
+        /// Maps multiple Unicode code points to glyph indices in a single operation.
+        /// </summary>
+        /// <param name="codePoints">Read-only span of code points to map.</param>
+        /// <param name="glyphIds">Output span to write glyph IDs. Must be at least as long as <paramref name="codePoints"/>.</param>
+        /// <remarks>
+        /// This method is significantly more efficient than calling the indexer multiple times as it:
+        /// - Reuses span references (no repeated .Span property access)
+        /// - Caches segment data for sequential lookups
+        /// - Optimizes for locality of code points (common in text runs)
+        /// This is the preferred method for batch character-to-glyph mapping in text shaping.
+        /// </remarks>
+        public void GetGlyphs(ReadOnlySpan<int> codePoints, Span<ushort> glyphIds)
+        {
+            if (glyphIds.Length < codePoints.Length)
+            {
+                throw new ArgumentException("Output span must be at least as long as input span", nameof(glyphIds));
+            }
+
+            // Cache all spans once
+            var startCodes = _startCodes.Span;
+            var endCodes = _endCodes.Span;
+            var idDeltas = _idDeltas.Span;
+            var idRangeOffsets = _idRangeOffsets.Span;
+            var glyphIdArray = _glyphIdArray.Span;
+            int glyphArrayWords = glyphIdArray.Length / 2;
+
+            // Track last segment for locality optimization
+            int lastSegment = -1;
+
+            for (int i = 0; i < codePoints.Length; i++)
+            {
+                int codePoint = codePoints[i];
+                int segmentIndex;
+
+                // Optimization: check if codepoint is in the same segment as previous
+                if (lastSegment >= 0 && lastSegment < _segCount)
+                {
+                    int lastStart = ReadUInt16BE(startCodes, lastSegment);
+                    int lastEnd = ReadUInt16BE(endCodes, lastSegment);
+
+                    if (codePoint >= lastStart && codePoint <= lastEnd)
+                    {
+                        segmentIndex = lastSegment;
+                        goto MapGlyph;
+                    }
+                }
+
+                // Binary search for segment
+                segmentIndex = FindSegmentIndexOptimized(codePoint, startCodes, endCodes);
+
+                if (segmentIndex < 0)
+                {
+                    glyphIds[i] = 0;
+                    continue;
+                }
+
+                lastSegment = segmentIndex;
+
+            MapGlyph:
+                ushort idRangeOffset = ReadUInt16BE(idRangeOffsets, segmentIndex);
+                ushort idDelta = ReadUInt16BE(idDeltas, segmentIndex);
+
+                if (idRangeOffset == 0)
+                {
+                    glyphIds[i] = (ushort)((codePoint + idDelta) & 0xFFFF);
+                }
+                else
+                {
+                    int start = ReadUInt16BE(startCodes, segmentIndex);
+                    int ro = idRangeOffset / 2;
+                    int idx = (codePoint - start) + ro - (_segCount - segmentIndex);
+
+                    if ((uint)idx < (uint)glyphArrayWords)
+                    {
+                        ushort glyphId = ReadUInt16BE(glyphIdArray, idx);
+
+                        if (glyphId != 0)
+                        {
+                            glyphId = (ushort)((glyphId + idDelta) & 0xFFFF);
+                        }
+
+                        glyphIds[i] = glyphId;
+                    }
+                    else
+                    {
+                        glyphIds[i] = 0;
+                    }
+                }
+            }
+        }
+
+        public bool TryGetGlyph(int codePoint, out ushort glyphId)
+        {
+            int seg = FindSegmentIndex(codePoint);
+
+            if ((uint)seg >= (uint)_segCount)
+            {
+                glyphId = 0;
+
+                return false;
+            }
+
+            ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets.Span, seg);
+            ushort idDelta = ReadUInt16BE(_idDeltas.Span, seg);
+
+            if (idRangeOffset == 0)
+            {
+                glyphId = (ushort)((codePoint + idDelta) & 0xFFFF);
+
+                return glyphId != 0;
+            }
+
+            int start = ReadUInt16BE(_startCodes.Span, seg);
+            int ro = idRangeOffset >> 1;
+            int idx = (codePoint - start) + ro - (_segCount - seg);
+
+            if ((uint)idx >= (uint)(_glyphIdArray.Length >> 1))
+            {
+                glyphId = 0;
+
+                return false;
+            }
+
+            glyphId = ReadUInt16BE(_glyphIdArray.Span, idx);
+
+            if (glyphId != 0)
+            {
+                glyphId = (ushort)((glyphId + idDelta) & 0xFFFF);
+            }
+
+            return glyphId != 0;
+        }
+
+        internal bool TryGetRange(int index, out CodepointRange range)
+        {
+            if ((uint)index >= (uint)_segCount)
+            {
+                range = default;
+                return false;
+            }
+
+            int start = ReadUInt16BE(_startCodes.Span, index);
+            int end = ReadUInt16BE(_endCodes.Span, index);
+
+            // Skip sentinel segment (0xFFFF)
+            if (start == 0xFFFF && end == 0xFFFF)
+            {
+                range = default;
+
+                return false;
+            }
+
+            range = new CodepointRange(start, end);
+
+            return true;
+        }
+                
+        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.Span, segmentIndex);
+                ushort idDelta = ReadUInt16BE(_idDeltas.Span, segmentIndex);
+
+                // If idRangeOffset is 0, glyphId = (codePoint + idDelta) % 65536
+                if (idRangeOffset == 0)
+                {
+                    return (ushort)((codePoint + idDelta) & 0xFFFF);
+                }
+                else
+                {
+                    int start = ReadUInt16BE(_startCodes.Span, 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.Span, 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;
+            }
+        }
+
+        // Resolves the glyph ID for a given code point within a specific segment
+        private ushort ResolveGlyph(int segmentIndex, int codePoint)
+        {
+            ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets.Span, segmentIndex);
+            ushort idDelta = ReadUInt16BE(_idDeltas.Span, segmentIndex);
+
+            if (idRangeOffset == 0)
+            {
+                return (ushort)((codePoint + idDelta) & 0xFFFF);
+            }
+            else
+            {
+                int start = ReadUInt16BE(_startCodes.Span, 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.Span, 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;
+
+            var startCodes = _startCodes.Span;
+            var endCodes = _endCodes.Span;
+
+            // 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
+        }
+
+        // Optimized binary search that works directly with cached spans
+        private int FindSegmentIndexOptimized(int codePoint, ReadOnlySpan<byte> startCodes, ReadOnlySpan<byte> endCodes)
+        {
+            int lo = 0;
+            int hi = _segCount - 1;
+
+            while (lo <= hi)
+            {
+                int mid = (lo + hi) >> 1;
+                int end = ReadUInt16BE(endCodes, mid);
+
+                if (codePoint > end)
+                {
+                    lo = mid + 1;
+                }
+                else
+                {
+                    hi = mid - 1;
+                }
+            }
+
+            if (lo < _segCount)
+            {
+                int start = ReadUInt16BE(startCodes, lo);
+
+                if (codePoint >= start)
+                {
+                    return lo;
+                }
+            }
+
+            return -1;
+        }
+
+        // Reads a big-endian UInt16 from the specified word index in the given memory
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static ushort ReadUInt16BE(ReadOnlySpan<byte> span, int wordIndex)
+        {
+            int byteIndex = wordIndex * 2;
+
+            // Ensure we don't go out of bounds
+            return BinaryPrimitives.ReadUInt16BigEndian(span.Slice(byteIndex, 2));
+        }
+    }
+}

+ 42 - 0
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)
+        {
+            Platform = platform;
+            Encoding = encoding;
+            Offset = offset;
+            Format = format;
+        }
+
+        /// <summary>
+        /// Gets the platform identifier for the current environment.
+        /// </summary>
+        public PlatformID Platform { get; }
+
+        /// <summary>
+        /// Gets the character map (CMap) encoding associated with this instance.
+        /// </summary>
+        /// 
+        public CmapEncoding Encoding { get; }
+
+        /// <summary>
+        /// Gets the offset of the sub table.
+        /// </summary>
+        public int Offset { get; }
+
+        /// <summary>
+        /// Gets the format of the character-to-glyph mapping (cmap) table.
+        /// </summary>
+        public CmapFormat Format { get; }
+
+        public ReadOnlyMemory<byte> GetSubtableMemory(ReadOnlyMemory<byte> table)
+        {
+            return table.Slice(Offset);
+        }
+    }
+}

+ 166 - 0
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 CharacterToGlyphMap Load(GlyphTypeface 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 CharacterToGlyphMap(new CmapFormat12Table(format12Entry.GetSubtableMemory(table)));
+            }
+
+            // Fallback to Format 4
+            if (TryFindFormat4Entry(entries, out var format4Entry))
+            {
+                return new CharacterToGlyphMap(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;
+            }
+        }
+    }
+}

+ 52 - 0
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRange.cs

@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace Avalonia.Media.Fonts.Tables.Cmap
+{
+    /// <summary>
+    /// Represents a range of Unicode code points, defined by inclusive start and end values.
+    /// </summary>
+    public readonly struct CodepointRange
+    {
+        /// <summary>
+        /// Gets the start of the range.
+        /// </summary>
+        public readonly int Start;
+
+        /// <summary>
+        /// Gets the end of the range.
+        /// </summary>
+        public readonly int End;
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public CodepointRange(int start, int end)
+        {
+            Start = start;
+            End = end;
+        }
+
+        public override bool Equals(object? obj)
+        {
+            return obj is CodepointRange range &&
+                   Start == range.Start &&
+                   End == range.End;
+        }
+
+        public override int GetHashCode()
+        {
+            return HashCode.Combine(Start, End);
+        }
+
+        public static bool operator ==(CodepointRange left, CodepointRange right)
+        {
+            return left.Equals(right);
+        }
+
+        public static bool operator !=(CodepointRange left, CodepointRange right)
+        {
+            return !(left == right);
+        }
+    }
+}

+ 71 - 0
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs

@@ -0,0 +1,71 @@
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Media.Fonts.Tables.Cmap
+{
+    /// <summary>
+    /// Enumerates contiguous ranges of Unicode code points present in a character map (cmap) table.
+    /// </summary>
+    /// <remarks>This enumerator is typically used to iterate over all code point ranges defined by a cmap
+    /// table in an OpenType or TrueType font. It supports both Format 4 and Format 12 cmap subtables. The enumerator is
+    /// a ref struct and must be used within the stack context; it cannot be stored on the heap or used across await or
+    /// yield boundaries.</remarks>
+    public ref struct CodepointRangeEnumerator
+    {
+        private readonly CmapFormat _format;
+        private readonly CmapFormat4Table? _f4;
+        private readonly CmapFormat12Table? _f12;
+        private int _index;
+
+        internal CodepointRangeEnumerator(CmapFormat format, CmapFormat4Table? f4, CmapFormat12Table? f12)
+        {
+            _format = format;
+            _f4 = f4;
+            _f12 = f12;
+            _index = -1;
+        }
+
+        /// <summary>
+        /// Gets the current code point range in the enumeration sequence.
+        /// </summary>
+        public CodepointRange Current { get; private set; }
+
+        /// <summary>
+        /// Advances the enumerator to the next character mapping range in the collection.
+        /// </summary>
+        /// <remarks>After calling MoveNext, check the Current property to access the current character
+        /// mapping range. If the end of the collection is reached, MoveNext returns false and Current is set to its
+        /// default value.</remarks>
+        /// <returns>true if the enumerator was successfully advanced to the next range; otherwise, false.</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public bool MoveNext()
+        {
+            _index++;
+
+            switch (_format)
+            {
+                case CmapFormat.Format4:
+                    {
+                        var result = _f4!.TryGetRange(_index, out var range);
+
+                        Current = range;
+
+                        return result;
+                    }
+                case CmapFormat.Format12:
+                    {
+                        var result = _f12!.TryGetRange(_index, out var range);
+
+                        Current = range;
+
+                        return result;
+                    }
+                default:
+                    {
+                        Current = default;
+
+                        return false;
+                    }
+            }
+        }
+    }
+}

+ 38 - 26
src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs

@@ -2,8 +2,8 @@
 // Licensed under the Apache License, Version 2.0.
 // Licensed under the Apache License, Version 2.0.
 // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
 // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
 
 
+using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.IO;
 
 
 namespace Avalonia.Media.Fonts.Tables
 namespace Avalonia.Media.Fonts.Tables
 {
 {
@@ -17,8 +17,8 @@ namespace Avalonia.Media.Fonts.Tables
     /// </summary>
     /// </summary>
     internal class FeatureListTable
     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)
         private FeatureListTable(IReadOnlyList<OpenTypeTag> features)
         {
         {
@@ -27,34 +27,31 @@ namespace Avalonia.Media.Fonts.Tables
 
 
         public IReadOnlyList<OpenTypeTag> Features { get; }
         public IReadOnlyList<OpenTypeTag> Features { get; }
 
 
-        public static FeatureListTable? LoadGSub(IGlyphTypeface glyphTypeface)
+        public static FeatureListTable? LoadGSub(GlyphTypeface glyphTypeface)
         {
         {
-            if (!glyphTypeface.TryGetTable(GSubTag, out var gPosTable))
+            if (!glyphTypeface.PlatformTypeface.TryGetTable(GSubTag, out var gPosTable))
             {
             {
                 return null;
                 return null;
             }
             }
 
 
-            using var stream = new MemoryStream(gPosTable);
-            using var reader = new BigEndianBinaryReader(stream, false);
-
-            return Load(reader);
+            var reader = new BigEndianBinaryReader(gPosTable.Span);
 
 
+            return Load(ref reader);
         }
         }
-        public static FeatureListTable? LoadGPos(IGlyphTypeface glyphTypeface)
+
+        public static FeatureListTable? LoadGPos(GlyphTypeface glyphTypeface)
         {
         {
-            if (!glyphTypeface.TryGetTable(GPosTag, out var gSubTable))
+            if (!glyphTypeface.PlatformTypeface.TryGetTable(GPosTag, out var gSubTable))
             {
             {
                 return null;
                 return null;
             }
             }
 
 
-            using var stream = new MemoryStream(gSubTable);
-            using var reader = new BigEndianBinaryReader(stream, false);
-
-            return Load(reader);
+            var reader = new BigEndianBinaryReader(gSubTable.Span);
 
 
+            return Load(ref reader);
         }
         }
 
 
-        private static FeatureListTable Load(BigEndianBinaryReader reader)
+        private static FeatureListTable Load(ref BigEndianBinaryReader reader)
         {
         {
             // GPOS/GSUB Header, Version 1.0
             // GPOS/GSUB Header, Version 1.0
             // +----------+-------------------+-----------------------------------------------------------+
             // +----------+-------------------+-----------------------------------------------------------+
@@ -73,14 +70,14 @@ namespace Avalonia.Media.Fonts.Tables
 
 
             reader.ReadUInt16();
             reader.ReadUInt16();
             reader.ReadUInt16();
             reader.ReadUInt16();
-
             reader.ReadOffset16();
             reader.ReadOffset16();
+
             var featureListOffset = reader.ReadOffset16();
             var featureListOffset = reader.ReadOffset16();
 
 
-            return Load(reader, featureListOffset);
+            return Load(ref reader, featureListOffset);
         }
         }
 
 
-        private static FeatureListTable Load(BigEndianBinaryReader reader, long offset)
+        private static FeatureListTable Load(ref BigEndianBinaryReader reader, int offset)
         {
         {
             // FeatureList
             // FeatureList
             // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+
             // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+
@@ -90,11 +87,21 @@ namespace Avalonia.Media.Fonts.Tables
             // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+
             // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+
             // | FeatureRecord | featureRecords[featureCount] | Array of FeatureRecords — zero-based (first feature has FeatureIndex = 0), listed alphabetically by feature tag |
             // | 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();
             var featureCount = reader.ReadUInt16();
 
 
-            var features = new List<OpenTypeTag>(featureCount);
+            if (featureCount == 0)
+            {
+                return new FeatureListTable(Array.Empty<OpenTypeTag>());
+            }
+
+            // Use stackalloc for small counts, array for larger
+            Span<OpenTypeTag> tempFeatures = featureCount <= 64 
+                ? stackalloc OpenTypeTag[featureCount] 
+                : new OpenTypeTag[featureCount];
+
+            int uniqueCount = 0;
 
 
             for (var i = 0; i < featureCount; i++)
             for (var i = 0; i < featureCount; i++)
             {
             {
@@ -107,19 +114,24 @@ namespace Avalonia.Media.Fonts.Tables
                 // | Offset16 | featureOffset | Offset to Feature table, from beginning of FeatureList |
                 // | Offset16 | featureOffset | Offset to Feature table, from beginning of FeatureList |
                 // +----------+---------------+--------------------------------------------------------+
                 // +----------+---------------+--------------------------------------------------------+
                 var featureTag = reader.ReadUInt32();
                 var featureTag = reader.ReadUInt32();
-
                 reader.ReadOffset16();
                 reader.ReadOffset16();
 
 
                 var tag = new OpenTypeTag(featureTag);
                 var tag = new OpenTypeTag(featureTag);
 
 
-                if (!features.Contains(tag))
+                // Check for duplicates in already added features
+                bool isDuplicate = tempFeatures.Contains(tag);
+
+                if (!isDuplicate)
                 {
                 {
-                    features.Add(tag);
+                    tempFeatures[uniqueCount++] = tag;
                 }
                 }
             }
             }
 
 
-            return new FeatureListTable(features /*featureTables*/);
-        }
+            // Create array with only unique features
+            var features = new OpenTypeTag[uniqueCount];
+            tempFeatures.Slice(0, uniqueCount).CopyTo(features);
 
 
+            return new FeatureListTable(features);
+        }
     }
     }
 }
 }

+ 75 - 0
src/Avalonia.Base/Media/Fonts/Tables/FontVersion.cs

@@ -0,0 +1,75 @@
+using System.Diagnostics;
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    /// <summary>
+    /// Represents a Version16Dot16 value from OpenType font tables.
+    /// The high 16 bits represent the major version, and the low 16 bits represent the minor version as a fraction.
+    /// </summary>
+    [DebuggerDisplay("{ToString(),nq}")]
+    internal readonly struct FontVersion
+    {
+        /// <summary>
+        /// Gets the major version number.
+        /// </summary>
+        public ushort Major { get; }
+        
+        /// <summary>
+        /// Gets the minor version number (as a fraction of 65536).
+        /// </summary>
+        public ushort Minor { get; }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="FontVersion"/> struct from raw Version16Dot16 value.
+        /// </summary>
+        /// <param name="value">The 32-bit Version16Dot16 value.</param>
+        public FontVersion(uint value)
+        {
+            Major = (ushort)(value >> 16);
+            Minor = (ushort)(value & 0xFFFF);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="FontVersion"/> struct from major and minor components.
+        /// </summary>
+        /// <param name="major">The major version number.</param>
+        /// <param name="minor">The minor version number (as a fraction of 65536).</param>
+        public FontVersion(ushort major, ushort minor)
+        {
+            Major = major;
+            Minor = minor;
+        }
+
+        /// <summary>
+        /// Converts the version to a floating-point representation.
+        /// </summary>
+        public float ToFloat() => Major + (Minor / 65536f);
+
+        /// <summary>
+        /// Returns the raw 32-bit Version16Dot16 value.
+        /// </summary>
+        public uint ToUInt32() => ((uint)Major << 16) | Minor;
+
+        public override string ToString()
+        {
+            // For common fractional values, show them nicely (e.g., 2.5 instead of 2.5000076)
+            if (Minor == 0)
+                return Major.ToString();
+            if (Minor == 0x8000) // 0.5
+                return $"{Major}.5";
+            
+            return ToFloat().ToString("F6").TrimEnd('0').TrimEnd('.');
+        }
+
+        public static implicit operator float(FontVersion version) => version.ToFloat();
+
+        public static bool operator ==(FontVersion left, FontVersion right) =>
+            left.Major == right.Major && left.Minor == right.Minor;
+
+        public static bool operator !=(FontVersion left, FontVersion right) => !(left == right);
+
+        public override bool Equals(object? obj) => obj is FontVersion other && this == other;
+
+        public override int GetHashCode() => ((int)Major << 16) | Minor;
+    }
+}

+ 322 - 0
src/Avalonia.Base/Media/Fonts/Tables/HeadTable.cs

@@ -0,0 +1,322 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    internal sealed class HeadTable
+    {
+        internal const string TableName = "head";
+        internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName);
+
+        private static readonly DateTime s_fontEpoch = new DateTime(1904, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+        public FontVersion Version { get; }
+        public FontVersion FontRevision { get; }
+        public uint CheckSumAdjustment { get; }
+        public uint MagicNumber { get; }
+        public HeadFlags Flags { get; }
+        public ushort UnitsPerEm { get; }
+        public DateTime Created { get; }
+        public DateTime Modified { get; }
+        public short XMin { get; }
+        public short YMin { get; }
+        public short XMax { get; }
+        public short YMax { get; }
+        public MacStyleFlags MacStyle { get; }
+        public ushort LowestRecPPEM { get; }
+        public FontDirectionHint FontDirectionHint { get; }
+        public IndexToLocFormat IndexToLocFormat { get; }
+        public GlyphDataFormat GlyphDataFormat { get; }
+
+        private HeadTable(
+            FontVersion version,
+            FontVersion fontRevision,
+            uint checkSumAdjustment,
+            uint magicNumber,
+            HeadFlags flags,
+            ushort unitsPerEm,
+            DateTime created,
+            DateTime modified,
+            short xMin,
+            short yMin,
+            short xMax,
+            short yMax,
+            MacStyleFlags macStyle,
+            ushort lowestRecPPEM,
+            FontDirectionHint fontDirectionHint,
+            IndexToLocFormat indexToLocFormat,
+            GlyphDataFormat 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 bool TryLoad(GlyphTypeface glyphTypeface, [NotNullWhen(true)] out HeadTable? headTable)
+        {
+            headTable = null;
+
+            if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table))
+            {
+                return false;
+            }
+
+            var reader = new BigEndianBinaryReader(table.Span);
+            headTable = Load(ref reader);
+
+            return true;
+        }
+
+        private static HeadTable Load(ref BigEndianBinaryReader reader)
+        {
+            FontVersion version = reader.ReadVersion16Dot16();
+            FontVersion fontRevision = reader.ReadVersion16Dot16();
+            uint checkSumAdjustment = reader.ReadUInt32();
+            uint magicNumber = reader.ReadUInt32();
+            HeadFlags flags = (HeadFlags)reader.ReadUInt16();
+            ushort unitsPerEm = reader.ReadUInt16();
+            long createdRaw = reader.ReadInt64();
+            long modifiedRaw = reader.ReadInt64();
+            short xMin = reader.ReadInt16();
+            short yMin = reader.ReadInt16();
+            short xMax = reader.ReadInt16();
+            short yMax = reader.ReadInt16();
+            MacStyleFlags macStyle = (MacStyleFlags)reader.ReadUInt16();
+            ushort lowestRecPPEM = reader.ReadUInt16();
+            FontDirectionHint fontDirectionHint = (FontDirectionHint)reader.ReadInt16();
+            IndexToLocFormat indexToLocFormat = (IndexToLocFormat)reader.ReadInt16();
+            GlyphDataFormat glyphDataFormat = (GlyphDataFormat)reader.ReadInt16();
+
+            DateTime created = SafeAddSeconds(s_fontEpoch, createdRaw);
+            DateTime modified = SafeAddSeconds(s_fontEpoch, modifiedRaw);
+
+            return new HeadTable(
+                version,
+                fontRevision,
+                checkSumAdjustment,
+                magicNumber,
+                flags,
+                unitsPerEm,
+                created,
+                modified,
+                xMin,
+                yMin,
+                xMax,
+                yMax,
+                macStyle,
+                lowestRecPPEM,
+                fontDirectionHint,
+                indexToLocFormat,
+                glyphDataFormat);
+        }
+
+        private static DateTime SafeAddSeconds(DateTime epoch, long seconds)
+        {
+            // Handle invalid/corrupted timestamps gracefully
+            // Valid range for font timestamps is roughly 1904-01-01 to ~2040
+            // Negative values or extremely large values indicate corrupted data
+            
+            try
+            {
+                // Check if the resulting date would be valid before attempting addition
+                // DateTime.MinValue is 0001-01-01, DateTime.MaxValue is 9999-12-31
+                if (seconds < 0)
+                {
+                    // Calculate minimum allowed seconds from epoch to DateTime.MinValue
+                    var minSeconds = (long)(DateTime.MinValue - epoch).TotalSeconds;
+                    if (seconds < minSeconds)
+                    {
+                        return DateTime.MinValue;
+                    }
+                }
+                else
+                {
+                    // Calculate maximum allowed seconds from epoch to DateTime.MaxValue
+                    var maxSeconds = (long)(DateTime.MaxValue - epoch).TotalSeconds;
+                    if (seconds > maxSeconds)
+                    {
+                        return DateTime.MaxValue;
+                    }
+                }
+
+                return epoch.AddSeconds(seconds);
+            }
+            catch (ArgumentOutOfRangeException)
+            {
+                // Fallback for any edge cases that slip through
+                return seconds < 0 ? DateTime.MinValue : DateTime.MaxValue;
+            }
+        }
+    }
+
+    /// <summary>
+    /// Flags for the 'head' table.
+    /// </summary>
+    [Flags]
+    internal enum HeadFlags : ushort
+    {
+        /// <summary>
+        /// Bit 0: Baseline for font at y=0.
+        /// </summary>
+        BaselineAtY0 = 1 << 0,
+
+        /// <summary>
+        /// Bit 1: Left sidebearing point at x=0 (relevant only for TrueType rasterizers).
+        /// </summary>
+        LeftSidebearingAtX0 = 1 << 1,
+
+        /// <summary>
+        /// Bit 2: Instructions may depend on point size.
+        /// </summary>
+        InstructionsDependOnPointSize = 1 << 2,
+
+        /// <summary>
+        /// Bit 3: Force ppem to integer values for all internal scaler math; may use fractional ppem sizes if this bit is clear.
+        /// </summary>
+        ForcePpemToInteger = 1 << 3,
+
+        /// <summary>
+        /// Bit 4: Instructions may alter advance width (the advance widths might not scale linearly).
+        /// </summary>
+        InstructionsMayAlterAdvanceWidth = 1 << 4,
+
+        /// <summary>
+        /// Bit 5: This bit should be set in fonts that are intended to be laid out vertically, and in which the glyphs have been drawn such that an x-coordinate of 0 corresponds to the desired vertical baseline.
+        /// </summary>
+        VerticalBaseline = 1 << 5,
+
+        /// <summary>
+        /// Bit 7: Font data is 'lossless' as a result of having been subjected to optimizing transformation and/or compression.
+        /// </summary>
+        Lossless = 1 << 7,
+
+        /// <summary>
+        /// Bit 8: Font converted (produce compatible metrics).
+        /// </summary>
+        FontConverted = 1 << 8,
+
+        /// <summary>
+        /// Bit 9: Font optimized for ClearType. Note that this implies that instructions may alter advance widths (bit 4 should also be set).
+        /// </summary>
+        ClearTypeOptimized = 1 << 9,
+
+        /// <summary>
+        /// Bit 10: Last Resort font. If set, indicates that the glyphs encoded in the 'cmap' subtables are simply generic symbolic representations of code point ranges and don't truly represent support for those code points.
+        /// </summary>
+        LastResortFont = 1 << 10,
+    }
+
+    /// <summary>
+    /// Mac style flags for font styling (used by macOS).
+    /// </summary>
+    [Flags]
+    internal enum MacStyleFlags : ushort
+    {
+        /// <summary>
+        /// Bit 0: Bold (if set to 1).
+        /// </summary>
+        Bold = 1 << 0,
+
+        /// <summary>
+        /// Bit 1: Italic (if set to 1).
+        /// </summary>
+        Italic = 1 << 1,
+
+        /// <summary>
+        /// Bit 2: Underline (if set to 1).
+        /// </summary>
+        Underline = 1 << 2,
+
+        /// <summary>
+        /// Bit 3: Outline (if set to 1).
+        /// </summary>
+        Outline = 1 << 3,
+
+        /// <summary>
+        /// Bit 4: Shadow (if set to 1).
+        /// </summary>
+        Shadow = 1 << 4,
+
+        /// <summary>
+        /// Bit 5: Condensed (if set to 1).
+        /// </summary>
+        Condensed = 1 << 5,
+
+        /// <summary>
+        /// Bit 6: Extended (if set to 1).
+        /// </summary>
+        Extended = 1 << 6,
+    }
+
+    /// <summary>
+    /// Specifies the format used for the 'loca' table.
+    /// </summary>
+    internal enum IndexToLocFormat : short
+    {
+        /// <summary>
+        /// Short offsets (Offset16). The actual local offset divided by 2 is stored.
+        /// </summary>
+        Short = 0,
+
+        /// <summary>
+        /// Long offsets (Offset32). The actual local offset is stored.
+        /// </summary>
+        Long = 1
+    }
+
+    /// <summary>
+    /// Specifies the format of glyph data.
+    /// </summary>
+    internal enum GlyphDataFormat : short
+    {
+        /// <summary>
+        /// Current format (TrueType outlines).
+        /// </summary>
+        Current = 0
+    }
+
+    /// <summary>
+    /// Font direction hint for mixed directional text.
+    /// </summary>
+    internal enum FontDirectionHint : short
+    {
+        /// <summary>
+        /// Fully mixed directional glyphs.
+        /// </summary>
+        FullyMixed = 0,
+
+        /// <summary>
+        /// Only strongly left to right glyphs.
+        /// </summary>
+        OnlyLeftToRight = 1,
+
+        /// <summary>
+        /// Like 1 but also contains neutrals.
+        /// </summary>
+        LeftToRightWithNeutrals = 2,
+
+        /// <summary>
+        /// Only strongly right to left glyphs.
+        /// </summary>
+        OnlyRightToLeft = -1,
+
+        /// <summary>
+        /// Like -1 but also contains neutrals.
+        /// </summary>
+        RightToLeftWithNeutrals = -2
+    }
+}

+ 87 - 40
src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs → src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeaderTable.cs

@@ -2,16 +2,79 @@
 // Licensed under the Apache License, Version 2.0.
 // Licensed under the Apache License, Version 2.0.
 // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
 // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
 
 
-using System.IO;
-
 namespace Avalonia.Media.Fonts.Tables
 namespace Avalonia.Media.Fonts.Tables
 {
 {
-    internal class HorizontalHeadTable
+    internal readonly struct HorizontalHeaderTable
     {
     {
         internal const string TableName = "hhea";
         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);
+
+        /// <summary>
+        /// Gets the version of the horizontal header table.
+        /// </summary>
+        public FontVersion Version { get; }
+
+        /// <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 HorizontalHeaderTable(
+            FontVersion version,
             short ascender,
             short ascender,
             short descender,
             short descender,
             short lineGap,
             short lineGap,
@@ -24,6 +87,7 @@ namespace Avalonia.Media.Fonts.Tables
             short caretOffset,
             short caretOffset,
             ushort numberOfHMetrics)
             ushort numberOfHMetrics)
         {
         {
+            Version = version;
             Ascender = ascender;
             Ascender = ascender;
             Descender = descender;
             Descender = descender;
             LineGap = lineGap;
             LineGap = lineGap;
@@ -37,48 +101,28 @@ namespace Avalonia.Media.Fonts.Tables
             NumberOfHMetrics = numberOfHMetrics;
             NumberOfHMetrics = numberOfHMetrics;
         }
         }
 
 
-        public ushort AdvanceWidthMax { 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 MinLeftSideBearing { get; }
-
-        public short MinRightSideBearing { get; }
-
-        public ushort NumberOfHMetrics { get; }
-
-        public short XMaxExtent { get; }
-
-        public static HorizontalHeadTable? Load(IGlyphTypeface glyphTypeface)
+        public static bool TryLoad(GlyphTypeface fontFace, out HorizontalHeaderTable horizontalHeaderTable)
         {
         {
-            if (!glyphTypeface.TryGetTable(Tag, out var table))
+            horizontalHeaderTable = default;
+
+            if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table))
             {
             {
-                return null;
+                return false;
             }
             }
 
 
-            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);
+            return TryLoad(ref binaryReader, out horizontalHeaderTable);
         }
         }
 
 
-        public static HorizontalHeadTable Load(BigEndianBinaryReader reader)
+        private static bool TryLoad(ref BigEndianBinaryReader reader, out HorizontalHeaderTable horizontalHeaderTable)
         {
         {
+            horizontalHeaderTable = default;
+
             // +--------+---------------------+---------------------------------------------------------------------------------+
             // +--------+---------------------+---------------------------------------------------------------------------------+
             // | Type   | Name                | Description                                                                     |
             // | Type   | Name                | Description                                                                     |
             // +========+=====================+=================================================================================+
             // +========+=====================+=================================================================================+
-            // | Fixed  | version             | 0x00010000 (1.0)                                                                |
+            // | Version16Dot16 | version     | 0x00010000 (1.0)                                                                |
             // +--------+---------------------+---------------------------------------------------------------------------------+
             // +--------+---------------------+---------------------------------------------------------------------------------+
             // | FWord  | ascent              | Distance from baseline of highest ascender                                      |
             // | FWord  | ascent              | Distance from baseline of highest ascender                                      |
             // +--------+---------------------+---------------------------------------------------------------------------------+
             // +--------+---------------------+---------------------------------------------------------------------------------+
@@ -112,8 +156,7 @@ namespace Avalonia.Media.Fonts.Tables
             // +--------+---------------------+---------------------------------------------------------------------------------+
             // +--------+---------------------+---------------------------------------------------------------------------------+
             // | uint16 | numOfLongHorMetrics | number of advance widths in metrics table                                       |
             // | uint16 | numOfLongHorMetrics | number of advance widths in metrics table                                       |
             // +--------+---------------------+---------------------------------------------------------------------------------+
             // +--------+---------------------+---------------------------------------------------------------------------------+
-            ushort majorVersion = reader.ReadUInt16();
-            ushort minorVersion = reader.ReadUInt16();
+            FontVersion version = reader.ReadVersion16Dot16();
             short ascender = reader.ReadFWORD();
             short ascender = reader.ReadFWORD();
             short descender = reader.ReadFWORD();
             short descender = reader.ReadFWORD();
             short lineGap = reader.ReadFWORD();
             short lineGap = reader.ReadFWORD();
@@ -129,14 +172,16 @@ namespace Avalonia.Media.Fonts.Tables
             reader.ReadInt16(); // reserved
             reader.ReadInt16(); // reserved
             reader.ReadInt16(); // reserved
             reader.ReadInt16(); // reserved
             short metricDataFormat = reader.ReadInt16(); // 0
             short metricDataFormat = reader.ReadInt16(); // 0
+
             if (metricDataFormat != 0)
             if (metricDataFormat != 0)
             {
             {
-                throw new InvalidFontTableException($"Expected metricDataFormat = 0 found {metricDataFormat}", TableName);
+                return false;
             }
             }
 
 
             ushort numberOfHMetrics = reader.ReadUInt16();
             ushort numberOfHMetrics = reader.ReadUInt16();
 
 
-            return new HorizontalHeadTable(
+            horizontalHeaderTable = new HorizontalHeaderTable(
+                version,
                 ascender,
                 ascender,
                 descender,
                 descender,
                 lineGap,
                 lineGap,
@@ -148,6 +193,8 @@ namespace Avalonia.Media.Fonts.Tables
                 caretSlopeRun,
                 caretSlopeRun,
                 caretOffset,
                 caretOffset,
                 numberOfHMetrics);
                 numberOfHMetrics);
+
+            return true;
         }
         }
     }
     }
 }
 }

+ 138 - 0
src/Avalonia.Base/Media/Fonts/Tables/MaxpTable.cs

@@ -0,0 +1,138 @@
+using System;
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    internal readonly struct MaxpTable
+    {
+        internal const string TableName = "maxp";
+        internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName);
+
+        public FontVersion Version { get; }
+        public ushort NumGlyphs { get; }        
+        public ushort MaxPoints { get; }
+        public ushort MaxContours { get; }
+        public ushort MaxCompositePoints { get; }
+        public ushort MaxCompositeContours { get; }
+        public ushort MaxZones { get; }
+        public ushort MaxTwilightPoints { get; }
+        public ushort MaxStorage { get; }
+        public ushort MaxFunctionDefs { get; }
+        public ushort MaxInstructionDefs { get; }
+        public ushort MaxStackElements { get; }
+        public ushort MaxSizeOfInstructions { get; }
+        public ushort MaxComponentElements { get; }
+        public ushort MaxComponentDepth { get; }
+
+        private MaxpTable(
+            FontVersion version,
+            ushort numGlyphs,
+            ushort maxPoints,
+            ushort maxContours,
+            ushort maxCompositePoints,
+            ushort maxCompositeContours,
+            ushort maxZones,
+            ushort maxTwilightPoints,
+            ushort maxStorage,
+            ushort maxFunctionDefs,
+            ushort maxInstructionDefs,
+            ushort maxStackElements,
+            ushort maxSizeOfInstructions,
+            ushort maxComponentElements,
+            ushort maxComponentDepth)
+        {
+            Version = version;
+            NumGlyphs = numGlyphs;
+            MaxPoints = maxPoints;
+            MaxContours = maxContours;
+            MaxCompositePoints = maxCompositePoints;
+            MaxCompositeContours = maxCompositeContours;
+            MaxZones = maxZones;
+            MaxTwilightPoints = maxTwilightPoints;
+            MaxStorage = maxStorage;
+            MaxFunctionDefs = maxFunctionDefs;
+            MaxInstructionDefs = maxInstructionDefs;
+            MaxStackElements = maxStackElements;
+            MaxSizeOfInstructions = maxSizeOfInstructions;
+            MaxComponentElements = maxComponentElements;
+            MaxComponentDepth = maxComponentDepth;
+        }
+
+        public static MaxpTable Load(GlyphTypeface fontFace)
+        {
+            if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table))
+            {
+                throw new InvalidOperationException($"Could not load the '{TableName}' table.");
+            }
+
+            var binaryReader = new BigEndianBinaryReader(table.Span);
+
+            return Load(ref binaryReader);
+        }
+
+        private static MaxpTable Load(ref BigEndianBinaryReader reader)
+        {
+            // Version 0.5 (CFF/CFF2 fonts):
+            // | Version16Dot16 | version   | 0x00005000 for version 0.5      |
+            // | uint16         | numGlyphs | The number of glyphs in the font|
+            
+            // Version 1.0 (TrueType fonts):
+            // | Version16Dot16 | version                | 0x00010000 for version 1.0                          |
+            // | uint16         | numGlyphs              | The number of glyphs in the font                    |
+            // | uint16         | maxPoints              | Maximum points in a non-composite glyph             |
+            // | uint16         | maxContours            | Maximum contours in a non-composite glyph           |
+            // | uint16         | maxCompositePoints     | Maximum points in a composite glyph                 |
+            // | uint16         | maxCompositeContours   | Maximum contours in a composite glyph               |
+            // | uint16         | maxZones               | 1 or 2; should be set to 2 in most cases            |
+            // | uint16         | maxTwilightPoints      | Maximum points used in Z0                           |
+            // | uint16         | maxStorage             | Number of Storage Area locations                    |
+            // | uint16         | maxFunctionDefs        | Number of FDEFs                                     |
+            // | uint16         | maxInstructionDefs     | Number of IDEFs                                     |
+            // | uint16         | maxStackElements       | Maximum stack depth                                 |
+            // | uint16         | maxSizeOfInstructions  | Maximum byte count for glyph instructions           |
+            // | uint16         | maxComponentElements   | Maximum number of components at top level           |
+            // | uint16         | maxComponentDepth      | Maximum levels of recursion                         |
+
+            FontVersion version = reader.ReadVersion16Dot16();
+            ushort numGlyphs = reader.ReadUInt16();
+
+            if (version.Major < 1)
+            {
+                return new MaxpTable(
+                    version,
+                    numGlyphs,
+                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
+            }
+
+            ushort maxPoints = reader.ReadUInt16();
+            ushort maxContours = reader.ReadUInt16();
+            ushort maxCompositePoints = reader.ReadUInt16();
+            ushort maxCompositeContours = reader.ReadUInt16();
+            ushort maxZones = reader.ReadUInt16();
+            ushort maxTwilightPoints = reader.ReadUInt16();
+            ushort maxStorage = reader.ReadUInt16();
+            ushort maxFunctionDefs = reader.ReadUInt16();
+            ushort maxInstructionDefs = reader.ReadUInt16();
+            ushort maxStackElements = reader.ReadUInt16();
+            ushort maxSizeOfInstructions = reader.ReadUInt16();
+            ushort maxComponentElements = reader.ReadUInt16();
+            ushort maxComponentDepth = reader.ReadUInt16();
+
+            return new MaxpTable(
+                version,
+                numGlyphs,
+                maxPoints,
+                maxContours,
+                maxCompositePoints,
+                maxCompositeContours,
+                maxZones,
+                maxTwilightPoints,
+                maxStorage,
+                maxFunctionDefs,
+                maxInstructionDefs,
+                maxStackElements,
+                maxSizeOfInstructions,
+                maxComponentElements,
+                maxComponentDepth);
+        }
+    }
+}

+ 26 - 0
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}";
+    }
+}

+ 227 - 0
src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalMetricsTable.cs

@@ -0,0 +1,227 @@
+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 int _numGlyphs;
+
+        private HorizontalMetricsTable(ReadOnlyMemory<byte> data, ushort numOfHMetrics, int numGlyphs)
+        {
+            _data = data;
+            _numOfHMetrics = numOfHMetrics;
+            _numGlyphs = numGlyphs;
+        }
+
+        internal static HorizontalMetricsTable? Load(GlyphTypeface glyphTypeface, ushort numberOfHMetrics, int glyphCount)
+        {
+            if (glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table))
+            {
+                return new HorizontalMetricsTable(table, numberOfHMetrics, glyphCount);
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Attempts to retrieve the horizontal glyph metrics for the specified glyph index.
+        /// </summary>
+        /// <param name="glyphIndex">The index of the glyph for which to retrieve metrics.</param>
+        /// <param name="metric">When this method returns, contains the horizontal glyph metric if the glyph index is valid; otherwise, the default value.</param>
+        /// <returns><c>true</c> if the glyph index is valid and metrics were retrieved; otherwise, <c>false</c>.</returns>
+        public bool TryGetMetrics(ushort glyphIndex, out HorizontalGlyphMetric metric)
+        {
+            metric = default;
+
+            if (glyphIndex >= _numGlyphs)
+            {
+                return false;
+            }
+
+            var reader = new BigEndianBinaryReader(_data.Span);
+
+            if (glyphIndex < _numOfHMetrics)
+            {
+                reader.Seek(glyphIndex * 4);
+
+                ushort advanceWidth = reader.ReadUInt16();
+                short leftSideBearing = reader.ReadInt16();
+
+                metric = new HorizontalGlyphMetric(advanceWidth, leftSideBearing);
+            }
+            else
+            {
+                reader.Seek((_numOfHMetrics - 1) * 4);
+
+                ushort lastAdvanceWidth = reader.ReadUInt16();
+
+                int lsbIndex = glyphIndex - _numOfHMetrics;
+                int lsbOffset = _numOfHMetrics * 4 + lsbIndex * 2;
+
+                reader.Seek(lsbOffset);
+
+                short leftSideBearing = reader.ReadInt16();
+
+                metric = new HorizontalGlyphMetric(lastAdvanceWidth, leftSideBearing);
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Attempts to retrieve the advance width for a single glyph.
+        /// </summary>
+        /// <param name="glyphIndex">Glyph index to query.</param>
+        /// <param name="advance">When this method returns, contains the advance width if the glyph index is valid; otherwise, zero.</param>
+        /// <returns><c>true</c> if the glyph index is valid and the advance was retrieved; otherwise, <c>false</c>.</returns>
+        public bool TryGetAdvance(ushort glyphIndex, out ushort advance)
+        {
+            advance = 0;
+
+            if (glyphIndex >= _numGlyphs)
+            {
+                return false;
+            }
+
+            var reader = new BigEndianBinaryReader(_data.Span);
+
+            if (glyphIndex < _numOfHMetrics)
+            {
+                reader.Seek(glyphIndex * 4);
+
+                advance = reader.ReadUInt16();
+            }
+            else
+            {
+                reader.Seek((_numOfHMetrics - 1) * 4);
+
+                advance = reader.ReadUInt16();
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Attempts to retrieve advance widths for multiple glyphs in a single operation.
+        /// </summary>
+        /// <param name="glyphIndices">Read-only span of glyph indices to query.</param>
+        /// <param name="advances">Output span to write the advance widths. Must be at least as long as <paramref name="glyphIndices"/>.</param>
+        /// <returns><c>true</c> if all glyph indices are valid and advances were retrieved; otherwise, <c>false</c>.</returns>
+        /// <remarks>
+        /// This method is more efficient than calling <see cref="TryGetAdvance"/> multiple times as it reuses
+        /// the same reader and span reference. If any glyph index is invalid, the method returns <c>false</c>
+        /// and the contents of <paramref name="advances"/> are undefined.
+        /// </remarks>
+        public bool TryGetAdvances(ReadOnlySpan<ushort> glyphIndices, Span<ushort> advances)
+        {
+            if (advances.Length < glyphIndices.Length)
+            {
+                return false;
+            }
+
+            var data = _data.Span;
+            var reader = new BigEndianBinaryReader(data);
+
+            // Cache the last advance width for glyphs beyond numOfHMetrics
+            ushort? lastAdvanceWidth = null;
+
+            for (int i = 0; i < glyphIndices.Length; i++)
+            {
+                ushort glyphIndex = glyphIndices[i];
+
+                if (glyphIndex >= _numGlyphs)
+                {
+                    return false;
+                }
+
+                if (glyphIndex < _numOfHMetrics)
+                {
+                    reader.Seek(glyphIndex * 4);
+                    advances[i] = reader.ReadUInt16();
+                }
+                else
+                {
+                    // All glyphs beyond numOfHMetrics share the same advance width
+                    if (!lastAdvanceWidth.HasValue)
+                    {
+                        reader.Seek((_numOfHMetrics - 1) * 4);
+                        lastAdvanceWidth = reader.ReadUInt16();
+                    }
+
+                    advances[i] = lastAdvanceWidth.Value;
+                }
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Attempts to retrieve horizontal glyph metrics for multiple glyphs in a single operation.
+        /// </summary>
+        /// <param name="glyphIndices">Read-only span of glyph indices to query.</param>
+        /// <param name="metrics">Output span to write the metrics. Must be at least as long as <paramref name="glyphIndices"/>.</param>
+        /// <returns><c>true</c> if all glyph indices are valid and metrics were retrieved; otherwise, <c>false</c>.</returns>
+        /// <remarks>
+        /// This method is more efficient than calling <see cref="TryGetMetrics(ushort, out HorizontalGlyphMetric)"/> multiple times as it reuses
+        /// the same reader and span reference. If any glyph index is invalid, the method returns <c>false</c>
+        /// and the contents of <paramref name="metrics"/> are undefined.
+        /// </remarks>
+        public bool TryGetMetrics(ReadOnlySpan<ushort> glyphIndices, Span<HorizontalGlyphMetric> metrics)
+        {
+            if (metrics.Length < glyphIndices.Length)
+            {
+                return false;
+            }
+
+            var data = _data.Span;
+            var reader = new BigEndianBinaryReader(data);
+
+            // Cache the last advance width for glyphs beyond numOfHMetrics
+            ushort? lastAdvanceWidth = null;
+
+            for (int i = 0; i < glyphIndices.Length; i++)
+            {
+                ushort glyphIndex = glyphIndices[i];
+
+                if (glyphIndex >= _numGlyphs)
+                {
+                    return false;
+                }
+
+                if (glyphIndex < _numOfHMetrics)
+                {
+                    reader.Seek(glyphIndex * 4);
+
+                    ushort advanceWidth = reader.ReadUInt16();
+                    short leftSideBearing = reader.ReadInt16();
+
+                    metrics[i] = new HorizontalGlyphMetric(advanceWidth, leftSideBearing);
+                }
+                else
+                {
+                    // All glyphs beyond numOfHMetrics share the same advance width
+                    if (!lastAdvanceWidth.HasValue)
+                    {
+                        reader.Seek((_numOfHMetrics - 1) * 4);
+                        lastAdvanceWidth = reader.ReadUInt16();
+                    }
+
+                    int lsbIndex = glyphIndex - _numOfHMetrics;
+                    int lsbOffset = _numOfHMetrics * 4 + lsbIndex * 2;
+
+                    reader.Seek(lsbOffset);
+                    short leftSideBearing = reader.ReadInt16();
+
+                    metrics[i] = new HorizontalGlyphMetric(lastAdvanceWidth.Value, leftSideBearing);
+                }
+            }
+
+            return true;
+        }
+    }
+}

+ 24 - 0
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; }
+    }
+}

+ 227 - 0
src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalMetricsTable.cs

@@ -0,0 +1,227 @@
+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 int _numGlyphs;
+
+        private VerticalMetricsTable(ReadOnlyMemory<byte> data, ushort numOfVMetrics, int numGlyphs)
+        {
+            _data = data;
+            _numOfVMetrics = numOfVMetrics;
+            _numGlyphs = numGlyphs;
+        }
+
+        public static VerticalMetricsTable? Load(GlyphTypeface glyphTypeface, ushort numberOfVMetrics, int glyphCount)
+        {
+            if (glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table))
+            {
+                return new VerticalMetricsTable(table, numberOfVMetrics, glyphCount);
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Attempts to retrieve the vertical glyph metrics for the specified glyph index.
+        /// </summary>
+        /// <param name="glyphIndex">The index of the glyph for which to retrieve metrics.</param>
+        /// <param name="metric">When this method returns, contains the vertical glyph metric if the glyph index is valid; otherwise, the default value.</param>
+        /// <returns><c>true</c> if the glyph index is valid and metrics were retrieved; otherwise, <c>false</c>.</returns>
+        public bool TryGetMetrics(ushort glyphIndex, out VerticalGlyphMetric metric)
+        {
+            metric = default;
+
+            if (glyphIndex >= _numGlyphs)
+            {
+                return false;
+            }
+
+            var reader = new BigEndianBinaryReader(_data.Span);
+
+            if (glyphIndex < _numOfVMetrics)
+            {
+                reader.Seek(glyphIndex * 4);
+
+                ushort advanceHeight = reader.ReadUInt16();
+                short topSideBearing = reader.ReadInt16();
+
+                metric = new VerticalGlyphMetric(advanceHeight, topSideBearing);
+            }
+            else
+            {
+                reader.Seek((_numOfVMetrics - 1) * 4);
+
+                ushort lastAdvanceHeight = reader.ReadUInt16();
+
+                int tsbIndex = glyphIndex - _numOfVMetrics;
+                int tsbOffset = _numOfVMetrics * 4 + tsbIndex * 2;
+
+                reader.Seek(tsbOffset);
+
+                short tsb = reader.ReadInt16();
+
+                metric = new VerticalGlyphMetric(lastAdvanceHeight, tsb);
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Attempts to retrieve the advance height for a single glyph.
+        /// </summary>
+        /// <param name="glyphIndex">Glyph index to query.</param>
+        /// <param name="advance">When this method returns, contains the advance height if the glyph index is valid; otherwise, zero.</param>
+        /// <returns><c>true</c> if the glyph index is valid and the advance was retrieved; otherwise, <c>false</c>.</returns>
+        public bool TryGetAdvance(ushort glyphIndex, out ushort advance)
+        {
+            advance = 0;
+
+            if (glyphIndex >= _numGlyphs)
+            {
+                return false;
+            }
+
+            var reader = new BigEndianBinaryReader(_data.Span);
+
+            if (glyphIndex < _numOfVMetrics)
+            {
+                reader.Seek(glyphIndex * 4);
+
+                advance = reader.ReadUInt16();
+            }
+            else
+            {
+                reader.Seek((_numOfVMetrics - 1) * 4);
+
+                advance = reader.ReadUInt16();
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Attempts to retrieve advance heights for multiple glyphs in a single operation.
+        /// </summary>
+        /// <param name="glyphIndices">Read-only span of glyph indices to query.</param>
+        /// <param name="advances">Output span to write the advance heights. Must be at least as long as <paramref name="glyphIndices"/>.</param>
+        /// <returns><c>true</c> if all glyph indices are valid and advances were retrieved; otherwise, <c>false</c>.</returns>
+        /// <remarks>
+        /// This method is more efficient than calling <see cref="TryGetAdvance"/> multiple times as it reuses
+        /// the same reader and span reference. If any glyph index is invalid, the method returns <c>false</c>
+        /// and the contents of <paramref name="advances"/> are undefined.
+        /// </remarks>
+        public bool TryGetAdvances(ReadOnlySpan<ushort> glyphIndices, Span<ushort> advances)
+        {
+            if (advances.Length < glyphIndices.Length)
+            {
+                return false;
+            }
+
+            var data = _data.Span;
+            var reader = new BigEndianBinaryReader(data);
+
+            // Cache the last advance height for glyphs beyond numOfVMetrics
+            ushort? lastAdvanceHeight = null;
+
+            for (int i = 0; i < glyphIndices.Length; i++)
+            {
+                ushort glyphIndex = glyphIndices[i];
+
+                if (glyphIndex >= _numGlyphs)
+                {
+                    return false;
+                }
+
+                if (glyphIndex < _numOfVMetrics)
+                {
+                    reader.Seek(glyphIndex * 4);
+                    advances[i] = reader.ReadUInt16();
+                }
+                else
+                {
+                    // All glyphs beyond numOfVMetrics share the same advance height
+                    if (!lastAdvanceHeight.HasValue)
+                    {
+                        reader.Seek((_numOfVMetrics - 1) * 4);
+                        lastAdvanceHeight = reader.ReadUInt16();
+                    }
+
+                    advances[i] = lastAdvanceHeight.Value;
+                }
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Attempts to retrieve vertical glyph metrics for multiple glyphs in a single operation.
+        /// </summary>
+        /// <param name="glyphIndices">Read-only span of glyph indices to query.</param>
+        /// <param name="metrics">Output span to write the metrics. Must be at least as long as <paramref name="glyphIndices"/>.</param>
+        /// <returns><c>true</c> if all glyph indices are valid and metrics were retrieved; otherwise, <c>false</c>.</returns>
+        /// <remarks>
+        /// This method is more efficient than calling <see cref="TryGetMetrics(ushort, out VerticalGlyphMetric)"/> multiple times as it reuses
+        /// the same reader and span reference. If any glyph index is invalid, the method returns <c>false</c>
+        /// and the contents of <paramref name="metrics"/> are undefined.
+        /// </remarks>
+        public bool TryGetMetrics(ReadOnlySpan<ushort> glyphIndices, Span<VerticalGlyphMetric> metrics)
+        {
+            if (metrics.Length < glyphIndices.Length)
+            {
+                return false;
+            }
+
+            var data = _data.Span;
+            var reader = new BigEndianBinaryReader(data);
+
+            // Cache the last advance height for glyphs beyond numOfVMetrics
+            ushort? lastAdvanceHeight = null;
+
+            for (int i = 0; i < glyphIndices.Length; i++)
+            {
+                ushort glyphIndex = glyphIndices[i];
+
+                if (glyphIndex >= _numGlyphs)
+                {
+                    return false;
+                }
+
+                if (glyphIndex < _numOfVMetrics)
+                {
+                    reader.Seek(glyphIndex * 4);
+
+                    ushort advanceHeight = reader.ReadUInt16();
+                    short topSideBearing = reader.ReadInt16();
+
+                    metrics[i] = new VerticalGlyphMetric(advanceHeight, topSideBearing);
+                }
+                else
+                {
+                    // All glyphs beyond numOfVMetrics share the same advance height
+                    if (!lastAdvanceHeight.HasValue)
+                    {
+                        reader.Seek((_numOfVMetrics - 1) * 4);
+                        lastAdvanceHeight = reader.ReadUInt16();
+                    }
+
+                    int tsbIndex = glyphIndex - _numOfVMetrics;
+                    int tsbOffset = _numOfVMetrics * 4 + tsbIndex * 2;
+
+                    reader.Seek(tsbOffset);
+                    short topSideBearing = reader.ReadInt16();
+
+                    metrics[i] = new VerticalGlyphMetric(lastAdvanceHeight.Value, topSideBearing);
+                }
+            }
+
+            return true;
+        }
+    }
+}

+ 30 - 19
src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs

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

+ 56 - 46
src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs

@@ -4,7 +4,6 @@
 
 
 using System.Collections;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.IO;
 using Avalonia.Utilities;
 using Avalonia.Utilities;
 
 
 namespace Avalonia.Media.Fonts.Tables.Name
 namespace Avalonia.Media.Fonts.Tables.Name
@@ -14,7 +13,11 @@ namespace Avalonia.Media.Fonts.Tables.Name
         internal const string TableName = "name";
         internal const string TableName = "name";
         internal static readonly OpenTypeTag Tag = OpenTypeTag.Parse(TableName);
         internal static readonly OpenTypeTag Tag = OpenTypeTag.Parse(TableName);
 
 
+        private const ushort USEnglishLanguageId = 0x0409;
+
         private readonly NameRecord[] _names;
         private readonly NameRecord[] _names;
+        private string? _cachedFamilyName;
+        private string? _cachedTypographicFamilyName;
 
 
         internal NameTable(NameRecord[] names)
         internal NameTable(NameRecord[] names)
         {
         {
@@ -46,7 +49,21 @@ namespace Avalonia.Media.Fonts.Tables.Name
         /// The name of the font.
         /// The name of the font.
         /// </value>
         /// </value>
         public string FontFamilyName(ushort culture)
         public string FontFamilyName(ushort culture)
-            => GetNameById(culture, KnownNameIds.FontFamilyName);
+        {
+            if (culture == USEnglishLanguageId && _cachedFamilyName is not null)
+            {
+                return _cachedFamilyName;
+            }
+
+            var value = GetNameById(culture, KnownNameIds.FontFamilyName);
+
+            if (culture == USEnglishLanguageId)
+            {
+                _cachedFamilyName = value;
+            }
+
+            return value;
+        }
 
 
         /// <summary>
         /// <summary>
         /// Gets the name of the font.
         /// Gets the name of the font.
@@ -59,86 +76,79 @@ namespace Avalonia.Media.Fonts.Tables.Name
 
 
         public string GetNameById(ushort culture, KnownNameIds nameId)
         public string GetNameById(ushort culture, KnownNameIds nameId)
         {
         {
+            if (nameId == KnownNameIds.TypographicFamilyName && culture == USEnglishLanguageId && _cachedTypographicFamilyName is not null)
+            {
+                return _cachedTypographicFamilyName;
+            }
+
             var languageId = culture;
             var languageId = culture;
             NameRecord? usaVersion = null;
             NameRecord? usaVersion = null;
             NameRecord? firstWindows = null;
             NameRecord? firstWindows = null;
             NameRecord? first = null;
             NameRecord? first = null;
+
             foreach (var name in _names)
             foreach (var name in _names)
             {
             {
                 if (name.NameID == nameId)
                 if (name.NameID == nameId)
                 {
                 {
-                    // Get just the first one, just in case.
                     first ??= name;
                     first ??= name;
-                    if (name.Platform == PlatformIDs.Windows)
+                    if (name.Platform == PlatformID.Windows)
                     {
                     {
-                        // If us not found return the first windows one.
                         firstWindows ??= name;
                         firstWindows ??= name;
-                        if (name.LanguageID == 0x0409)
+                        if (name.LanguageID == USEnglishLanguageId)
                         {
                         {
-                            // Grab the us version as its on next best match.
                             usaVersion ??= name;
                             usaVersion ??= name;
                         }
                         }
 
 
                         if (name.LanguageID == languageId)
                         if (name.LanguageID == languageId)
                         {
                         {
-                            // Return the most exact first.
-                            return name.Value;
+                            return name.GetValue();
                         }
                         }
                     }
                     }
                 }
                 }
             }
             }
 
 
-            return usaVersion?.Value ??
-                   firstWindows?.Value ??
-                   first?.Value ??
-                   string.Empty;
+            var value = usaVersion?.GetValue() ??
+                       firstWindows?.GetValue() ??
+                       first?.GetValue() ??
+                       string.Empty;
+
+            if (nameId == KnownNameIds.TypographicFamilyName && culture == USEnglishLanguageId)
+            {
+                _cachedTypographicFamilyName = value;
+            }
+
+            return value;
         }
         }
 
 
         public string GetNameById(ushort culture, ushort nameId)
         public string GetNameById(ushort culture, ushort nameId)
             => GetNameById(culture, (KnownNameIds)nameId);
             => GetNameById(culture, (KnownNameIds)nameId);
 
 
-        public static NameTable? Load(IGlyphTypeface glyphTypeface)
+        public static NameTable? Load(GlyphTypeface glyphTypeface)
         {
         {
-            if (!glyphTypeface.TryGetTable(Tag, out var table))
+            if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table))
             {
             {
                 return null;
                 return null;
             }
             }
 
 
-            using var stream = new MemoryStream(table);
-            using var binaryReader = new BigEndianBinaryReader(stream, false);
-
-            // Move to start of table.
-            return Load(binaryReader);
-        }
+            var reader = new BigEndianBinaryReader(table.Span);
 
 
-        public static NameTable Load(BigEndianBinaryReader reader)
-        {
-            var strings = new List<StringLoader>();
-            var format = reader.ReadUInt16();
-            var nameCount = reader.ReadUInt16();
-            var stringOffset = reader.ReadUInt16();
+            reader.ReadUInt16();
+            var count = reader.ReadUInt16();
+            var storageOffset = reader.ReadUInt16();
 
 
-            var names = new NameRecord[nameCount];
+            var names = new NameRecord[count];
 
 
-            for (var i = 0; i < nameCount; i++)
+            for (var i = 0; i < count; i++)
             {
             {
-                names[i] = NameRecord.Read(reader);
-
-                var sr = names[i].StringReader;
-
-                if (sr is not null)
-                {
-                    strings.Add(sr);
-                }
-            }
-
-            foreach (var readable in strings)
-            {
-                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);
             return new NameTable(names);

+ 199 - 345
src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs

@@ -3,338 +3,199 @@
 // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
 // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
 
 
 using System;
 using System;
-using System.IO;
 
 
 namespace Avalonia.Media.Fonts.Tables
 namespace Avalonia.Media.Fonts.Tables
 {
 {
-    internal sealed class OS2Table
+    internal readonly struct OS2Table
     {
     {
         internal const string TableName = "OS/2";
         internal const string TableName = "OS/2";
-        internal static OpenTypeTag Tag = OpenTypeTag.Parse(TableName);
-  
-        private readonly byte[] panose;
-        private readonly short capHeight;
-        private readonly short familyClass;
-        private readonly short heightX;
-        private readonly string tag;
-        private readonly ushort codePageRange1;
-        private readonly ushort codePageRange2;
-        private readonly uint unicodeRange1;
-        private readonly uint unicodeRange2;
-        private readonly uint unicodeRange3;
-        private readonly uint unicodeRange4;
-        private readonly ushort breakChar;
-        private readonly ushort defaultChar;
-        private readonly ushort firstCharIndex;
-        private readonly ushort lastCharIndex;
-        private readonly ushort lowerOpticalPointSize;
-        private readonly ushort maxContext;
-        private readonly ushort upperOpticalPointSize;
-        private readonly short averageCharWidth;
+        internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName);
 
 
-        public OS2Table(
-            short averageCharWidth,
+        [Flags]
+        internal enum FontSelectionFlags : ushort
+        {
+            ITALIC = 1,
+            UNDERSCORE = 1 << 1,
+            NEGATIVE = 1 << 2,
+            OUTLINED = 1 << 3,
+            STRIKEOUT = 1 << 4,
+            BOLD = 1 << 5,
+            REGULAR = 1 << 6,
+            USE_TYPO_METRICS = 1 << 7,
+            WWS = 1 << 8,
+            OBLIQUE = 1 << 9,
+        }
+
+        public ushort Version { get; }
+        public short XAvgCharWidth { get; }
+        public ushort WeightClass { get; }
+        public ushort WidthClass { get; }
+        public ushort FsType { get; }
+        public short YSubscriptXSize { get; }
+        public short YSubscriptYSize { get; }
+        public short YSubscriptXOffset { get; }
+        public short YSubscriptYOffset { get; }
+        public short YSuperscriptXSize { get; }
+        public short YSuperscriptYSize { get; }
+        public short YSuperscriptXOffset { get; }
+        public short YSuperscriptYOffset { get; }
+        public short StrikeoutSize { get; }
+        public short StrikeoutPosition { get; }
+        public short FamilyClass { get; }
+        public Panose Panose { get; }
+        public uint UnicodeRange1 { get; }
+        public uint UnicodeRange2 { get; }
+        public uint UnicodeRange3 { get; }
+        public uint UnicodeRange4 { get; }
+        public uint VendorId { get; }
+        public FontSelectionFlags Selection { get; }
+        public ushort FirstCharIndex { get; }
+        public ushort LastCharIndex { get; }
+        public short TypoAscender { get; }
+        public short TypoDescender { get; }
+        public short TypoLineGap { get; }
+        public ushort WinAscent { get; }
+        public ushort WinDescent { get; }
+        
+        public uint CodePageRange1 { get; }
+        public uint CodePageRange2 { get; }
+        
+        public short XHeight { get; }
+        public short CapHeight { get; }
+        public ushort DefaultChar { get; }
+        public ushort BreakChar { get; }
+        public ushort MaxContext { get; }
+        
+        public ushort LowerOpticalPointSize { get; }
+        public ushort UpperOpticalPointSize { get; }
+
+        private OS2Table(
+            ushort version,
+            short xAvgCharWidth,
             ushort weightClass,
             ushort weightClass,
             ushort widthClass,
             ushort widthClass,
-            ushort styleType,
-            short subscriptXSize,
-            short subscriptYSize,
-            short subscriptXOffset,
-            short subscriptYOffset,
-            short superscriptXSize,
-            short superscriptYSize,
-            short superscriptXOffset,
-            short superscriptYOffset,
+            ushort fsType,
+            short ySubscriptXSize,
+            short ySubscriptYSize,
+            short ySubscriptXOffset,
+            short ySubscriptYOffset,
+            short ySuperscriptXSize,
+            short ySuperscriptYSize,
+            short ySuperscriptXOffset,
+            short ySuperscriptYOffset,
             short strikeoutSize,
             short strikeoutSize,
             short strikeoutPosition,
             short strikeoutPosition,
             short familyClass,
             short familyClass,
-            byte[] panose,
+            Panose panose,
             uint unicodeRange1,
             uint unicodeRange1,
             uint unicodeRange2,
             uint unicodeRange2,
             uint unicodeRange3,
             uint unicodeRange3,
             uint unicodeRange4,
             uint unicodeRange4,
-            string tag,
-            FontStyleSelection fontStyle,
+            uint vendorId,
+            FontSelectionFlags selection,
             ushort firstCharIndex,
             ushort firstCharIndex,
             ushort lastCharIndex,
             ushort lastCharIndex,
             short typoAscender,
             short typoAscender,
             short typoDescender,
             short typoDescender,
             short typoLineGap,
             short typoLineGap,
             ushort winAscent,
             ushort winAscent,
-            ushort winDescent)
+            ushort winDescent,
+            uint codePageRange1,
+            uint codePageRange2,
+            short xHeight,
+            short capHeight,
+            ushort defaultChar,
+            ushort breakChar,
+            ushort maxContext,
+            ushort lowerOpticalPointSize,
+            ushort upperOpticalPointSize)
         {
         {
-            this.averageCharWidth = averageCharWidth;
+            Version = version;
+            XAvgCharWidth = xAvgCharWidth;
             WeightClass = weightClass;
             WeightClass = weightClass;
             WidthClass = widthClass;
             WidthClass = widthClass;
-            StyleType = styleType;
-            SubscriptXSize = subscriptXSize;
-            SubscriptYSize = subscriptYSize;
-            SubscriptXOffset = subscriptXOffset;
-            SubscriptYOffset = subscriptYOffset;
-            SuperscriptXSize = superscriptXSize;
-            SuperscriptYSize = superscriptYSize;
-            SuperscriptXOffset = superscriptXOffset;
-            SuperscriptYOffset = superscriptYOffset;
+            FsType = fsType;
+            YSubscriptXSize = ySubscriptXSize;
+            YSubscriptYSize = ySubscriptYSize;
+            YSubscriptXOffset = ySubscriptXOffset;
+            YSubscriptYOffset = ySubscriptYOffset;
+            YSuperscriptXSize = ySuperscriptXSize;
+            YSuperscriptYSize = ySuperscriptYSize;
+            YSuperscriptXOffset = ySuperscriptXOffset;
+            YSuperscriptYOffset = ySuperscriptYOffset;
             StrikeoutSize = strikeoutSize;
             StrikeoutSize = strikeoutSize;
             StrikeoutPosition = strikeoutPosition;
             StrikeoutPosition = strikeoutPosition;
-            this.familyClass = familyClass;
-            this.panose = panose;
-            this.unicodeRange1 = unicodeRange1;
-            this.unicodeRange2 = unicodeRange2;
-            this.unicodeRange3 = unicodeRange3;
-            this.unicodeRange4 = unicodeRange4;
-            this.tag = tag;
-            FontStyle = fontStyle;
-            this.firstCharIndex = firstCharIndex;
-            this.lastCharIndex = lastCharIndex;
+            FamilyClass = familyClass;
+            Panose = panose;
+            UnicodeRange1 = unicodeRange1;
+            UnicodeRange2 = unicodeRange2;
+            UnicodeRange3 = unicodeRange3;
+            UnicodeRange4 = unicodeRange4;
+            VendorId = vendorId;
+            Selection = selection;
+            FirstCharIndex = firstCharIndex;
+            LastCharIndex = lastCharIndex;
             TypoAscender = typoAscender;
             TypoAscender = typoAscender;
             TypoDescender = typoDescender;
             TypoDescender = typoDescender;
             TypoLineGap = typoLineGap;
             TypoLineGap = typoLineGap;
             WinAscent = winAscent;
             WinAscent = winAscent;
             WinDescent = winDescent;
             WinDescent = winDescent;
+            CodePageRange1 = codePageRange1;
+            CodePageRange2 = codePageRange2;
+            XHeight = xHeight;
+            CapHeight = capHeight;
+            DefaultChar = defaultChar;
+            BreakChar = breakChar;
+            MaxContext = maxContext;
+            LowerOpticalPointSize = lowerOpticalPointSize;
+            UpperOpticalPointSize = upperOpticalPointSize;
         }
         }
 
 
-        public OS2Table(
-            OS2Table version0Table,
-            ushort codePageRange1,
-            ushort codePageRange2,
-            short heightX,
-            short capHeight,
-            ushort defaultChar,
-            ushort breakChar,
-            ushort maxContext)
-            : this(
-                version0Table.averageCharWidth,
-                version0Table.WeightClass,
-                version0Table.WidthClass,
-                version0Table.StyleType,
-                version0Table.SubscriptXSize,
-                version0Table.SubscriptYSize,
-                version0Table.SubscriptXOffset,
-                version0Table.SubscriptYOffset,
-                version0Table.SuperscriptXSize,
-                version0Table.SuperscriptYSize,
-                version0Table.SuperscriptXOffset,
-                version0Table.SuperscriptYOffset,
-                version0Table.StrikeoutSize,
-                version0Table.StrikeoutPosition,
-                version0Table.familyClass,
-                version0Table.panose,
-                version0Table.unicodeRange1,
-                version0Table.unicodeRange2,
-                version0Table.unicodeRange3,
-                version0Table.unicodeRange4,
-                version0Table.tag,
-                version0Table.FontStyle,
-                version0Table.firstCharIndex,
-                version0Table.lastCharIndex,
-                version0Table.TypoAscender,
-                version0Table.TypoDescender,
-                version0Table.TypoLineGap,
-                version0Table.WinAscent,
-                version0Table.WinDescent)
+        public static bool TryLoad(GlyphTypeface fontFace, out OS2Table os2Table)
         {
         {
-            this.codePageRange1 = codePageRange1;
-            this.codePageRange2 = codePageRange2;
-            this.heightX = heightX;
-            this.capHeight = capHeight;
-            this.defaultChar = defaultChar;
-            this.breakChar = breakChar;
-            this.maxContext = maxContext;
-        }
-
-        public OS2Table(OS2Table versionLessThan5Table, ushort lowerOpticalPointSize, ushort upperOpticalPointSize)
-            : this(
-                versionLessThan5Table,
-                versionLessThan5Table.codePageRange1,
-                versionLessThan5Table.codePageRange2,
-                versionLessThan5Table.heightX,
-                versionLessThan5Table.capHeight,
-                versionLessThan5Table.defaultChar,
-                versionLessThan5Table.breakChar,
-                versionLessThan5Table.maxContext)
-        {
-            this.lowerOpticalPointSize = lowerOpticalPointSize;
-            this.upperOpticalPointSize = upperOpticalPointSize;
-        }
-
-        [Flags]
-        internal enum FontStyleSelection : ushort
-        {
-            /// <summary>
-            /// Font contains italic or oblique characters, otherwise they are upright.
-            /// </summary>
-            ITALIC = 1,
-
-            /// <summary>
-            /// Characters are underscored.
-            /// </summary>
-            UNDERSCORE = 1 << 1,
-
-            /// <summary>
-            /// Characters have their foreground and background reversed.
-            /// </summary>
-            NEGATIVE = 1 << 2,
-
-            /// <summary>
-            /// characters, otherwise they are solid.
-            /// </summary>
-            OUTLINED = 1 << 3,
-
-            /// <summary>
-            /// Characters are overstruck.
-            /// </summary>
-            STRIKEOUT = 1 << 4,
-
-            /// <summary>
-            /// Characters are emboldened.
-            /// </summary>
-            BOLD = 1 << 5,
-
-            /// <summary>
-            /// Characters are in the standard weight/style for the font.
-            /// </summary>
-            REGULAR = 1 << 6,
-
-            /// <summary>
-            /// If set, it is strongly recommended to use OS/2.typoAscender - OS/2.typoDescender+ OS/2.typoLineGap as a value for default line spacing for this font.
-            /// </summary>
-            USE_TYPO_METRICS = 1 << 7,
-
-            /// <summary>
-            /// The font has ‘name’ table strings consistent with a weight/width/slope family without requiring use of ‘name’ IDs 21 and 22. (Please see more detailed description below.)
-            /// </summary>
-            WWS = 1 << 8,
-
-            /// <summary>
-            /// Font contains oblique characters.
-            /// </summary>
-            OBLIQUE = 1 << 9,
-
-            // 10–15        <reserved>  Reserved; set to 0.
-        }
-
-        public FontStyleSelection FontStyle { get; }
-
-        public short TypoAscender { get; }
-
-        public short TypoDescender { get; }
-
-        public short TypoLineGap { get; }
-
-        public ushort WinAscent { get; }
-
-        public ushort WinDescent { get; }
-
-        public short StrikeoutPosition { get; }
-
-        public short StrikeoutSize { get; }
-
-        public short SubscriptXOffset { get; }
-
-        public short SubscriptXSize { get; }
-
-        public short SubscriptYOffset { get; }
-
-        public short SubscriptYSize { get; }
-
-        public short SuperscriptXOffset { get; }
-
-        public short SuperscriptXSize { get; }
-
-        public short SuperscriptYOffset { get; }
-
-        public short SuperscriptYSize { get; }
-
-        public ushort StyleType { get; }
-
-        public ushort WeightClass { get; }
-
-        public ushort WidthClass { get; }
-
-        public static OS2Table? Load(IGlyphTypeface glyphTypeface)
-        {
-            if (!glyphTypeface.TryGetTable(Tag, out var table))
+            os2Table = default;
+            
+            if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table))
             {
             {
-                return null;
+                return false;
             }
             }
 
 
-            using var stream = new MemoryStream(table);
-            using var binaryReader = new BigEndianBinaryReader(stream, false);
+            var binaryReader = new BigEndianBinaryReader(table.Span);
+
+            os2Table = Load(ref binaryReader);
 
 
-            // Move to start of table.
-            return Load(binaryReader);
+            return true;
         }
         }
 
 
-        public static OS2Table Load(BigEndianBinaryReader reader)
+        private static OS2Table Load(ref BigEndianBinaryReader reader)
         {
         {
-            // Version 1.0
-            // Type   | Name                   | Comments
-            // -------|------------------------|-----------------------
-            // uint16 |version                 | 0x0005
-            // int16  |xAvgCharWidth           |
-            // uint16 |usWeightClass           |
-            // uint16 |usWidthClass            |
-            // uint16 |fsType                  |
-            // int16  |ySubscriptXSize         |
-            // int16  |ySubscriptYSize         |
-            // int16  |ySubscriptXOffset       |
-            // int16  |ySubscriptYOffset       |
-            // int16  |ySuperscriptXSize       |
-            // int16  |ySuperscriptYSize       |
-            // int16  |ySuperscriptXOffset     |
-            // int16  |ySuperscriptYOffset     |
-            // int16  |yStrikeoutSize          |
-            // int16  |yStrikeoutPosition      |
-            // int16  |sFamilyClass            |
-            // uint8  |panose[10]              |
-            // uint32 |ulUnicodeRange1         | Bits 0–31
-            // uint32 |ulUnicodeRange2         | Bits 32–63
-            // uint32 |ulUnicodeRange3         | Bits 64–95
-            // uint32 |ulUnicodeRange4         | Bits 96–127
-            // Tag    |achVendID               |
-            // uint16 |fsSelection             |
-            // uint16 |usFirstCharIndex        |
-            // uint16 |usLastCharIndex         |
-            // int16  |sTypoAscender           |
-            // int16  |sTypoDescender          |
-            // int16  |sTypoLineGap            |
-            // uint16 |usWinAscent             |
-            // uint16 |usWinDescent            |
-            // uint32 |ulCodePageRange1        | Bits 0–31
-            // uint32 |ulCodePageRange2        | Bits 32–63
-            // int16  |sxHeight                |
-            // int16  |sCapHeight              |
-            // uint16 |usDefaultChar           |
-            // uint16 |usBreakChar             |
-            // uint16 |usMaxContext            |
-            // uint16 |usLowerOpticalPointSize |
-            // uint16 |usUpperOpticalPointSize |
-            ushort version = reader.ReadUInt16(); // assert 0x0005
-            short averageCharWidth = reader.ReadInt16();
+            ushort version = reader.ReadUInt16();
+            short xAvgCharWidth = reader.ReadInt16();
             ushort weightClass = reader.ReadUInt16();
             ushort weightClass = reader.ReadUInt16();
             ushort widthClass = reader.ReadUInt16();
             ushort widthClass = reader.ReadUInt16();
-            ushort styleType = reader.ReadUInt16();
-            short subscriptXSize = reader.ReadInt16();
-            short subscriptYSize = reader.ReadInt16();
-            short subscriptXOffset = reader.ReadInt16();
-            short subscriptYOffset = reader.ReadInt16();
-
-            short superscriptXSize = reader.ReadInt16();
-            short superscriptYSize = reader.ReadInt16();
-            short superscriptXOffset = reader.ReadInt16();
-            short superscriptYOffset = reader.ReadInt16();
-
+            ushort fsType = reader.ReadUInt16();
+            short ySubscriptXSize = reader.ReadInt16();
+            short ySubscriptYSize = reader.ReadInt16();
+            short ySubscriptXOffset = reader.ReadInt16();
+            short ySubscriptYOffset = reader.ReadInt16();
+            short ySuperscriptXSize = reader.ReadInt16();
+            short ySuperscriptYSize = reader.ReadInt16();
+            short ySuperscriptXOffset = reader.ReadInt16();
+            short ySuperscriptYOffset = reader.ReadInt16();
             short strikeoutSize = reader.ReadInt16();
             short strikeoutSize = reader.ReadInt16();
             short strikeoutPosition = reader.ReadInt16();
             short strikeoutPosition = reader.ReadInt16();
             short familyClass = reader.ReadInt16();
             short familyClass = reader.ReadInt16();
-            byte[] panose = reader.ReadUInt8Array(10);
-            uint unicodeRange1 = reader.ReadUInt32(); // Bits 0–31
-            uint unicodeRange2 = reader.ReadUInt32(); // Bits 32–63
-            uint unicodeRange3 = reader.ReadUInt32(); // Bits 64–95
-            uint unicodeRange4 = reader.ReadUInt32(); // Bits 96–127
-            string tag = reader.ReadTag();
-            FontStyleSelection fontStyle = reader.ReadUInt16<FontStyleSelection>();
+
+            Panose panose = Panose.Load(ref reader);
+
+            uint unicodeRange1 = reader.ReadUInt32();
+            uint unicodeRange2 = reader.ReadUInt32();
+            uint unicodeRange3 = reader.ReadUInt32();
+            uint unicodeRange4 = reader.ReadUInt32();
+            
+            uint vendorId = reader.ReadUInt32();
+            
+            FontSelectionFlags selection = reader.ReadUInt16<FontSelectionFlags>();
             ushort firstCharIndex = reader.ReadUInt16();
             ushort firstCharIndex = reader.ReadUInt16();
             ushort lastCharIndex = reader.ReadUInt16();
             ushort lastCharIndex = reader.ReadUInt16();
             short typoAscender = reader.ReadInt16();
             short typoAscender = reader.ReadInt16();
@@ -343,82 +204,75 @@ namespace Avalonia.Media.Fonts.Tables
             ushort winAscent = reader.ReadUInt16();
             ushort winAscent = reader.ReadUInt16();
             ushort winDescent = reader.ReadUInt16();
             ushort winDescent = reader.ReadUInt16();
 
 
-            var version0Table = new OS2Table(
-                    averageCharWidth,
-                    weightClass,
-                    widthClass,
-                    styleType,
-                    subscriptXSize,
-                    subscriptYSize,
-                    subscriptXOffset,
-                    subscriptYOffset,
-                    superscriptXSize,
-                    superscriptYSize,
-                    superscriptXOffset,
-                    superscriptYOffset,
-                    strikeoutSize,
-                    strikeoutPosition,
-                    familyClass,
-                    panose,
-                    unicodeRange1,
-                    unicodeRange2,
-                    unicodeRange3,
-                    unicodeRange4,
-                    tag,
-                    fontStyle,
-                    firstCharIndex,
-                    lastCharIndex,
-                    typoAscender,
-                    typoDescender,
-                    typoLineGap,
-                    winAscent,
-                    winDescent);
-
-            if (version == 0)
-            {
-                return version0Table;
-            }
-
-            short heightX = 0;
+            uint codePageRange1 = 0;
+            uint codePageRange2 = 0;
+            short xHeight = 0;
             short capHeight = 0;
             short capHeight = 0;
-
             ushort defaultChar = 0;
             ushort defaultChar = 0;
             ushort breakChar = 0;
             ushort breakChar = 0;
             ushort maxContext = 0;
             ushort maxContext = 0;
+            ushort lowerOpticalPointSize = 0;
+            ushort upperOpticalPointSize = 0xFFFF;
 
 
-            ushort codePageRange1 = reader.ReadUInt16(); // Bits 0–31
-            ushort codePageRange2 = reader.ReadUInt16(); // Bits 32–63
+            if (version >= 1)
+            {
+                codePageRange1 = reader.ReadUInt32();
+                codePageRange2 = reader.ReadUInt32();
+            }
 
 
-            // fields exist only in > v1 https://docs.microsoft.com/en-us/typography/opentype/spec/os2
-            if (version > 1)
+            if (version >= 2)
             {
             {
-                heightX = reader.ReadInt16();
+                xHeight = reader.ReadInt16();
                 capHeight = reader.ReadInt16();
                 capHeight = reader.ReadInt16();
                 defaultChar = reader.ReadUInt16();
                 defaultChar = reader.ReadUInt16();
                 breakChar = reader.ReadUInt16();
                 breakChar = reader.ReadUInt16();
                 maxContext = reader.ReadUInt16();
                 maxContext = reader.ReadUInt16();
             }
             }
 
 
-            var versionLessThan5Table = new OS2Table(
-                    version0Table,
-                    codePageRange1,
-                    codePageRange2,
-                    heightX,
-                    capHeight,
-                    defaultChar,
-                    breakChar,
-                    maxContext);
-
-            if (version < 5)
+            if (version >= 5)
             {
             {
-                return versionLessThan5Table;
+                lowerOpticalPointSize = reader.ReadUInt16();
+                upperOpticalPointSize = reader.ReadUInt16();
             }
             }
 
 
-            ushort lowerOpticalPointSize = reader.ReadUInt16();
-            ushort upperOpticalPointSize = reader.ReadUInt16();
-
             return new OS2Table(
             return new OS2Table(
-                versionLessThan5Table,
+                version,
+                xAvgCharWidth,
+                weightClass,
+                widthClass,
+                fsType,
+                ySubscriptXSize,
+                ySubscriptYSize,
+                ySubscriptXOffset,
+                ySubscriptYOffset,
+                ySuperscriptXSize,
+                ySuperscriptYSize,
+                ySuperscriptXOffset,
+                ySuperscriptYOffset,
+                strikeoutSize,
+                strikeoutPosition,
+                familyClass,
+                panose,
+                unicodeRange1,
+                unicodeRange2,
+                unicodeRange3,
+                unicodeRange4,
+                vendorId,
+                selection,
+                firstCharIndex,
+                lastCharIndex,
+                typoAscender,
+                typoDescender,
+                typoLineGap,
+                winAscent,
+                winDescent,
+                codePageRange1,
+                codePageRange2,
+                xHeight,
+                capHeight,
+                defaultChar,
+                breakChar,
+                maxContext,
                 lowerOpticalPointSize,
                 lowerOpticalPointSize,
                 upperOpticalPointSize);
                 upperOpticalPointSize);
         }
         }

+ 248 - 0
src/Avalonia.Base/Media/Fonts/Tables/Panose.cs

@@ -0,0 +1,248 @@
+namespace Avalonia.Media.Fonts.Tables
+{
+    /// <summary>
+    /// Represents the PANOSE classification for a font.
+    /// PANOSE is a font classification system that describes the visual characteristics of a typeface.
+    /// </summary>
+    /// <remarks>
+    /// The interpretation of bytes 1-9 depends on the FamilyKind (byte 0).
+    /// This struct represents the Latin Text interpretation (FamilyKind = 2), which is the most common.
+    /// For other family kinds, access the raw bytes via the indexer.
+    /// </remarks>
+    internal readonly struct Panose
+    {
+        private readonly byte[] _data;
+
+        public Panose(byte b0, byte b1, byte b2, byte b3, byte b4, byte b5, byte b6, byte b7, byte b8, byte b9)
+        {
+            _data = new byte[10] { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9 };
+        }
+
+        public static Panose Load(ref BigEndianBinaryReader reader)
+        {
+            return new Panose(
+                reader.ReadByte(),
+                reader.ReadByte(),
+                reader.ReadByte(),
+                reader.ReadByte(),
+                reader.ReadByte(),
+                reader.ReadByte(),
+                reader.ReadByte(),
+                reader.ReadByte(),
+                reader.ReadByte(),
+                reader.ReadByte()
+            );
+        }
+
+        /// <summary>
+        /// Gets the family kind classification (byte 0).
+        /// </summary>
+        public PanoseFamilyKind FamilyKind => (PanoseFamilyKind)_data[0];
+
+        // Latin Text properties (when FamilyKind == LatinText)
+
+        /// <summary>
+        /// Gets the serif style (byte 1) for Latin Text fonts.
+        /// </summary>
+        public PanoseSerifStyle SerifStyle => (PanoseSerifStyle)_data[1];
+
+        /// <summary>
+        /// Gets the weight (byte 2) for Latin Text fonts.
+        /// </summary>
+        public PanoseWeight Weight => (PanoseWeight)_data[2];
+
+        /// <summary>
+        /// Gets the proportion (byte 3) for Latin Text fonts.
+        /// </summary>
+        public PanoseProportion Proportion => (PanoseProportion)_data[3];
+
+        /// <summary>
+        /// Gets the contrast (byte 4) for Latin Text fonts.
+        /// </summary>
+        public PanoseContrast Contrast => (PanoseContrast)_data[4];
+
+        /// <summary>
+        /// Gets the stroke variation (byte 5) for Latin Text fonts.
+        /// </summary>
+        public PanoseStrokeVariation StrokeVariation => (PanoseStrokeVariation)_data[5];
+
+        /// <summary>
+        /// Gets the arm style (byte 6) for Latin Text fonts.
+        /// </summary>
+        public PanoseArmStyle ArmStyle => (PanoseArmStyle)_data[6];
+
+        /// <summary>
+        /// Gets the letterform (byte 7) for Latin Text fonts.
+        /// </summary>
+        public PanoseLetterform Letterform => (PanoseLetterform)_data[7];
+
+        /// <summary>
+        /// Gets the midline (byte 8) for Latin Text fonts.
+        /// </summary>
+        public PanoseMidline Midline => (PanoseMidline)_data[8];
+
+        /// <summary>
+        /// Gets the x-height (byte 9) for Latin Text fonts.
+        /// </summary>
+        public PanoseXHeight XHeight => (PanoseXHeight)_data[9];
+    }
+
+    internal enum PanoseFamilyKind : byte
+    {
+        Any = 0,
+        NoFit = 1,
+        LatinText = 2,
+        LatinHandWritten = 3,
+        LatinDecorative = 4,
+        LatinSymbol = 5
+    }
+
+    internal enum PanoseSerifStyle : byte
+    {
+        Any = 0,
+        NoFit = 1,
+        Cove = 2,
+        ObtuseCove = 3,
+        SquareCove = 4,
+        ObtuseSquareCove = 5,
+        Square = 6,
+        Thin = 7,
+        Oval = 8,
+        Exaggerated = 9,
+        Triangle = 10,
+        NormalSans = 11,
+        ObtuseSans = 12,
+        PerpendicularSans = 13,
+        Flared = 14,
+        Rounded = 15
+    }
+
+    internal enum PanoseWeight : byte
+    {
+        Any = 0,
+        NoFit = 1,
+        VeryLight = 2,
+        Light = 3,
+        Thin = 4,
+        Book = 5,
+        Medium = 6,
+        Demi = 7,
+        Bold = 8,
+        Heavy = 9,
+        Black = 10,
+        ExtraBlack = 11
+    }
+
+    internal enum PanoseProportion : byte
+    {
+        Any = 0,
+        NoFit = 1,
+        OldStyle = 2,
+        Modern = 3,
+        EvenWidth = 4,
+        Extended = 5,
+        Condensed = 6,
+        VeryExtended = 7,
+        VeryCondensed = 8,
+        Monospaced = 9
+    }
+
+    internal enum PanoseContrast : byte
+    {
+        Any = 0,
+        NoFit = 1,
+        None = 2,
+        VeryLow = 3,
+        Low = 4,
+        MediumLow = 5,
+        Medium = 6,
+        MediumHigh = 7,
+        High = 8,
+        VeryHigh = 9,
+        HorizontalLow = 10,
+        HorizontalMedium = 11,
+        HorizontalHigh = 12,
+        Broken = 13
+    }
+
+    internal enum PanoseStrokeVariation : byte
+    {
+        Any = 0,
+        NoFit = 1,
+        NoVariation = 2,
+        GradualDiagonal = 3,
+        GradualTransitional = 4,
+        GradualVertical = 5,
+        GradualHorizontal = 6,
+        RapidVertical = 7,
+        RapidHorizontal = 8,
+        InstantVertical = 9,
+        InstantHorizontal = 10
+    }
+
+    internal enum PanoseArmStyle : byte
+    {
+        Any = 0,
+        NoFit = 1,
+        StraightArmsHorizontal = 2,
+        StraightArmsWedge = 3,
+        StraightArmsVertical = 4,
+        StraightArmsSingleSerif = 5,
+        StraightArmsDoubleSerif = 6,
+        NonStraightArmsHorizontal = 7,
+        NonStraightArmsWedge = 8,
+        NonStraightArmsVertical = 9,
+        NonStraightArmsSingleSerif = 10,
+        NonStraightArmsDoubleSerif = 11
+    }
+
+    internal enum PanoseLetterform : byte
+    {
+        Any = 0,
+        NoFit = 1,
+        NormalContact = 2,
+        NormalWeighted = 3,
+        NormalBoxed = 4,
+        NormalFlattened = 5,
+        NormalRounded = 6,
+        NormalOffCenter = 7,
+        NormalSquare = 8,
+        ObliqueContact = 9,
+        ObliqueWeighted = 10,
+        ObliqueBoxed = 11,
+        ObliqueFlattened = 12,
+        ObliqueRounded = 13,
+        ObliqueOffCenter = 14,
+        ObliqueSquare = 15
+    }
+
+    internal enum PanoseMidline : byte
+    {
+        Any = 0,
+        NoFit = 1,
+        StandardTrimmed = 2,
+        StandardPointed = 3,
+        StandardSerifed = 4,
+        HighTrimmed = 5,
+        HighPointed = 6,
+        HighSerifed = 7,
+        ConstantTrimmed = 8,
+        ConstantPointed = 9,
+        ConstantSerifed = 10,
+        LowTrimmed = 11,
+        LowPointed = 12,
+        LowSerifed = 13
+    }
+
+    internal enum PanoseXHeight : byte
+    {
+        Any = 0,
+        NoFit = 1,
+        ConstantSmall = 2,
+        ConstantStandard = 3,
+        ConstantLarge = 4,
+        DuckingSmall = 5,
+        DuckingStandard = 6,
+        DuckingLarge = 7
+    }
+}

+ 1 - 1
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>
     /// <summary>
     /// platforms ids
     /// platforms ids
     /// </summary>
     /// </summary>
-    internal enum PlatformIDs : ushort
+    internal enum PlatformID : ushort
     {
     {
         /// <summary>
         /// <summary>
         /// Unicode platform
         /// Unicode platform

+ 46 - 0
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 FontVersion Version { get; }
+        public float ItalicAngle { get; }
+        public short UnderlinePosition { get; }
+        public short UnderlineThickness { get; }
+        public bool IsFixedPitch { get; }
+
+        private PostTable(FontVersion version, float italicAngle, short underlinePosition, short underlineThickness, bool isFixedPitch)
+        {
+            Version = version;
+            ItalicAngle = italicAngle;
+            UnderlinePosition = underlinePosition;
+            UnderlineThickness = underlineThickness;
+            IsFixedPitch = isFixedPitch;
+        }
+
+        public static PostTable Load(GlyphTypeface glyphTypeface)
+        {
+            if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table))
+            {
+                return default;
+            }
+
+            var binaryReader = new BigEndianBinaryReader(table.Span);
+
+            return Load(ref binaryReader);
+        }
+
+        private static PostTable Load(ref BigEndianBinaryReader reader)
+        {
+            FontVersion version = reader.ReadVersion16Dot16();
+            float italicAngle = reader.ReadFixed();
+            short underlinePosition = reader.ReadFWORD();
+            short underlineThickness = reader.ReadFWORD();
+            uint isFixedPitch = reader.ReadUInt32();
+
+            return new PostTable(version, italicAngle, underlinePosition, underlineThickness, isFixedPitch != 0);
+        }
+    }
+}

+ 0 - 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);
-    }
-}

+ 127 - 0
src/Avalonia.Base/Media/Fonts/Tables/VerticalHeaderTable.cs

@@ -0,0 +1,127 @@
+namespace Avalonia.Media.Fonts.Tables
+{
+    internal readonly struct VerticalHeaderTable
+    {
+        internal const string TableName = "vhea";
+        internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName);
+
+        public FontVersion Version { get; }
+        public short Ascender { get; }
+        public short Descender { get; }
+        public short LineGap { get; }
+        public ushort AdvanceHeightMax { get; }
+        public short MinTopSideBearing { get; }
+        public short MinBottomSideBearing { get; }
+        public short YMaxExtent { get; }
+        public short CaretSlopeRise { get; }
+        public short CaretSlopeRun { get; }
+        public short CaretOffset { get; }
+        public ushort NumberOfVMetrics { get; }
+
+        public VerticalHeaderTable(
+            FontVersion version,
+            short ascender,
+            short descender,
+            short lineGap,
+            ushort advanceHeightMax,
+            short minTopSideBearing,
+            short minBottomSideBearing,
+            short yMaxExtent,
+            short caretSlopeRise,
+            short caretSlopeRun,
+            short caretOffset,
+            ushort numberOfVMetrics)
+        {
+            Version = version;
+            Ascender = ascender;
+            Descender = descender;
+            LineGap = lineGap;
+            AdvanceHeightMax = advanceHeightMax;
+            MinTopSideBearing = minTopSideBearing;
+            MinBottomSideBearing = minBottomSideBearing;
+            YMaxExtent = yMaxExtent;
+            CaretSlopeRise = caretSlopeRise;
+            CaretSlopeRun = caretSlopeRun;
+            CaretOffset = caretOffset;
+            NumberOfVMetrics = numberOfVMetrics;
+        }
+
+        public static bool TryLoad(GlyphTypeface fontFace, out VerticalHeaderTable verticalHeaderTable)
+        {
+            verticalHeaderTable = default;
+
+            if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table))
+            {
+                return false;
+            }
+
+            var binaryReader = new BigEndianBinaryReader(table.Span);
+
+            return TryLoad(ref binaryReader, out verticalHeaderTable);
+        }
+
+        private static bool TryLoad(ref BigEndianBinaryReader reader, out VerticalHeaderTable verticalHeaderTable)
+        {
+            verticalHeaderTable = default;
+
+            // See OpenType spec for vhea:
+            // | Version16Dot16 | version             | 0x00010000 (1.0) or 0x00011000 (1.1)                                            |
+            // | 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                              |
+
+            FontVersion version = reader.ReadVersion16Dot16();
+            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)
+            {
+                return false;
+            }
+
+            ushort numberOfVMetrics = reader.ReadUInt16();
+
+            verticalHeaderTable = new VerticalHeaderTable(
+                version,
+                ascender,
+                descender,
+                lineGap,
+                advanceHeightMax,
+                minTopSideBearing,
+                minBottomSideBearing,
+                yMaxExtent,
+                caretSlopeRise,
+                caretSlopeRun,
+                caretOffset,
+                numberOfVMetrics);
+
+            return true;
+        }
+    }
+}

+ 356 - 0
src/Avalonia.Base/Media/Fonts/UnmanagedFontMemory.cs

@@ -0,0 +1,356 @@
+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 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 (_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 = BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(8, 4));
+                    var length = BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(12, 4));
+
+                    // Bounds checks - ensure values fit within the span
+                    if (offset > (uint)fontData.Length || length > (uint)fontData.Length)
+                    {
+                        return false;
+                    }
+
+                    if (offset + length > (uint)fontData.Length)
+                    {
+                        return false;
+                    }
+
+                    // Safe to cast to int for Slice since we validated bounds
+                    table = Memory.Slice((int)offset, (int)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
+                    {
+                        data.CopyTo(new Span<byte>((void*)ptr, 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 (_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 (_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);
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            // Always use lock for disposal since we don't have a finalizer
+            _lock.EnterWriteLock();
+
+            try
+            {
+                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;
+            }
+            finally
+            {
+                _lock.ExitWriteLock();
+                _lock.Dispose();
+            }
+        }
+    }
+}

+ 3 - 3
src/Avalonia.Base/Media/GlyphMetrics.cs

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

+ 18 - 12
src/Avalonia.Base/Media/GlyphRun.cs

@@ -39,7 +39,7 @@ namespace Avalonia.Media
         /// <param name="baselineOrigin">The baseline origin of the run.</param>
         /// <param name="baselineOrigin">The baseline origin of the run.</param>
         /// <param name="biDiLevel">The bidi level.</param>
         /// <param name="biDiLevel">The bidi level.</param>
         public GlyphRun(
         public GlyphRun(
-            IGlyphTypeface glyphTypeface,
+            GlyphTypeface glyphTypeface,
             double fontRenderingEmSize,
             double fontRenderingEmSize,
             ReadOnlyMemory<char> characters,
             ReadOnlyMemory<char> characters,
             IReadOnlyList<ushort> glyphIndices,
             IReadOnlyList<ushort> glyphIndices,
@@ -61,7 +61,7 @@ namespace Avalonia.Media
         /// <param name="baselineOrigin">The baseline origin of the run.</param>
         /// <param name="baselineOrigin">The baseline origin of the run.</param>
         /// <param name="biDiLevel">The bidi level.</param>
         /// <param name="biDiLevel">The bidi level.</param>
         public GlyphRun(
         public GlyphRun(
-            IGlyphTypeface glyphTypeface,
+            GlyphTypeface glyphTypeface,
             double fontRenderingEmSize,
             double fontRenderingEmSize,
             ReadOnlyMemory<char> characters,
             ReadOnlyMemory<char> characters,
             IReadOnlyList<GlyphInfo> glyphInfos,
             IReadOnlyList<GlyphInfo> glyphInfos,
@@ -90,19 +90,25 @@ namespace Avalonia.Media
         }
         }
 
 
         private static IReadOnlyList<GlyphInfo> CreateGlyphInfos(IReadOnlyList<ushort> glyphIndices,
         private static IReadOnlyList<GlyphInfo> CreateGlyphInfos(IReadOnlyList<ushort> glyphIndices,
-            double fontRenderingEmSize, IGlyphTypeface glyphTypeface)
+            double fontRenderingEmSize, GlyphTypeface glyphTypeface)
         {
         {
             var glyphIndexSpan = ListToSpan(glyphIndices);
             var glyphIndexSpan = ListToSpan(glyphIndices);
-            var glyphAdvances = glyphTypeface.GetGlyphAdvances(glyphIndexSpan);
 
 
             var glyphInfos = new GlyphInfo[glyphIndexSpan.Length];
             var glyphInfos = new GlyphInfo[glyphIndexSpan.Length];
             var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
             var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
 
 
-            for (var i = 0; i < glyphIndexSpan.Length; ++i)
+            var advances = glyphIndexSpan.Length <= 256
+                ? stackalloc ushort[glyphIndexSpan.Length]
+                : new ushort[glyphIndexSpan.Length];
+
+            if (glyphTypeface.TryGetHorizontalGlyphAdvances(glyphIndexSpan, advances))
             {
             {
-                glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale);
+                for (var i = 0; i < glyphIndexSpan.Length; ++i)
+                {
+                    glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, advances[i] * scale);
+                }
             }
             }
-
+            
             return glyphInfos;
             return glyphInfos;
         }
         }
 
 
@@ -137,9 +143,9 @@ namespace Avalonia.Media
         }
         }
 
 
         /// <summary>
         /// <summary>
-        ///     Gets the <see cref="IGlyphTypeface"/> for the <see cref="GlyphRun"/>.
+        ///     Gets the <see cref="GlyphTypeface"/> for the <see cref="GlyphRun"/>.
         /// </summary>
         /// </summary>
-        public IGlyphTypeface GlyphTypeface { get; }
+        public GlyphTypeface GlyphTypeface { get; }
 
 
         /// <summary>
         /// <summary>
         ///     Gets or sets the em size used for rendering the <see cref="GlyphRun"/>.
         ///     Gets or sets the em size used for rendering the <see cref="GlyphRun"/>.
@@ -205,7 +211,7 @@ namespace Avalonia.Media
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the scale of the current <see cref="IGlyphTypeface"/>
+        /// Gets the scale of the current <see cref="GlyphTypeface"/>
         /// </summary>
         /// </summary>
         internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight;
         internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight;
 
 
@@ -270,7 +276,7 @@ namespace Avalonia.Media
                 //For in cluster hits we need to move to the start of the next cluster.
                 //For in cluster hits we need to move to the start of the next cluster.
                 if (inClusterHit)
                 if (inClusterHit)
                 {
                 {
-                    for(; glyphIndex < _glyphInfos.Count; glyphIndex++)
+                    for (; glyphIndex < _glyphInfos.Count; glyphIndex++)
                     {
                     {
                         if (_glyphInfos[glyphIndex].GlyphCluster > characterIndex)
                         if (_glyphInfos[glyphIndex].GlyphCluster > characterIndex)
                         {
                         {
@@ -367,7 +373,7 @@ namespace Avalonia.Media
                     characterIndex = glyphInfo.GlyphCluster;
                     characterIndex = glyphInfo.GlyphCluster;
 
 
                     if (currentX + advance > distance)
                     if (currentX + advance > distance)
-                    {                            
+                    {
                         break;
                         break;
                     }
                     }
 
 

+ 655 - 0
src/Avalonia.Base/Media/GlyphTypeface.cs

@@ -0,0 +1,655 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+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>
+    public sealed class GlyphTypeface
+    {
+        private static readonly IReadOnlyDictionary<CultureInfo, string> s_emptyStringDictionary =
+            new Dictionary<CultureInfo, string>(0);
+
+        private bool _isDisposed;
+
+        private readonly NameTable? _nameTable;
+        private readonly OS2Table _os2Table;
+        private readonly CharacterToGlyphMap _cmapTable;
+        private readonly HorizontalHeaderTable _hhTable;
+        private readonly VerticalHeaderTable _vhTable;
+        private readonly HorizontalMetricsTable? _hmTable;
+        private readonly VerticalMetricsTable? _vmTable;
+        private readonly bool _hasOs2Table;
+        private readonly bool _hasHorizontalMetrics;
+        private readonly bool _hasVerticalMetrics;
+
+        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;
+
+            _hasOs2Table = OS2Table.TryLoad(this, out _os2Table);
+            _cmapTable = CmapTable.Load(this);
+
+            var maxpTable = MaxpTable.Load(this);
+
+            GlyphCount = maxpTable.NumGlyphs;
+
+            _hasHorizontalMetrics = HorizontalHeaderTable.TryLoad(this, out _hhTable);
+
+            if (_hasHorizontalMetrics)
+            {
+                _hmTable = HorizontalMetricsTable.Load(this, _hhTable.NumberOfHMetrics, GlyphCount);
+            }
+
+            _hasVerticalMetrics = VerticalHeaderTable.TryLoad(this, out _vhTable);
+
+            if (_hasVerticalMetrics)
+            {
+                _vmTable = VerticalMetricsTable.Load(this, _vhTable.NumberOfVMetrics, GlyphCount);
+            }
+
+            var ascent = 0;
+            var descent = 0;
+            var lineGap = 0;
+
+            if (_hasOs2Table && (_os2Table.Selection & OS2Table.FontSelectionFlags.USE_TYPO_METRICS) != 0)
+            {
+                ascent = -_os2Table.TypoAscender;
+                descent = -_os2Table.TypoDescender;
+                lineGap = _os2Table.TypoLineGap;
+            }
+            else
+            {
+                if (_hasHorizontalMetrics)
+                {
+                    ascent = -_hhTable.Ascender;
+                    descent = -_hhTable.Descender;
+                    lineGap = _hhTable.LineGap;
+                }
+            }
+
+            if (_hasOs2Table && (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;
+                }
+            }
+
+            HeadTable.TryLoad(this, out var headTable);
+
+            var postTable = PostTable.Load(this);
+
+            var isFixedPitch = postTable.IsFixedPitch;
+            var underlineOffset = postTable.UnderlinePosition;
+            var underlineSize = postTable.UnderlineThickness;
+
+            Metrics = new FontMetrics
+            {
+                DesignEmHeight = headTable?.UnitsPerEm ?? 0,
+                Ascent = ascent,
+                Descent = descent,
+                LineGap = lineGap,
+                UnderlinePosition = -underlineOffset,
+                UnderlineThickness = underlineSize,
+                StrikethroughPosition = _hasOs2Table ? -_os2Table.StrikeoutPosition : 0,
+                StrikethroughThickness = _hasOs2Table ? _os2Table.StrikeoutSize : 0,
+                IsFixedPitch = isFixedPitch
+            };
+
+            FontSimulations = fontSimulations;
+
+            var fontWeight = GetFontWeight(_hasOs2Table ? _os2Table : null, headTable);
+
+            Weight = (fontSimulations & FontSimulations.Bold) != 0 ? FontWeight.Bold : fontWeight;
+
+            var style = GetFontStyle(_hasOs2Table ? _os2Table : null, headTable, postTable);
+
+            Style = (fontSimulations & FontSimulations.Oblique) != 0 ? FontStyle.Italic : style;
+
+            var stretch = GetFontStretch(_hasOs2Table ? _os2Table : null);
+
+            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)
+            {
+                Dictionary<CultureInfo, string>? familyNames = null;
+                Dictionary<CultureInfo, string>? faceNames = null;
+
+                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);
+
+                        familyNames ??= new Dictionary<CultureInfo, string>(1);
+
+                        if (!familyNames.ContainsKey(culture))
+                        {
+                            familyNames[culture] = nameRecord.GetValue();
+                        }
+                    }
+
+                    if (nameRecord.NameID == KnownNameIds.FontSubfamilyName)
+                    {
+                        if (nameRecord.Platform != Fonts.Tables.PlatformID.Windows || nameRecord.LanguageID == 0)
+                        {
+                            continue;
+                        }
+
+                        var culture = GetCulture(nameRecord.LanguageID);
+
+                        faceNames ??= new Dictionary<CultureInfo, string>(1);
+
+                        if (!faceNames.ContainsKey(culture))
+                        {
+                            faceNames[culture] = nameRecord.GetValue();
+                        }
+                    }
+                }
+
+                FamilyNames = familyNames ?? s_emptyStringDictionary;
+                FaceNames = faceNames ?? s_emptyStringDictionary;
+            }
+            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);
+                }
+                catch (CultureNotFoundException)
+                {
+                    return CultureInfo.InvariantCulture;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the family name of the font.
+        /// </summary>
+        public string FamilyName { get; }
+
+        /// <summary>
+        /// Gets the typographic family name of the font.
+        /// </summary>
+        public string TypographicFamilyName { get; }
+
+        /// <summary>
+        /// Gets a read-only mapping of localized culture-specific family names.
+        /// </summary>
+        /// <remarks>The dictionary contains entries for each supported culture, where the key is a <see
+        /// cref="CultureInfo"/> representing the culture, and the value is the corresponding localized family name. The
+        /// dictionary may be empty if no family names are available.</remarks>
+        public IReadOnlyDictionary<CultureInfo, string> FamilyNames { get; }
+
+        /// <summary>
+        /// Gets a read-only mapping of culture-specific face names.
+        /// </summary>
+        /// <remarks>Each entry in the dictionary maps a <see cref="System.Globalization.CultureInfo"/> to
+        /// the corresponding localized face name. The dictionary is empty if no face names are defined.</remarks>
+        public IReadOnlyDictionary<CultureInfo, string> FaceNames { get; }
+
+        /// <summary>
+        /// Gets a read-only mapping of Unicode character codes to glyph indices for the font.
+        /// </summary>
+        /// <remarks>This dictionary provides the correspondence between Unicode code points and the
+        /// glyphs defined in the font. The mapping can be used to look up the glyph index for a given character when
+        /// rendering or processing text. The set of mapped characters depends on the font's supported character
+        /// set.</remarks>
+        public CharacterToGlyphMap CharacterToGlyphMap => _cmapTable;
+
+        /// <summary>
+        /// Gets the font metrics associated with this font.
+        /// </summary>
+        public FontMetrics Metrics { get; }
+
+        /// <summary>
+        /// Gets the font weight.
+        /// </summary>
+        public FontWeight Weight { get; }
+
+        /// <summary>
+        /// Gets the font style.
+        /// </summary>
+        public FontStyle Style { get; }
+
+        /// <summary>
+        /// Gets the font stretch.
+        /// </summary>
+        public FontStretch Stretch { get; }
+
+        /// <summary>
+        /// Gets the font simulation settings applied to the <see cref="GlyphTypeface"/>.
+        /// </summary>
+        public FontSimulations FontSimulations { get; }
+
+        /// <summary>
+        /// Gets the number of glyphs held by this font.
+        /// </summary>
+        public int GlyphCount { get; }
+
+        /// <summary>
+        /// Gets the list of OpenType feature tags supported by the font.
+        /// </summary>
+        /// <remarks>The returned list reflects the features available in the underlying font and is
+        /// read-only. The order of features in the list is not guaranteed. This property does not return null; if the
+        /// font does not support any features, the list will be empty.</remarks>
+        public IReadOnlyList<OpenTypeTag> SupportedFeatures
+        {
+            get
+            {
+                if (_supportedFeatures != null)
+                {
+                    return _supportedFeatures;
+                }
+
+                _supportedFeatures = LoadSupportedFeatures();
+
+                return _supportedFeatures;
+            }
+        }
+
+        /// <summary>
+        /// Gets the platform-specific typeface associated with this font.
+        /// </summary>
+        public IPlatformTypeface PlatformTypeface { get; }
+
+        /// <summary>
+        /// Gets the typeface information used by the text shaper for this font.
+        /// </summary>
+        /// <remarks>The returned typeface is created on demand and cached for subsequent accesses. This
+        /// property is typically used by text rendering components that require low-level font shaping
+        /// details.</remarks>
+        public ITextShaperTypeface TextShaperTypeface
+        {
+            get
+            {
+                if (_textShaperTypeface != null)
+                {
+                    return _textShaperTypeface;
+                }
+
+                var textShaper = AvaloniaLocator.Current.GetRequiredService<ITextShaperImpl>();
+
+                _textShaperTypeface = textShaper.CreateTypeface(this);
+
+                return _textShaperTypeface;
+            }
+        }
+
+        /// <summary>
+        /// Attempts to retrieve the horizontal advance width for the specified glyph.
+        /// </summary>
+        /// <remarks>Returns false if horizontal metrics are not available or if the specified glyph is
+        /// not present in the metrics table.</remarks>
+        /// <param name="glyphId">The identifier of the glyph for which to obtain the horizontal advance width.</param>
+        /// <param name="advance">When this method returns, contains the horizontal advance width of the glyph if found; otherwise, zero. This
+        /// parameter is passed uninitialized.</param>
+        /// <returns>true if the horizontal advance width was successfully retrieved; otherwise, false.</returns>
+        public bool TryGetHorizontalGlyphAdvance(ushort glyphId, out ushort advance)
+        {
+            advance = default;
+
+            if (!_hasHorizontalMetrics || _hmTable is null)
+            {
+                return false;
+            }
+
+            if (!_hmTable.TryGetAdvance(glyphId, out advance))
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Attempts to retrieve horizontal advance widths for multiple glyphs in a single operation.
+        /// </summary>
+        /// <remarks>This method is significantly more efficient than calling <see cref="TryGetHorizontalGlyphAdvance"/>
+        /// multiple times as it minimizes memory access overhead and exploits data locality. This is the preferred method
+        /// for batch glyph metrics retrieval in text layout and rendering scenarios. Returns false if horizontal metrics
+        /// are not available.</remarks>
+        /// <param name="glyphIds">Read-only span of glyph identifiers for which to retrieve advance widths.</param>
+        /// <param name="advances">Output span to write the advance widths. Must be at least as long as <paramref name="glyphIds"/>.</param>
+        /// <returns>true if horizontal metrics are available and all advances were successfully retrieved; otherwise, false.</returns>
+        public bool TryGetHorizontalGlyphAdvances(ReadOnlySpan<ushort> glyphIds, Span<ushort> advances)
+        {
+            if (!_hasHorizontalMetrics || _hmTable is null)
+            {
+                return false;
+            }
+
+            return _hmTable.TryGetAdvances(glyphIds, advances);
+        }
+
+        /// <summary>
+        /// Attempts to retrieve the metrics for the specified glyph.
+        /// </summary>
+        /// <remarks>This method returns metrics only if horizontal or vertical metrics are available for
+        /// the specified glyph. If neither is available, the method returns false and the output parameter is set to
+        /// its default value.</remarks>
+        /// <param name="glyph">The identifier of the glyph for which to obtain metrics.</param>
+        /// <param name="metrics">When this method returns, contains the metrics for the specified glyph if found; otherwise, contains the
+        /// default value.</param>
+        /// <returns>true if metrics for the specified glyph are available; otherwise, false.</returns>
+        public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
+        {
+            metrics = default;
+
+            HorizontalGlyphMetric hMetric = default;
+            VerticalGlyphMetric vMetric = default;
+
+            var hasHorizontal = false;
+            var hasVertical = false;
+
+            if (_hasHorizontalMetrics && _hmTable != null)
+            {
+                hasHorizontal = _hmTable.TryGetMetrics(glyph, out hMetric);
+            }
+
+            if (_hasVerticalMetrics && _vmTable != null)
+            {
+                hasVertical = _vmTable.TryGetMetrics(glyph, out vMetric);
+            }
+
+            if (!hasHorizontal && !hasVertical)
+            {
+                return false;
+            }
+
+            metrics = new GlyphMetrics
+            {
+                XBearing = hMetric.LeftSideBearing,
+                YBearing = vMetric.TopSideBearing,
+                Width = hMetric.AdvanceWidth,
+                Height = vMetric.AdvanceHeight
+            };
+
+            return true;
+        }
+
+        /// <summary>
+        /// Attempts to retrieve glyph metrics for multiple glyphs in a single operation.
+        /// </summary>
+        /// <remarks>This method is significantly more efficient than calling <see cref="TryGetGlyphMetrics(ushort, out GlyphMetrics)"/>
+        /// multiple times as it minimizes memory access overhead and exploits data locality. This is the preferred
+        /// method for batch glyph metrics retrieval in text layout and rendering scenarios. Returns false if neither
+        /// horizontal nor vertical metrics are available.</remarks>
+        /// <param name="glyphIds">Read-only span of glyph identifiers for which to retrieve metrics.</param>
+        /// <param name="metrics">Output span to write the glyph metrics. Must be at least as long as <paramref name="glyphIds"/>.</param>
+        /// <returns>true if metrics are available and all were successfully retrieved; otherwise, false.</returns>
+        public bool TryGetGlyphMetrics(ReadOnlySpan<ushort> glyphIds, Span<GlyphMetrics> metrics)
+        {
+            if (metrics.Length < glyphIds.Length)
+            {
+                throw new ArgumentException("Output span must be at least as long as input span", nameof(metrics));
+            }
+
+            if (!_hasHorizontalMetrics && !_hasVerticalMetrics)
+            {
+                return false;
+            }
+
+            // Use stackalloc for temporary buffers to avoid heap allocations
+            Span<HorizontalGlyphMetric> hMetrics = glyphIds.Length <= 256
+                ? stackalloc HorizontalGlyphMetric[glyphIds.Length]
+                : new HorizontalGlyphMetric[glyphIds.Length];
+
+            Span<VerticalGlyphMetric> vMetrics = glyphIds.Length <= 256
+                ? stackalloc VerticalGlyphMetric[glyphIds.Length]
+                : new VerticalGlyphMetric[glyphIds.Length];
+
+            bool hasHorizontal = false;
+            bool hasVertical = false;
+
+            // Batch retrieve horizontal metrics
+            if (_hasHorizontalMetrics && _hmTable != null)
+            {
+                hasHorizontal = _hmTable.TryGetMetrics(glyphIds, hMetrics);
+            }
+
+            // Batch retrieve vertical metrics
+            if (_hasVerticalMetrics && _vmTable != null)
+            {
+                hasVertical = _vmTable.TryGetMetrics(glyphIds, vMetrics);
+            }
+
+            if (!hasHorizontal && !hasVertical)
+            {
+                return false;
+            }
+
+            // Combine horizontal and vertical metrics
+            for (int i = 0; i < glyphIds.Length; i++)
+            {
+                metrics[i] = new GlyphMetrics
+                {
+                    XBearing = hasHorizontal ? hMetrics[i].LeftSideBearing : (short)0,
+                    YBearing = hasVertical ? vMetrics[i].TopSideBearing : (short)0,
+                    Width = hasHorizontal ? hMetrics[i].AdvanceWidth : (ushort)0,
+                    Height = hasVertical ? vMetrics[i].AdvanceHeight : (ushort)0
+                };
+            }
+
+            return true;
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        private IReadOnlyList<OpenTypeTag> LoadSupportedFeatures()
+        {
+            var gPosFeatures = FeatureListTable.LoadGPos(this);
+            var gSubFeatures = FeatureListTable.LoadGSub(this);
+
+            var count = (gPosFeatures?.Features.Count ?? 0) + (gSubFeatures?.Features.Count ?? 0);
+
+            if (count == 0)
+            {
+                return [];
+            }
+
+            var supportedFeatures = new List<OpenTypeTag>(count);
+
+            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);
+                }
+            }
+
+            return supportedFeatures;
+        }
+
+        private static FontStyle GetFontStyle(OS2Table? oS2Table, HeadTable? headTable, PostTable postTable)
+        {
+            bool isItalic = false;
+            bool isOblique = false;
+
+            if (oS2Table.HasValue)
+            {
+                isItalic = (oS2Table.Value.Selection & OS2Table.FontSelectionFlags.ITALIC) != 0;
+                isOblique = (oS2Table.Value.Selection & OS2Table.FontSelectionFlags.OBLIQUE) != 0;
+            }
+
+            if (!isItalic && headTable != null)
+            {
+                isItalic = headTable.MacStyle.HasFlag(MacStyleFlags.Italic);
+            }
+
+            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 static FontWeight GetFontWeight(OS2Table? os2Table, HeadTable? headTable)
+        {
+            if (os2Table.HasValue && os2Table.Value.WeightClass >= 1 && os2Table.Value.WeightClass <= 1000)
+            {
+                return (FontWeight)os2Table.Value.WeightClass;
+            }
+
+            if (headTable != null && headTable.MacStyle.HasFlag(MacStyleFlags.Bold))
+            {
+                return FontWeight.Bold;
+            }
+
+            if (os2Table.HasValue && os2Table.Value.Panose.FamilyKind == PanoseFamilyKind.LatinText)
+            {
+                return os2Table.Value.Panose.Weight switch
+                {
+                    PanoseWeight.VeryLight => FontWeight.Thin,
+                    PanoseWeight.Light => FontWeight.Light,
+                    PanoseWeight.Thin => FontWeight.ExtraLight,
+                    PanoseWeight.Book => FontWeight.Normal,
+                    PanoseWeight.Medium => FontWeight.Medium,
+                    PanoseWeight.Demi => FontWeight.SemiBold,
+                    PanoseWeight.Bold => FontWeight.Bold,
+                    PanoseWeight.Heavy => FontWeight.ExtraBold,
+                    PanoseWeight.Black => FontWeight.Black,
+                    PanoseWeight.ExtraBlack => FontWeight.ExtraBlack,
+                    _ => FontWeight.Normal
+                };
+            }
+
+            return FontWeight.Normal;
+        }
+
+        private static FontStretch GetFontStretch(OS2Table? os2Table)
+        {
+            if (os2Table.HasValue && os2Table.Value.WidthClass >= 1 && os2Table.Value.WidthClass <= 9)
+            {
+                return (FontStretch)os2Table.Value.WidthClass;
+            }
+
+            if (os2Table.HasValue && os2Table.Value.Panose.FamilyKind == PanoseFamilyKind.LatinText)
+            {
+                return os2Table.Value.Panose.Proportion switch
+                {
+                    PanoseProportion.VeryCondensed => FontStretch.UltraCondensed,
+                    PanoseProportion.Condensed => FontStretch.Condensed,
+                    PanoseProportion.Modern or PanoseProportion.EvenWidth or PanoseProportion.OldStyle => FontStretch.Normal,
+                    PanoseProportion.Extended => FontStretch.Expanded,
+                    PanoseProportion.VeryExtended => FontStretch.UltraExpanded,
+                    PanoseProportion.Monospaced => FontStretch.Normal,
+                    _ => FontStretch.Normal
+                };
+            }
+
+            return FontStretch.Normal;
+        }
+
+        private void Dispose(bool disposing)
+        {
+            if (_isDisposed)
+            {
+                return;
+            }
+
+            _isDisposed = true;
+
+            if (!disposing)
+            {
+                return;
+            }
+
+            PlatformTypeface.Dispose();
+        }
+    }
+}

+ 20 - 0
src/Avalonia.Base/Media/IFontMemory.cs

@@ -0,0 +1,20 @@
+using System;
+using Avalonia.Media.Fonts;
+using Avalonia.Metadata;
+
+namespace Avalonia.Media
+{
+    [NotClientImplementable]
+    public interface IFontMemory : IDisposable
+    {
+        /// <summary>
+        /// Attempts to retrieve the memory block associated with the specified OpenType table tag.
+        /// </summary>
+        /// <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);
+    }
+}

+ 0 - 115
src/Avalonia.Base/Media/IGlyphTypeface.cs

@@ -1,115 +0,0 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-using Avalonia.Metadata;
-
-namespace Avalonia.Media
-{
-    [Unstable]
-    public interface IGlyphTypeface : IDisposable
-    {
-        /// <summary>
-        /// Gets the family name for the <see cref="IGlyphTypeface"/> object.
-        /// </summary>
-        string FamilyName { get; }
-
-        /// <summary>
-        /// Gets the designed weight of the font represented by the <see cref="IGlyphTypeface"/> object.
-        /// </summary>
-        FontWeight Weight { get; }
-
-        /// <summary>
-        /// Gets the style for the <see cref="IGlyphTypeface"/> object.
-        /// </summary>
-        FontStyle Style { get; }
-
-        /// <summary>
-        /// Gets the <see cref="FontStretch"/> value for the <see cref="IGlyphTypeface"/> object.
-        /// </summary>
-        FontStretch Stretch { get; }
-
-        /// <summary>
-        ///     Gets the number of glyphs held by this glyph typeface. 
-        /// </summary>
-        int GlyphCount { get; }
-
-        /// <summary>
-        ///     Gets the font metrics.
-        /// </summary>
-        /// <returns>
-        ///     The font metrics.
-        /// </returns>
-        FontMetrics Metrics { get; }
-
-        /// <summary>
-        ///     Gets the algorithmic style simulations applied to this glyph typeface.
-        /// </summary>
-        FontSimulations FontSimulations { get; }
-
-        /// <summary>
-        ///     Tries to get a glyph's metrics in em units.
-        /// </summary>
-        /// <param name="glyph">The glyph id.</param>
-        /// <param name="metrics">The glyph metrics.</param>
-        /// <returns>
-        ///   <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);
-
-        /// <summary>
-        ///     Tries to get an glyph index for specified codepoint.
-        /// </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);
-
-        /// <summary>
-        ///     Returns an array of glyph indices. Codepoints that are not represented by the font are returned as <code>0</code>.
-        /// </summary>
-        /// <param name="codepoints">The codepoints to map.</param>
-        /// <returns>
-        ///     An array of glyph indices.
-        /// </returns>
-        ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints);
-
-        /// <summary>
-        ///     Returns the glyph advance for the specified glyph.
-        /// </summary>
-        /// <param name="glyph">The glyph.</param>
-        /// <returns>
-        ///     The advance.
-        /// </returns>
-        int GetGlyphAdvance(ushort glyph);
-
-        /// <summary>
-        ///     Returns an array of glyph advances in design em size.
-        /// </summary>
-        /// <param name="glyphs">The glyph indices.</param>
-        /// <returns>
-        ///     An array of glyph advances.
-        /// </returns>
-        int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs);
-
-        /// <summary>
-        ///     Returns the contents of the table data for the specified 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, [NotNullWhen(true)] out byte[]? table);
-    }
-}

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

+ 46 - 0
src/Avalonia.Base/Media/IPlatformTypeface.cs

@@ -0,0 +1,46 @@
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using Avalonia.Metadata;
+
+namespace Avalonia.Media
+{
+    [NotClientImplementable]
+    public interface IPlatformTypeface : IFontMemory
+    {
+        /// <summary>
+        /// Gets the font family name.
+        /// </summary>
+        /// <remarks>
+        /// The family name should be the same as the one used to create the typeface via the platform font manager. 
+        /// It can be different from the actaual family name because an alias or a fallback name could have been used.
+        /// </remarks>
+        string FamilyName { get; }
+
+        /// <summary>
+        /// Gets the designed weight of the font represented by the <see cref="IPlatformTypeface"/> object.
+        /// </summary>
+        FontWeight Weight { get; }
+
+        /// <summary>
+        /// Gets the style for the <see cref="IPlatformTypeface"/> object.
+        /// </summary>
+        FontStyle Style { get; }
+
+        /// <summary>
+        /// Gets the <see cref="FontStretch"/> value for the <see cref="IPlatformTypeface"/> object.
+        /// </summary>
+        FontStretch Stretch { get; }
+
+        /// <summary>
+        ///     Gets the algorithmic style simulations applied to <see cref="IPlatformTypeface"/> object.
+        /// </summary>
+        FontSimulations FontSimulations { get; }
+
+        /// <summary>
+        /// Returns the font file stream represented by the <see cref="GlyphTypeface"/>.
+        /// </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);
+    }
+}

+ 11 - 0
src/Avalonia.Base/Media/ITextShaperTypeface.cs

@@ -0,0 +1,11 @@
+using System;
+using Avalonia.Metadata;
+
+namespace Avalonia.Media
+{
+    [NotClientImplementable]
+    public interface ITextShaperTypeface : IDisposable
+    {
+
+    }
+}

+ 3 - 3
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Media.TextFormatting
         private GlyphInfo[]? _rentedBuffer;
         private GlyphInfo[]? _rentedBuffer;
         private ArraySlice<GlyphInfo> _glyphInfos;
         private ArraySlice<GlyphInfo> _glyphInfos;
 
 
-        public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
+        public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
         {
         {
             Text = text;
             Text = text;
             _rentedBuffer = ArrayPool<GlyphInfo>.Shared.Rent(bufferLength);
             _rentedBuffer = ArrayPool<GlyphInfo>.Shared.Rent(bufferLength);
@@ -23,7 +23,7 @@ namespace Avalonia.Media.TextFormatting
             BidiLevel = bidiLevel;
             BidiLevel = bidiLevel;
         }
         }
 
 
-        internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
+        internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfos, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
         {
         {
             Text = text;
             Text = text;
             _glyphInfos = glyphInfos;
             _glyphInfos = glyphInfos;
@@ -40,7 +40,7 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// <summary>
         /// The buffer's glyph typeface.
         /// The buffer's glyph typeface.
         /// </summary>
         /// </summary>
-        public IGlyphTypeface GlyphTypeface { get; }
+        public GlyphTypeface GlyphTypeface { get; }
 
 
         /// <summary>
         /// <summary>
         /// The buffers font rendering em size.
         /// The buffers font rendering em size.

+ 7 - 7
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@@ -146,7 +146,7 @@ namespace Avalonia.Media.TextFormatting
             //Move forward until we reach the next base character
             //Move forward until we reach the next base character
             while (enumerator.MoveNext(out grapheme))
             while (enumerator.MoveNext(out grapheme))
             {
             {
-                if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
+                if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.CharacterToGlyphMap.TryGetGlyph(grapheme.FirstCodepoint, out _))
                 {
                 {
                     break;
                     break;
                 }
                 }
@@ -167,8 +167,8 @@ namespace Avalonia.Media.TextFormatting
         /// <returns></returns>
         /// <returns></returns>
         internal static bool TryGetShapeableLength(
         internal static bool TryGetShapeableLength(
             ReadOnlySpan<char> text,
             ReadOnlySpan<char> text,
-            IGlyphTypeface glyphTypeface,
-            IGlyphTypeface? defaultGlyphTypeface,
+            GlyphTypeface glyphTypeface,
+            GlyphTypeface? defaultGlyphTypeface,
             out int length)
             out int length)
         {
         {
             length = 0;
             length = 0;
@@ -194,15 +194,15 @@ namespace Avalonia.Media.TextFormatting
 
 
                 if (!currentCodepoint.IsWhiteSpace
                 if (!currentCodepoint.IsWhiteSpace
                     && defaultGlyphTypeface != null
                     && defaultGlyphTypeface != null
-                    && defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _))
+                    && defaultGlyphTypeface.CharacterToGlyphMap.TryGetGlyph(currentCodepoint, out _))
                 {
                 {
                     break;
                     break;
                 }
                 }
 
 
                 //Stop at the first missing glyph
                 //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.TryGetGlyph(currentCodepoint, out _))
                 {
                 {
                     break;
                     break;
                 }
                 }

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -719,7 +719,7 @@ namespace Avalonia.Media.TextFormatting
             var flowDirection = paragraphProperties.FlowDirection;
             var flowDirection = paragraphProperties.FlowDirection;
             var properties = paragraphProperties.DefaultTextRunProperties;
             var properties = paragraphProperties.DefaultTextRunProperties;
             var glyphTypeface = properties.CachedGlyphTypeface;
             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 glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) };
 
 
             var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
             var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize,

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs

@@ -5,7 +5,7 @@
     /// </summary>
     /// </summary>
     public readonly record struct TextMetrics
     public readonly record struct TextMetrics
     {
     {
-        public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize)
+        public TextMetrics(GlyphTypeface glyphTypeface, double fontRenderingEmSize)
         {
         {
             var fontMetrics = glyphTypeface.Metrics;
             var fontMetrics = glyphTypeface.Metrics;
 
 

+ 2 - 2
src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs

@@ -12,7 +12,7 @@ namespace Avalonia.Media.TextFormatting
     /// </remarks>
     /// </remarks>
     public abstract class TextRunProperties : IEquatable<TextRunProperties>
     public abstract class TextRunProperties : IEquatable<TextRunProperties>
     {
     {
-        private IGlyphTypeface? _cachedGlyphTypeFace;
+        private GlyphTypeface? _cachedGlyphTypeFace;
 
 
         /// <summary>
         /// <summary>
         /// Run typeface
         /// Run typeface
@@ -54,7 +54,7 @@ namespace Avalonia.Media.TextFormatting
         /// </summary>
         /// </summary>
         public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline;
         public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline;
 
 
-        internal IGlyphTypeface CachedGlyphTypeface
+        internal GlyphTypeface CachedGlyphTypeface
             => _cachedGlyphTypeFace ??= Typeface.GlyphTypeface;
             => _cachedGlyphTypeFace ??= Typeface.GlyphTypeface;
 
 
         public bool Equals(TextRunProperties? other)
         public bool Equals(TextRunProperties? other)

+ 4 - 4
src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Media.TextFormatting
     {
     {
         // TODO12: Remove in 12.0.0 and make fontFeatures parameter in main ctor optional
         // TODO12: Remove in 12.0.0 and make fontFeatures parameter in main ctor optional
         public TextShaperOptions(
         public TextShaperOptions(
-            IGlyphTypeface typeface,
+            GlyphTypeface typeface,
             double fontRenderingEmSize = 12,
             double fontRenderingEmSize = 12,
             sbyte bidiLevel = 0,
             sbyte bidiLevel = 0,
             CultureInfo? culture = null,
             CultureInfo? culture = null,
@@ -22,7 +22,7 @@ namespace Avalonia.Media.TextFormatting
 
 
         // TODO12:Change signature in 12.0.0
         // TODO12:Change signature in 12.0.0
         public TextShaperOptions(
         public TextShaperOptions(
-            IGlyphTypeface typeface, 
+            GlyphTypeface typeface, 
             IReadOnlyList<FontFeature>? fontFeatures,
             IReadOnlyList<FontFeature>? fontFeatures,
             double fontRenderingEmSize = 12, 
             double fontRenderingEmSize = 12, 
             sbyte bidiLevel = 0, 
             sbyte bidiLevel = 0, 
@@ -30,7 +30,7 @@ namespace Avalonia.Media.TextFormatting
             double incrementalTabWidth = 0,
             double incrementalTabWidth = 0,
             double letterSpacing = 0)
             double letterSpacing = 0)
         {
         {
-            Typeface = typeface;
+            GlyphTypeface = typeface;
             FontRenderingEmSize = fontRenderingEmSize;
             FontRenderingEmSize = fontRenderingEmSize;
             BidiLevel = bidiLevel;
             BidiLevel = bidiLevel;
             Culture = culture;
             Culture = culture;
@@ -42,7 +42,7 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// <summary>
         /// Get the typeface.
         /// Get the typeface.
         /// </summary>
         /// </summary>
-        public IGlyphTypeface Typeface { get; }
+        public GlyphTypeface GlyphTypeface { get; }
         /// <summary>
         /// <summary>
         /// Get the font rendering em size.
         /// Get the font rendering em size.
         /// </summary>
         /// </summary>

+ 1 - 1
src/Avalonia.Base/Media/Typeface.cs

@@ -83,7 +83,7 @@ namespace Avalonia.Media
         /// <value>
         /// <value>
         /// The glyph typeface.
         /// The glyph typeface.
         /// </value>
         /// </value>
-        public IGlyphTypeface GlyphTypeface
+        public GlyphTypeface GlyphTypeface
         {
         {
             get
             get
             {
             {

+ 6 - 25
src/Avalonia.Base/Platform/IFontManagerImpl.cs

@@ -30,12 +30,12 @@ namespace Avalonia.Platform
         /// <param name="fontStretch">The font stretch.</param>
         /// <param name="fontStretch">The font stretch.</param>
         /// <param name="familyName">The family name. This is optional and can be used as an initial hint for matching.</param>
         /// <param name="familyName">The family name. This is optional and can be used as an initial hint for matching.</param>
         /// <param name="culture">The culture.</param>
         /// <param name="culture">The culture.</param>
-        /// <param name="typeface">The matching typeface.</param>
+        /// <param name="platformTypeface">The matching platform typeface.</param>
         /// <returns>
         /// <returns>
         ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
         ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
         /// </returns>
         /// </returns>
         bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
         bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
-            FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface);
+            FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, [NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface);
 
 
         /// <summary>
         /// <summary>
         ///     Tries to get a glyph typeface for specified parameters.
         ///     Tries to get a glyph typeface for specified parameters.
@@ -44,42 +44,23 @@ namespace Avalonia.Platform
         /// <param name="style">The font style.</param>
         /// <param name="style">The font style.</param>
         /// <param name="weight">The font weiht.</param>
         /// <param name="weight">The font weiht.</param>
         /// <param name="stretch">The font stretch.</param>
         /// <param name="stretch">The font stretch.</param>
-        /// <param name="glyphTypeface">The created glyphTypeface</param>
+        /// <param name="platformTypeface">The created platform typeface</param>
         /// <returns>
         /// <returns>
         ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
         ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
         /// </returns>
         /// </returns>
         bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
         bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
-            FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
+            FontStretch stretch, [NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface);
 
 
         /// <summary>
         /// <summary>
         ///     Tries to create a glyph typeface from specified stream.
         ///     Tries to create a glyph typeface from specified stream.
         /// </summary>
         /// </summary>
         /// <param name="stream">A stream that holds the font's data.</param>
         /// <param name="stream">A stream that holds the font's data.</param>
         /// <param name="fontSimulations">Specifies algorithmic style simulations.</param>
         /// <param name="fontSimulations">Specifies algorithmic style simulations.</param>
-        /// <param name="glyphTypeface">The created glyphTypeface</param>
+        /// <param name=" platformTypeface">The created platform typeface</param>
         /// <returns>
         /// <returns>
         ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
         ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
         /// </returns>
         /// </returns>
-        bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
-    }
-
-    internal interface IFontManagerImpl2 : IFontManagerImpl
-    {
-        /// <summary>
-        ///     Tries to match a specified character to a typeface that supports specified font properties.
-        /// </summary>
-        /// <param name="codepoint">The codepoint to match against.</param>
-        /// <param name="fontStyle">The font style.</param>
-        /// <param name="fontWeight">The font weight.</param>
-        /// <param name="fontStretch">The font stretch.</param>
-        /// <param name="familyName">The family name. This is optional and can be used as an initial hint for matching.</param>
-        /// <param name="culture">The culture.</param>
-        /// <param name="typeface">The matching typeface.</param>
-        /// <returns>
-        ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
-        /// </returns>
-        bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
-            FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, [NotNullWhen(true)] out IGlyphTypeface? typeface);
+        bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface);
 
 
         /// <summary>
         /// <summary>
         /// Tries to get a list of typefaces for the specified family name.
         /// Tries to get a list of typefaces for the specified family name.

+ 0 - 4
src/Avalonia.Base/Platform/IGlyphRunImpl.cs

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

+ 1 - 1
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@@ -170,7 +170,7 @@ namespace Avalonia.Platform
         /// <param name="glyphInfos">The list of glyphs.</param>
         /// <param name="glyphInfos">The list of glyphs.</param>
         /// <param name="baselineOrigin">The baseline origin of the run. Can be null.</param>
         /// <param name="baselineOrigin">The baseline origin of the run. Can be null.</param>
         /// <returns>An <see cref="IGlyphRunImpl"/>.</returns>
         /// <returns>An <see cref="IGlyphRunImpl"/>.</returns>
-        IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin);
+        IGlyphRunImpl CreateGlyphRun(GlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin);
 
 
         /// <summary>
         /// <summary>
         /// Creates a backend-specific object using a low-level API graphics context
         /// Creates a backend-specific object using a low-level API graphics context

+ 11 - 2
src/Avalonia.Base/Platform/ITextShaperImpl.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Metadata;
 using Avalonia.Metadata;
 
 
@@ -7,7 +8,7 @@ namespace Avalonia.Platform
     /// <summary>
     /// <summary>
     /// An abstraction that is used produce shaped text.
     /// An abstraction that is used produce shaped text.
     /// </summary>
     /// </summary>
-    [Unstable]
+    [NotClientImplementable]
     public interface ITextShaperImpl
     public interface ITextShaperImpl
     {
     {
         /// <summary>
         /// <summary>
@@ -17,5 +18,13 @@ namespace Avalonia.Platform
         /// <param name="options">Text shaper options to customize the shaping process.</param>
         /// <param name="options">Text shaper options to customize the shaping process.</param>
         /// <returns>A shaped glyph run.</returns>
         /// <returns>A shaped glyph run.</returns>
         ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options);
         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(GlyphTypeface glyphTypeface);
+    }
 }
 }

+ 6 - 0
src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs

@@ -41,10 +41,16 @@ internal static class StorageBookmarkHelper
 
 
         var arrayLength = HeaderLength + nativeBookmarkBytes.Length;
         var arrayLength = HeaderLength + nativeBookmarkBytes.Length;
         var arrayPool = ArrayPool<byte>.Shared.Rent(arrayLength);
         var arrayPool = ArrayPool<byte>.Shared.Rent(arrayLength);
+
         try
         try
         {
         {
             // Write platform into first 16 bytes.
             // Write platform into first 16 bytes.
             var arraySpan = arrayPool.AsSpan(0, arrayLength);
             var arraySpan = arrayPool.AsSpan(0, arrayLength);
+
+            // Ensure any leftover data from the pooled array is cleared before we use it so
+            // that bytes we don't overwrite (e.g. header padding) won't leak into the encoded bookmark.
+            arraySpan.Clear();
+
             AvaHeaderPrefix.CopyTo(arraySpan);
             AvaHeaderPrefix.CopyTo(arraySpan);
             platform.CopyTo(arraySpan.Slice(AvaHeaderPrefix.Length));
             platform.CopyTo(arraySpan.Slice(AvaHeaderPrefix.Length));
 
 

+ 3 - 4
src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs

@@ -1,6 +1,5 @@
 using System;
 using System;
 using Avalonia.Media;
 using Avalonia.Media;
-using Avalonia.Platform;
 
 
 namespace Avalonia.Rendering.Composition.Server
 namespace Avalonia.Rendering.Composition.Server
 {
 {
@@ -30,15 +29,15 @@ namespace Avalonia.Rendering.Composition.Server
             return maxHeight;
             return maxHeight;
         }
         }
 
 
-        public DiagnosticTextRenderer(IGlyphTypeface typeface, double fontRenderingEmSize)
+        public DiagnosticTextRenderer(GlyphTypeface glyphTypeface, double fontRenderingEmSize)
         {
         {
             var chars = new char[LastChar - FirstChar + 1];
             var chars = new char[LastChar - FirstChar + 1];
             for (var c = FirstChar; c <= LastChar; c++)
             for (var c = FirstChar; c <= LastChar; c++)
             {
             {
                 var index = c - FirstChar;
                 var index = c - FirstChar;
                 chars[index] = c;
                 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 });
             }
             }
         }
         }
 
 

+ 7 - 2
src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs

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

+ 1 - 0
src/Avalonia.Desktop/Avalonia.Desktop.csproj

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

+ 22 - 0
src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.HarfBuzz.csproj

@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <PropertyGroup>
+        <TargetFrameworks>$(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks)</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 - 0
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());
+        }
+    }
+
+}

+ 33 - 12
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.Collections.Concurrent;
 using System.Globalization;
 using System.Globalization;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
+using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Platform;
 using Avalonia.Platform;
@@ -10,9 +11,9 @@ using HarfBuzzSharp;
 using Buffer = HarfBuzzSharp.Buffer;
 using Buffer = HarfBuzzSharp.Buffer;
 using GlyphInfo = HarfBuzzSharp.GlyphInfo;
 using GlyphInfo = HarfBuzzSharp.GlyphInfo;
 
 
-namespace Avalonia.Skia
+namespace Avalonia.Harfbuzz
 {
 {
-    internal class TextShaperImpl : ITextShaperImpl
+    public class HarfBuzzTextShaper : ITextShaperImpl
     {
     {
         [ThreadStatic]
         [ThreadStatic]
         private static Buffer? s_buffer;
         private static Buffer? s_buffer;
@@ -22,7 +23,14 @@ namespace Avalonia.Skia
         public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
         public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
         {
         {
             var textSpan = text.Span;
             var textSpan = text.Span;
-            var typeface = options.Typeface;
+
+            var glyphTypeface = options.GlyphTypeface;
+
+            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 fontRenderingEmSize = options.FontRenderingEmSize;
             var bidiLevel = options.BidiLevel;
             var bidiLevel = options.BidiLevel;
             var culture = options.Culture;
             var culture = options.Culture;
@@ -48,7 +56,7 @@ namespace Avalonia.Skia
                 static (_, culture) => new Language(culture),
                 static (_, culture) => new Language(culture),
                 usedCulture);
                 usedCulture);
 
 
-            var font = ((GlyphTypefaceImpl)typeface).Font;
+            var font = harfBuzzTypeface.HBFont;
 
 
             font.Shape(buffer, GetFeatures(options));
             font.Shape(buffer, GetFeatures(options));
 
 
@@ -63,7 +71,7 @@ namespace Avalonia.Skia
 
 
             var bufferLength = buffer.Length;
             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();
             var glyphInfos = buffer.GetGlyphInfoSpan();
 
 
@@ -83,11 +91,18 @@ namespace Avalonia.Skia
 
 
                 if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t')
                 if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t')
                 {
                 {
-                    glyphIndex = typeface.GetGlyph(' ');
+                    glyphIndex = glyphTypeface.CharacterToGlyphMap[' '];
 
 
-                    glyphAdvance = options.IncrementalTabWidth > 0 ?
-                        options.IncrementalTabWidth :
-                        4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
+                    if(options.IncrementalTabWidth > 0)
+                    {
+                        glyphAdvance = options.IncrementalTabWidth;
+                    }
+                    else
+                    {
+                        glyphTypeface.TryGetHorizontalGlyphAdvance(glyphIndex, out var advance);
+
+                        glyphAdvance = 4 * advance * textScale;
+                    }                          
                 }
                 }
 
 
                 shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
                 shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
@@ -96,6 +111,12 @@ namespace Avalonia.Skia
             return shapedBuffer;
             return shapedBuffer;
         }
         }
 
 
+
+        public ITextShaperTypeface CreateTypeface(GlyphTypeface glyphTypeface)
+        {
+            return new HarfBuzzTypeface(glyphTypeface);
+        }
+
         private static void MergeBreakPair(Buffer buffer)
         private static void MergeBreakPair(Buffer buffer)
         {
         {
             var length = buffer.Length;
             var length = buffer.Length;
@@ -193,18 +214,18 @@ namespace Avalonia.Skia
             }
             }
 
 
             var features = new Feature[options.FontFeatures.Count];
             var features = new Feature[options.FontFeatures.Count];
-            
+
             for (var i = 0; i < options.FontFeatures.Count; i++)
             for (var i = 0; i < options.FontFeatures.Count; i++)
             {
             {
                 var fontFeature = options.FontFeatures[i];
                 var fontFeature = options.FontFeatures[i];
 
 
                 features[i] = new Feature(
                 features[i] = new Feature(
-                    Tag.Parse(fontFeature.Tag), 
+                    Tag.Parse(fontFeature.Tag),
                     (uint)fontFeature.Value,
                     (uint)fontFeature.Value,
                     (uint)fontFeature.Start,
                     (uint)fontFeature.Start,
                     (uint)fontFeature.End);
                     (uint)fontFeature.End);
             }
             }
-            
+
             return features;
             return features;
         }
         }
     }
     }

+ 64 - 0
src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTypeface.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Media;
+using HarfBuzzSharp;
+
+namespace Avalonia.Harfbuzz
+{
+    internal class HarfBuzzTypeface : ITextShaperTypeface
+    {
+        public HarfBuzzTypeface(GlyphTypeface glyphTypeface)
+        {
+            GlyphTypeface = glyphTypeface;
+
+            HBFace = new Face(GetTable) { UnitsPerEm = glyphTypeface.Metrics.DesignEmHeight };
+
+            HBFont = new Font(HBFace);
+
+            HBFont.SetFunctionsOpenType();
+        }
+
+        public GlyphTypeface 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
+            {
+                table.Span.CopyTo(new Span<byte>((void*)nativePtr, 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();
+        }
+
+    }
+}

+ 6 - 0
src/Headless/Avalonia.Headless/Avalonia.Headless.csproj

@@ -5,6 +5,7 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
     <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
+    <ProjectReference Include="..\..\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 
   <Import Project="..\..\..\build\DevAnalyzers.props" />
   <Import Project="..\..\..\build\DevAnalyzers.props" />
@@ -24,4 +25,9 @@
     <InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
   </ItemGroup>
   </ItemGroup>
+
+  <ItemGroup>
+    <None Remove="BareMinimum.ttf" />
+    <EmbeddedResource Include="BareMinimum.ttf" />
+  </ItemGroup>
 </Project>
 </Project>

BIN
src/Headless/Avalonia.Headless/BareMinimum.ttf


+ 5 - 8
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@@ -5,11 +5,9 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 using Avalonia.Media;
 using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.Rendering.SceneGraph;
-using Avalonia.Utilities;
 using Avalonia.Media.Imaging;
 using Avalonia.Media.Imaging;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting;
+using Avalonia.Platform;
 
 
 namespace Avalonia.Headless
 namespace Avalonia.Headless
 {
 {
@@ -19,8 +17,7 @@ namespace Avalonia.Headless
         {
         {
             AvaloniaLocator.CurrentMutable
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface())
                 .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;
         public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) => this;
@@ -134,7 +131,7 @@ namespace Avalonia.Headless
         }
         }
 
 
         public IGlyphRunImpl CreateGlyphRun(
         public IGlyphRunImpl CreateGlyphRun(
-            IGlyphTypeface glyphTypeface, 
+            GlyphTypeface glyphTypeface, 
             double fontRenderingEmSize,
             double fontRenderingEmSize,
             IReadOnlyList<GlyphInfo> glyphInfos, 
             IReadOnlyList<GlyphInfo> glyphInfos, 
             Point baselineOrigin)
             Point baselineOrigin)
@@ -145,7 +142,7 @@ namespace Avalonia.Headless
         internal class HeadlessGlyphRunStub : IGlyphRunImpl
         internal class HeadlessGlyphRunStub : IGlyphRunImpl
         {
         {
             public HeadlessGlyphRunStub(
             public HeadlessGlyphRunStub(
-                IGlyphTypeface glyphTypeface,
+                GlyphTypeface glyphTypeface,
                 double fontRenderingEmSize,
                 double fontRenderingEmSize,
                 Point baselineOrigin)
                 Point baselineOrigin)
             {
             {
@@ -158,7 +155,7 @@ namespace Avalonia.Headless
 
 
             public Point BaselineOrigin { get; }
             public Point BaselineOrigin { get; }
 
 
-            public IGlyphTypeface GlyphTypeface { get; }
+            public GlyphTypeface GlyphTypeface { get; }
 
 
             public double FontRenderingEmSize { get; }           
             public double FontRenderingEmSize { get; }           
 
 

+ 157 - 232
src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs

@@ -6,11 +6,13 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls.Utils;
+using Avalonia.Headless;
 using Avalonia.Input;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Input.Platform;
 using Avalonia.Media;
 using Avalonia.Media;
-using Avalonia.Media.TextFormatting;
-using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Media.Fonts;
 using Avalonia.Platform;
 using Avalonia.Platform;
 
 
 namespace Avalonia.Headless
 namespace Avalonia.Headless
@@ -66,32 +68,21 @@ 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, string? familyName = null)
         {
         {
-            DesignEmHeight = 10,
-            Ascent = 2,
-            Descent = 10,
-            IsFixedPitch = true,
-            LineGap = 0,
-            UnderlinePosition = 2,
-            UnderlineThickness = 1,
-            StrikethroughPosition = 2,
-            StrikethroughThickness = 1
-        };
-
-        public int GlyphCount => 1337;
+            _fontMemory = UnmanagedFontMemory.LoadFromStream(stream);
 
 
-        public FontSimulations FontSimulations => FontSimulations.None;
+            var dummy = new GlyphTypeface(this);
+
+            FamilyName = familyName ?? dummy.FamilyName;
+            Weight = dummy.Weight;
+            Style = dummy.Style;
+            Stretch = dummy.Stretch;
+        }
 
 
         public string FamilyName { get; }
         public string FamilyName { get; }
 
 
@@ -101,266 +92,200 @@ namespace Avalonia.Headless
 
 
         public FontStretch Stretch { get; }
         public FontStretch Stretch { get; }
 
 
-        public void Dispose()
-        {
-        }
-
-        public ushort GetGlyph(uint codepoint)
-        {
-            return (ushort)codepoint;
-        }
-
-        public bool TryGetGlyph(uint codepoint, out ushort glyph)
-        {
-            glyph = 8;
-
-            return true;
-        }
+        public FontSimulations FontSimulations => FontSimulations.None;
 
 
-        public int GetGlyphAdvance(ushort glyph)
+        public void Dispose()
         {
         {
-            return 8;
+            _fontMemory?.Dispose();
         }
         }
 
 
-        public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
+        public bool TryGetStream([NotNullWhen(true)] out Stream? stream)
         {
         {
-            var advances = new int[glyphs.Length];
-
-            for (var i = 0; i < advances.Length; i++)
+            stream = null;
+            
+            if (_fontMemory is null)
             {
             {
-                advances[i] = 8;
+                return false;
             }
             }
+            
+            var data = _fontMemory.Memory.Span;
 
 
-            return advances;
-        }
+            stream = new MemoryStream(data.ToArray());
 
 
-        public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
-        {
-            return codepoints.ToArray().Select(x => (ushort)x).ToArray();
+            return true;
         }
         }
 
 
-        public bool TryGetTable(uint tag, out byte[] table)
+        public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table)
         {
         {
-            table = null!;
-            return false;
-        }
-
-        public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
-        {
-            metrics = new GlyphMetrics
-            {
-                Width = 10,
-                Height = 10
-            };
-
-            return true;
+            table = default;
+            
+            return _fontMemory is not null && _fontMemory.TryGetTable(tag, out table);
         }
         }
     }
     }
+}
 
 
-    internal class HeadlessTextShaperStub : ITextShaperImpl
+internal class HeadlessFontManagerStub : IFontManagerImpl
+{
+    private readonly string _defaultFamilyName;
+    
+    public HeadlessFontManagerStub(string defaultFamilyName = "Default")
     {
     {
-        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;
-
-                var codepoint = Codepoint.ReadAt(textSpan, i, out var count);
-
-                // Handle CRLF as a single cluster
-                if (codepoint.Value == 0x0D && Codepoint.ReadAt(textSpan, i + count, out var lfCount).Value == 0x0A)
-                {
-                    count += lfCount;
-                }
-
-                var glyphIndex = typeface.GetGlyph(codepoint);
-
-                for (var j = 0; j < count; ++j)
-                {
-                    shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10);
-                }
+        _defaultFamilyName = defaultFamilyName;
+    }
 
 
-                i += count;
-            }
+    public string GetDefaultFontFamilyName() => _defaultFamilyName;
 
 
-            return shapedBuffer;
-        }
+    string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
+    {
+        return new[] { _defaultFamilyName };
     }
     }
 
 
-    internal class HeadlessFontManagerStub : IFontManagerImpl
+    public bool TryMatchCharacter(
+        int codepoint,
+        FontStyle fontStyle,
+        FontWeight fontWeight,
+        FontStretch fontStretch,
+        string? familyName,
+        CultureInfo? culture,
+        out IPlatformTypeface platformTypeface)
     {
     {
-        private readonly string _defaultFamilyName;
-
-        public HeadlessFontManagerStub(string defaultFamilyName = "Default")
-        {
-            _defaultFamilyName = defaultFamilyName;
-        }
-
-        public int TryCreateGlyphTypefaceCount { get; private set; }
-
-        public string GetDefaultFontFamilyName()
-        {
-            return _defaultFamilyName;
-        }
-
-        string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
-        {
-            return new[] { _defaultFamilyName };
-        }
-
-        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
-            FontStretch fontStretch, string? familyName, 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;
-
-            TryCreateGlyphTypefaceCount++;
-
-            if (familyName == "Unknown")
-            {
-                return false;
-            }
-
-            glyphTypeface = new HeadlessGlyphTypefaceImpl(familyName, style, weight, stretch);
-
-            return true;
-        }
-
-        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);
+        platformTypeface = null!;
 
 
-            TryCreateGlyphTypefaceCount++;
-
-            return true;
-        }
+        return false;
     }
     }
 
 
-    internal class HeadlessFontManagerWithMultipleSystemFontsStub : IFontManagerImpl
+    public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+        FontStretch stretch, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface)
     {
     {
-        private readonly string[] _installedFontFamilyNames;
-        private readonly string _defaultFamilyName;
-
-        public HeadlessFontManagerWithMultipleSystemFontsStub(
-            string[] installedFontFamilyNames,
-            string defaultFamilyName = "Default")
-        {
-            _installedFontFamilyNames = installedFontFamilyNames;
-            _defaultFamilyName = defaultFamilyName;
-        }
-
-        public int TryCreateGlyphTypefaceCount { get; private set; }
-
-        public string GetDefaultFontFamilyName()
-        {
-            return _defaultFamilyName;
-        }
+        var defaultFontUri = new Uri("resm:Avalonia.Headless.BareMinimum.ttf?assembly=Avalonia.Headless");
+
+        var assetLoader = new StandardAssetLoader(typeof(HeadlessFontManagerStub).Assembly);
+        
+        var stream = assetLoader.Open(defaultFontUri);
+        
+        platformTypeface = new HeadlessPlatformTypeface(stream, familyName);
+        
+        return true;
+    }
 
 
-        string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
-        {
-            return _installedFontFamilyNames;
-        }
+    public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface)
+    {
+        platformTypeface = new HeadlessPlatformTypeface(stream);
+        
+        return true;
+    }
 
 
-        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
-            FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey)
-        {
-            fontKey = new Typeface(_defaultFamilyName);
+    public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
+    {
+        familyTypefaces = null;
+        
+        return false;
+    }
+}
 
 
-            return false;
-        }
+internal class HeadlessFontManagerWithMultipleSystemFontsStub : IFontManagerImpl
+{
+    private readonly string[] _installedFontFamilyNames;
+    private readonly string _defaultFamilyName;
 
 
-        public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
-            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
-        {
-            glyphTypeface = null;
+    public HeadlessFontManagerWithMultipleSystemFontsStub(
+        string[] installedFontFamilyNames,
+        string defaultFamilyName = "Default")
+    {
+        _installedFontFamilyNames = installedFontFamilyNames;
+        _defaultFamilyName = defaultFamilyName;
+    }
 
 
-            TryCreateGlyphTypefaceCount++;
+    public int TryCreateGlyphTypefaceCount { get; private set; }
 
 
-            if (familyName == "Unknown")
-            {
-                return false;
-            }
+    string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
+    {
+        return _installedFontFamilyNames;
+    }
 
 
-            glyphTypeface = new HeadlessGlyphTypefaceImpl(familyName, style, weight, stretch);
+    public string GetDefaultFontFamilyName()
+    {
+        return _defaultFamilyName;
+    }
 
 
-            return true;
-        }
+    public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface)
+    {
+        platformTypeface = null;
+        
+        return false;
+    }
 
 
-        public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface)
-        {
-            glyphTypeface = new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal);
+    public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface)
+    {
+        platformTypeface = null;
+        
+        return false;
+    }
 
 
-            return true;
-        }
+    public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
+    {
+        familyTypefaces = null;
+        
+        return false;
     }
     }
 
 
-    internal class HeadlessIconLoaderStub : IPlatformIconLoader
+    public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, 
+        string? familyName, CultureInfo? culture, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface)
     {
     {
-        private class IconStub : IWindowIconImpl
-        {
-            public void Save(Stream outputStream)
-            {
+        platformTypeface = null;
+        
+        return false;
+    }
+}
 
 
-            }
-        }
-        public IWindowIconImpl LoadIcon(string fileName)
+internal class HeadlessIconLoaderStub : IPlatformIconLoader
+{
+    private class IconStub : IWindowIconImpl
+    {
+        public void Save(Stream outputStream)
         {
         {
-            return new IconStub();
-        }
 
 
-        public IWindowIconImpl LoadIcon(Stream stream)
-        {
-            return new IconStub();
         }
         }
+    }
+    public IWindowIconImpl LoadIcon(string fileName)
+    {
+        return new IconStub();
+    }
 
 
-        public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
-        {
-            return new IconStub();
-        }
+    public IWindowIconImpl LoadIcon(Stream stream)
+    {
+        return new IconStub();
     }
     }
 
 
-    internal class HeadlessScreensStub : ScreensBase<int, PlatformScreen>
+    public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
     {
     {
-        protected override IReadOnlyList<int> GetAllScreenKeys() => new[] { 1 };
+        return new IconStub();
+    }
+}
 
 
-        protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key);
+internal class HeadlessScreensStub : ScreensBase<int, PlatformScreen>
+{
+    protected override IReadOnlyList<int> GetAllScreenKeys() => new[] { 1 };
 
 
-        private class PlatformScreenStub : PlatformScreen
+    protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key);
+
+    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;
     }
     }
 }
 }

+ 11 - 32
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@@ -11,7 +11,7 @@ using SkiaSharp;
 
 
 namespace Avalonia.Skia
 namespace Avalonia.Skia
 {
 {
-    internal class FontManagerImpl : IFontManagerImpl, IFontManagerImpl2
+    internal class FontManagerImpl : IFontManagerImpl
     {
     {
         private SKFontManager _skFontManager = SKFontManager.Default;
         private SKFontManager _skFontManager = SKFontManager.Default;
 
 
@@ -33,28 +33,6 @@ namespace Avalonia.Skia
 
 
         [ThreadStatic] private static string[]? t_languageTagBuffer;
         [ThreadStatic] private static string[]? t_languageTagBuffer;
 
 
-        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
-            FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey)
-        {
-            if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out SKTypeface? skTypeface))
-            {
-                fontKey = default;
-
-                return false;
-            }
-
-            fontKey = new Typeface(
-                skTypeface.FamilyName, 
-                skTypeface.FontStyle.Slant.ToAvalonia(), 
-                (FontWeight)skTypeface.FontStyle.Weight, 
-                (FontStretch)skTypeface.FontStyle.Width);
-
-            skTypeface.Dispose();
-
-            return true;
-
-        }
-
         public bool TryMatchCharacter(
         public bool TryMatchCharacter(
             int codepoint,
             int codepoint,
             FontStyle fontStyle,
             FontStyle fontStyle,
@@ -62,18 +40,19 @@ namespace Avalonia.Skia
             FontStretch fontStretch,
             FontStretch fontStretch,
             string? familyName,
             string? familyName,
             CultureInfo? culture,
             CultureInfo? culture,
-            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+            [NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface)
         {
         {
             if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out SKTypeface? skTypeface))
             if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out SKTypeface? skTypeface))
             {
             {
-                glyphTypeface = null;
+                platformTypeface = null;
 
 
                 return false;
                 return false;
             }
             }
 
 
-            glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
+            platformTypeface = new SkiaTypeface(skTypeface, FontSimulations.None);
 
 
             return true;
             return true;
+
         }
         }
 
 
         private bool TryMatchCharacter(
         private bool TryMatchCharacter(
@@ -117,9 +96,9 @@ namespace Avalonia.Skia
         }
         }
 
 
         public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
         public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
-            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+            FontStretch stretch, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface)
         {
         {
-            glyphTypeface = null;
+            platformTypeface = null;
 
 
             var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch, style.ToSkia());
             var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch, style.ToSkia());
 
 
@@ -142,23 +121,23 @@ namespace Avalonia.Skia
                 fontSimulations |= FontSimulations.Oblique;
                 fontSimulations |= FontSimulations.Oblique;
             }
             }
 
 
-            glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);
+            platformTypeface = new SkiaTypeface(skTypeface, fontSimulations);
 
 
             return true;
             return true;
         }
         }
 
 
-        public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface)
         {
         {
             var skTypeface = SKTypeface.FromStream(stream);
             var skTypeface = SKTypeface.FromStream(stream);
 
 
             if (skTypeface != null)
             if (skTypeface != null)
             {
             {
-                glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);
+                platformTypeface = new SkiaTypeface(skTypeface, fontSimulations);
 
 
                 return true;
                 return true;
             }
             }
 
 
-            glyphTypeface = null;
+            platformTypeface = null;
 
 
             return false;
             return false;
         }
         }

+ 3 - 5
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

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

+ 0 - 385
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@@ -1,385 +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)
-                {
-                    var languageId = nameRecord.LanguageID == 0 ?
-                        (ushort)CultureInfo.InvariantCulture.LCID :
-                        nameRecord.LanguageID;
-                    
-                    switch (nameRecord.NameID)
-                    {
-                        case KnownNameIds.FontFamilyName:
-                            {
-                                familyNames.TryAdd(languageId, nameRecord.Value);
-                                break;
-                            }
-                        case KnownNameIds.FontSubfamilyName:
-                            {
-                                faceNames.TryAdd(languageId, nameRecord.Value);
-                                break;
-                            }
-                    }
-                }
-
-                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, [NotNullWhen(true)] 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;
-            }
-        }
-    }
-}

+ 6 - 6
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -2,15 +2,15 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using Avalonia.Media;
 using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Metal;
 using Avalonia.OpenGL;
 using Avalonia.OpenGL;
 using Avalonia.Platform;
 using Avalonia.Platform;
-using Avalonia.Media.Imaging;
+using Avalonia.Skia.Metal;
 using Avalonia.Skia.Vulkan;
 using Avalonia.Skia.Vulkan;
 using Avalonia.Vulkan;
 using Avalonia.Vulkan;
 using SkiaSharp;
 using SkiaSharp;
-using Avalonia.Media.TextFormatting;
-using Avalonia.Metal;
-using Avalonia.Skia.Metal;
 
 
 namespace Avalonia.Skia
 namespace Avalonia.Skia
 {
 {
@@ -81,7 +81,7 @@ namespace Avalonia.Skia
 
 
         public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
         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.");
                 throw new InvalidOperationException("PlatformImpl can't be null.");
             }
             }
@@ -205,7 +205,7 @@ namespace Avalonia.Skia
             return new WriteableBitmapImpl(size, dpi, format, alphaFormat);
             return new WriteableBitmapImpl(size, dpi, format, alphaFormat);
         }
         }
 
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, 
+        public IGlyphRunImpl CreateGlyphRun(GlyphTypeface glyphTypeface, double fontRenderingEmSize, 
             IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
             IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
         {
         {
             return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin);
             return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin);

+ 1 - 2
src/Skia/Avalonia.Skia/SkiaPlatform.cs

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

+ 83 - 0
src/Skia/Avalonia.Skia/SkiaTypeface.cs

@@ -0,0 +1,83 @@
+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 string FamilyName => SKTypeface.FamilyName;
+
+        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();
+        }
+    }
+}

+ 1 - 0
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@@ -12,6 +12,7 @@
   </PropertyGroup>
   </PropertyGroup>
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
     <ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
+    <ProjectReference Include="..\..\HarfBuzz\Avalonia.HarfBuzz\Avalonia.HarfBuzz.csproj" />
     <ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
     <ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 

+ 1 - 0
src/iOS/Avalonia.iOS/Platform.cs

@@ -52,6 +52,7 @@ namespace Avalonia
             return builder
             return builder
                 .UseStandardRuntimePlatformSubsystem()
                 .UseStandardRuntimePlatformSubsystem()
                 .UseWindowingSubsystem(() => iOS.Platform.Register(appDelegate), "iOS")
                 .UseWindowingSubsystem(() => iOS.Platform.Register(appDelegate), "iOS")
+                .UseHarfBuzz()
                 .UseSkia();
                 .UseSkia();
         }
         }
 
 

+ 2 - 4
tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs

@@ -1,5 +1,4 @@
 using System;
 using System;
-using Avalonia.Headless;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.UnitTests;
 using Avalonia.UnitTests;
 using Xunit;
 using Xunit;
@@ -65,12 +64,11 @@ namespace Avalonia.Base.UnitTests.Media
                 }
                 }
             };
             };
 
 
-            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
-                .With(fontManagerImpl: new HeadlessFontManagerStub())))
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
             {
             {
                 AvaloniaLocator.CurrentMutable.Bind<FontManagerOptions>().ToConstant(options);
                 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);
                     FontFamily.Default, null, out var typeface);
 
 
                 Assert.Equal("MyFont", typeface.FontFamily.Name);
                 Assert.Equal("MyFont", typeface.FontFamily.Name);

+ 256 - 0
tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/HeadTableTests.cs

@@ -0,0 +1,256 @@
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.Media.Fonts.Tables;
+using Avalonia.Platform;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Media.Fonts.Tables
+{
+    public class HeadTableTests
+    {
+        private static string s_InterFontUri = "resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests";
+
+        [Fact]
+        public void Should_Load_HeadTable_From_Inter_Font()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var success = HeadTable.TryLoad(typeface, out var headTable);
+
+            Assert.True(success);
+            Assert.NotNull(headTable);
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_Version()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+            Assert.Equal((ushort)1, headTable.Version.Major);
+            Assert.Equal((ushort)0, headTable.Version.Minor);
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_MagicNumber()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+            Assert.Equal(0x5F0F3CF5u, headTable.MagicNumber);
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_UnitsPerEm()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+            Assert.Equal(2816, headTable.UnitsPerEm);
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_BoundingBox()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+            Assert.Equal(-2080, headTable.XMin);
+            Assert.Equal(7274, headTable.XMax);
+            Assert.Equal(-900, headTable.YMin);
+            Assert.Equal(3072, headTable.YMax);
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_IndexToLocFormat()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+            Assert.Equal(IndexToLocFormat.Long, headTable.IndexToLocFormat);
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_GlyphDataFormat()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+            Assert.Equal(GlyphDataFormat.Current, headTable.GlyphDataFormat);
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_LowestRecPPEM()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+            Assert.Equal(6, headTable.LowestRecPPEM);
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_FontRevision()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+            Assert.True(headTable.FontRevision.ToFloat() > 0);
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_Created_Timestamp()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+            Assert.True(headTable.Created > new DateTime(1904, 1, 1));
+            Assert.True(headTable.Created < DateTime.UtcNow);
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_Modified_Timestamp()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+            Assert.True(headTable.Modified > new DateTime(1904, 1, 1));
+            Assert.True(headTable.Modified < DateTime.UtcNow);
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_Flags()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+
+            Assert.True(headTable.Flags.HasFlag(HeadFlags.BaselineAtY0));
+        }
+
+        [Fact]
+        public void HeadTable_Should_Have_Valid_FontDirectionHint()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(HeadTable.TryLoad(typeface, out var headTable));
+            Assert.Equal(FontDirectionHint.LeftToRightWithNeutrals, headTable.FontDirectionHint);
+        }
+
+        private class CustomPlatformTypeface : IPlatformTypeface
+        {
+            private readonly UnmanagedFontMemory _fontMemory;
+
+            public CustomPlatformTypeface(Stream stream, string fontFamily = "Custom")
+            {
+                _fontMemory = UnmanagedFontMemory.LoadFromStream(stream);
+                FamilyName = fontFamily;
+            }
+
+            public FontWeight Weight => FontWeight.Normal;
+
+            public FontStyle Style => FontStyle.Normal;
+
+            public FontStretch Stretch => FontStretch.Normal;
+
+            public string FamilyName { get; }
+
+            public FontSimulations FontSimulations => FontSimulations.None;
+
+            public void Dispose()
+            {
+                ((IDisposable)_fontMemory).Dispose();
+            }
+
+            public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream)
+            {
+                var memory = _fontMemory.Memory;
+
+                var handle = memory.Pin();
+                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);
+        }
+    }
+}

+ 233 - 0
tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/MaxpTableTests.cs

@@ -0,0 +1,233 @@
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.Media.Fonts.Tables;
+using Avalonia.Platform;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Media.Fonts.Tables
+{
+    public class MaxpTableTests
+    {
+        private static string s_InterFontUri = "resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests";
+
+        [Fact]
+        public void Should_Load_MaxpTable_From_Inter_Font()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var maxpTable = MaxpTable.Load(typeface);
+
+            Assert.NotEqual(default, maxpTable);
+        }
+
+        [Fact]
+        public void MaxpTable_Should_Have_Valid_NumGlyphs()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var maxpTable = MaxpTable.Load(typeface);
+
+            Assert.Equal(2547, maxpTable.NumGlyphs);
+        }
+
+        [Fact]
+        public void MaxpTable_TrueType_Should_Have_Version_1_0()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var maxpTable = MaxpTable.Load(typeface);
+
+            Assert.Equal(1, maxpTable.Version.Major);
+            Assert.Equal(0, maxpTable.Version.Minor);
+        }
+
+        [Fact]
+        public void MaxpTable_Version_1_0_Should_Have_Valid_MaxPoints()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var maxpTable = MaxpTable.Load(typeface);
+
+            Assert.Equal(148, maxpTable.MaxPoints);
+        }
+
+        [Fact]
+        public void MaxpTable_Version_1_0_Should_Have_Valid_MaxContours()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var maxpTable = MaxpTable.Load(typeface);
+
+            Assert.Equal(12, maxpTable.MaxContours);
+        }
+
+        [Fact]
+        public void MaxpTable_Version_1_0_Should_Have_Valid_MaxZones()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var maxpTable = MaxpTable.Load(typeface);
+
+            Assert.Equal(1, maxpTable.MaxZones);
+        }
+
+        [Fact]
+        public void MaxpTable_Should_Have_Valid_MaxCompositePoints()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var maxpTable = MaxpTable.Load(typeface);
+
+            Assert.Equal(112, maxpTable.MaxCompositePoints);
+        }
+
+        [Fact]
+        public void MaxpTable_Should_Have_Valid_MaxCompositeContours()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var maxpTable = MaxpTable.Load(typeface);
+
+            Assert.Equal(7, maxpTable.MaxCompositeContours);
+        }
+
+        [Fact]
+        public void MaxpTable_Should_Have_Valid_MaxStackElements()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var maxpTable = MaxpTable.Load(typeface);
+
+            Assert.Equal(0, maxpTable.MaxStackElements);
+        }
+
+        [Fact]
+        public void MaxpTable_Should_Have_Valid_MaxComponentDepth()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var maxpTable = MaxpTable.Load(typeface);
+
+            Assert.Equal(1, maxpTable.MaxComponentDepth);
+        }
+
+        [Fact]
+        public void MaxpTable_NumGlyphs_Should_Match_GlyphTypeface_GlyphCount()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var maxpTable = MaxpTable.Load(typeface);
+
+            Assert.Equal(maxpTable.NumGlyphs, typeface.GlyphCount);
+        }
+
+        private class CustomPlatformTypeface : IPlatformTypeface
+        {
+            private readonly UnmanagedFontMemory _fontMemory;
+
+            public CustomPlatformTypeface(Stream stream, string fontFamily = "Custom")
+            {
+                _fontMemory = UnmanagedFontMemory.LoadFromStream(stream);
+                FamilyName = fontFamily;
+            }
+
+            public FontWeight Weight => FontWeight.Normal;
+
+            public FontStyle Style => FontStyle.Normal;
+
+            public FontStretch Stretch => FontStretch.Normal;
+
+            public string FamilyName { get; }
+
+            public FontSimulations FontSimulations => FontSimulations.None;
+
+            public void Dispose()
+            {
+                ((IDisposable)_fontMemory).Dispose();
+            }
+
+            public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream)
+            {
+                var memory = _fontMemory.Memory;
+
+                var handle = memory.Pin();
+                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);
+        }
+    }
+}

+ 233 - 0
tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/OS2TableTests.cs

@@ -0,0 +1,233 @@
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.Media.Fonts.Tables;
+using Avalonia.Platform;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Media.Fonts.Tables
+{
+    public class OS2TableTests
+    {
+        private static string s_InterFontUri = "resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests";
+
+        [Fact]
+        public void Should_Load_OS2Table_From_Inter_Font()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var loaded = OS2Table.TryLoad(typeface, out var os2Table);
+
+            Assert.True(loaded);
+        }
+
+        [Fact]
+        public void OS2Table_Should_Have_Valid_WeightClass()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var loaded = OS2Table.TryLoad(typeface, out var os2Table);
+
+            Assert.True(loaded);
+            Assert.Equal(400, os2Table.WeightClass);
+        }
+
+        [Fact]
+        public void OS2Table_Should_Have_Valid_WidthClass()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var loaded = OS2Table.TryLoad(typeface, out var os2Table);
+
+            Assert.True(loaded);
+            Assert.Equal(5, os2Table.WidthClass);
+        }
+
+        [Fact]
+        public void OS2Table_Should_Have_Valid_TypoMetrics()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var loaded = OS2Table.TryLoad(typeface, out var os2Table);
+
+            Assert.True(loaded);
+            Assert.Equal(2728, os2Table.TypoAscender);
+            Assert.Equal(-680, os2Table.TypoDescender);
+            Assert.True(os2Table.TypoAscender > os2Table.TypoDescender);
+        }
+
+        [Fact]
+        public void OS2Table_Should_Have_Valid_WinMetrics()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var loaded = OS2Table.TryLoad(typeface, out var os2Table);
+
+            Assert.True(loaded);
+            Assert.Equal(2728, os2Table.WinAscent);
+            Assert.Equal(680, os2Table.WinDescent);
+        }
+
+        [Fact]
+        public void OS2Table_Should_Have_Valid_StrikeoutMetrics()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var loaded = OS2Table.TryLoad(typeface, out var os2Table);
+
+            Assert.True(loaded);
+            Assert.Equal(192, os2Table.StrikeoutSize);
+        }
+
+        [Fact]
+        public void OS2Table_Inter_Regular_Should_Be_Regular()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var loaded = OS2Table.TryLoad(typeface, out var os2Table);
+
+            Assert.True(loaded);
+            Assert.True(os2Table.Selection.HasFlag(OS2Table.FontSelectionFlags.REGULAR));
+        }
+
+        [Fact]
+        public void OS2Table_Should_Have_Consistent_Ascent_Values()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var loaded = OS2Table.TryLoad(typeface, out var os2Table);
+
+            Assert.True(loaded);
+            Assert.Equal(2728, os2Table.TypoAscender);
+            Assert.Equal(2728, os2Table.WinAscent);
+        }
+
+        [Fact]
+        public void OS2Table_Should_Have_Consistent_Descent_Values()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var loaded = OS2Table.TryLoad(typeface, out var os2Table);
+
+            Assert.True(loaded);
+            Assert.Equal(-680, os2Table.TypoDescender);
+            Assert.Equal(680, os2Table.WinDescent);
+        }
+
+        [Fact]
+        public void OS2Table_Should_Have_Valid_Panose()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var loaded = OS2Table.TryLoad(typeface, out var os2Table);
+
+            Assert.True(loaded);
+            var panose = os2Table.Panose;
+            Assert.Equal(PanoseFamilyKind.LatinText, panose.FamilyKind);
+        }
+
+        private class CustomPlatformTypeface : IPlatformTypeface
+        {
+            private readonly UnmanagedFontMemory _fontMemory;
+
+            public CustomPlatformTypeface(Stream stream, string fontFamily = "Custom")
+            {
+                _fontMemory = UnmanagedFontMemory.LoadFromStream(stream);
+                FamilyName = fontFamily;
+            }
+
+            public FontWeight Weight => FontWeight.Normal;
+
+            public FontStyle Style => FontStyle.Normal;
+
+            public FontStretch Stretch => FontStretch.Normal;
+
+            public string FamilyName { get; }
+
+            public FontSimulations FontSimulations => FontSimulations.None;
+
+            public void Dispose()
+            {
+                ((IDisposable)_fontMemory).Dispose();
+            }
+
+            public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream)
+            {
+                var memory = _fontMemory.Memory;
+
+                var handle = memory.Pin();
+                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);
+        }
+    }
+}

+ 185 - 0
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 { }
+                }
+            }
+        }
+    }
+}

+ 1 - 3
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]);
                 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()
         private static IDisposable Start()

+ 446 - 0
tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs

@@ -0,0 +1,446 @@
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+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.ContainsGlyph('A'));
+            Assert.True(map['A'] != 0);
+
+            Assert.True(map.ContainsGlyph('a'));
+            Assert.True(map['a'] != 0);
+
+            Assert.True(map.ContainsGlyph(' '));
+            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.ContainsGlyph('A'));
+
+            var glyphId = map['A'];
+
+            // Ensure metrics are available for this glyph
+            Assert.True(typeface.TryGetGlyphMetrics(glyphId, out var metrics));
+
+            // Ensure advance can be retrieved
+            Assert.True(typeface.TryGetHorizontalGlyphAdvance(glyphId, out var advance));
+
+            // Advance returned by GetGlyphAdvance should match the metrics width
+            Assert.Equal(metrics.Width, advance);
+        }
+
+        [Fact]
+        public void Should_Have_Valid_FontMetrics()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var metrics = typeface.Metrics;
+
+            Assert.True(metrics.DesignEmHeight > 0);
+            Assert.True(metrics.Ascent != 0);
+            Assert.True(metrics.Descent != 0);
+            Assert.True(metrics.LineSpacing > 0);
+        }
+
+        [Fact]
+        public void Should_Have_Positive_GlyphCount()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(typeface.GlyphCount > 0);
+        }
+
+        [Fact]
+        public void Should_Have_Correct_Font_Properties()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.Equal(FontWeight.Normal, typeface.Weight);
+            Assert.Equal(FontStyle.Normal, typeface.Style);
+            Assert.Equal(FontStretch.Normal, typeface.Stretch);
+            Assert.Equal(FontSimulations.None, typeface.FontSimulations);
+        }
+
+        [Fact]
+        public void Should_Apply_Bold_Simulation()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream), FontSimulations.Bold);
+
+            Assert.Equal(FontWeight.Bold, typeface.Weight);
+            Assert.Equal(FontSimulations.Bold, typeface.FontSimulations);
+        }
+
+        [Fact]
+        public void Should_Apply_Oblique_Simulation()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream), FontSimulations.Oblique);
+
+            Assert.Equal(FontStyle.Italic, typeface.Style);
+            Assert.Equal(FontSimulations.Oblique, typeface.FontSimulations);
+        }
+
+        [Fact]
+        public void Should_Apply_Combined_Simulations()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream), 
+                FontSimulations.Bold | FontSimulations.Oblique);
+
+            Assert.Equal(FontWeight.Bold, typeface.Weight);
+            Assert.Equal(FontStyle.Italic, typeface.Style);
+            Assert.Equal(FontSimulations.Bold | FontSimulations.Oblique, typeface.FontSimulations);
+        }
+
+        [Fact]
+        public void Should_Have_TypographicFamilyName()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.NotNull(typeface.TypographicFamilyName);
+        }
+
+        [Fact]
+        public void Should_Have_FamilyNames_Dictionary()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.NotNull(typeface.FamilyNames);
+            Assert.NotEmpty(typeface.FamilyNames);
+        }
+
+        [Fact]
+        public void Should_Have_FaceNames_Dictionary()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.NotNull(typeface.FaceNames);
+            Assert.NotEmpty(typeface.FaceNames);
+        }
+
+        [Fact]
+        public void Should_Have_SupportedFeatures()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var features = typeface.SupportedFeatures;
+
+            Assert.NotEmpty(features);
+        }
+
+        [Fact]
+        public void Should_Cache_SupportedFeatures()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var features1 = typeface.SupportedFeatures;
+            var features2 = typeface.SupportedFeatures;
+
+            Assert.Same(features1, features2);
+        }
+
+        [Fact]
+        public void TryGetGlyphAdvance_Should_Return_False_For_Invalid_GlyphId()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.False(typeface.TryGetHorizontalGlyphAdvance(ushort.MaxValue, out var advance));
+        }
+
+        [Fact]
+        public void TryGetGlyphMetrics_Should_Return_False_For_Invalid_GlyphId()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var result = typeface.TryGetGlyphMetrics(ushort.MaxValue, out var metrics);
+
+            Assert.False(result);
+            Assert.Equal(default, metrics);
+        }
+
+        [Fact]
+        public void TryGetGlyphMetrics_Should_Return_Valid_Metrics()
+        {
+            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.ContainsGlyph('A'));
+
+            var glyphId = map['A'];
+            var result = typeface.TryGetGlyphMetrics(glyphId, out var metrics);
+
+            Assert.True(result);
+            Assert.True(metrics.Width > 0);
+        }
+
+        [Fact]
+        public void Should_Have_Valid_PlatformTypeface()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var platformTypeface = new CustomPlatformTypeface(stream);
+            var typeface = new GlyphTypeface(platformTypeface);
+
+            Assert.NotNull(typeface.PlatformTypeface);
+            Assert.Same(platformTypeface, typeface.PlatformTypeface);
+        }
+
+        [Fact]
+        public void Should_Dispose_Properly()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            typeface.Dispose();
+
+            // Should not throw on double dispose
+            typeface.Dispose();
+        }
+
+        [Fact]
+        public void CharacterToGlyphMap_Should_Have_Different_Glyphs_For_Different_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.True(map.ContainsGlyph('A'));
+            Assert.True(map.ContainsGlyph('B'));
+
+            var glyphA = map['A'];
+            var glyphB = map['B'];
+
+            Assert.NotEqual(glyphA, glyphB);
+        }
+
+        [Fact]
+        public void FontMetrics_LineSpacing_Should_Be_Calculated_Correctly()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var metrics = typeface.Metrics;
+
+            var expectedLineSpacing = metrics.Descent - metrics.Ascent + metrics.LineGap;
+
+            Assert.Equal(expectedLineSpacing, metrics.LineSpacing);
+        }
+
+        [Fact]
+        public void Should_Support_Multiple_Characters_In_CharacterToGlyphMap()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            var map = typeface.CharacterToGlyphMap;
+
+            var testCharacters = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
+
+            foreach (var ch in testCharacters)
+            {
+                Assert.True(map.ContainsGlyph(ch), $"Character '{ch}' not found in glyph map");
+            }
+        }
+
+        [Fact]
+        public void FamilyNames_Should_Contain_InvariantCulture_Entry()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(typeface.FamilyNames.ContainsKey(CultureInfo.InvariantCulture) || 
+                       typeface.FamilyNames.Count > 0);
+        }
+
+        [Fact]
+        public void FaceNames_Should_Contain_InvariantCulture_Entry()
+        {
+            var assetLoader = new StandardAssetLoader();
+
+            using var stream = assetLoader.Open(new Uri(s_InterFontUri));
+
+            var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
+
+            Assert.True(typeface.FaceNames.ContainsKey(CultureInfo.InvariantCulture) || 
+                       typeface.FaceNames.Count > 0);
+        }
+
+        private class CustomPlatformTypeface : IPlatformTypeface
+        {
+            private readonly UnmanagedFontMemory _fontMemory;
+
+            public CustomPlatformTypeface(Stream stream, string fontFamily = "Custom")
+            {
+                _fontMemory = UnmanagedFontMemory.LoadFromStream(stream);
+                FamilyName = fontFamily;
+            }
+
+            public FontWeight Weight => FontWeight.Normal;
+
+            public FontStyle Style => FontStyle.Normal;
+
+            public FontStretch Stretch => FontStretch.Normal;
+
+            public string FamilyName { get; }
+
+            public FontSimulations FontSimulations => FontSimulations.None;
+
+            public void Dispose()
+            {
+                ((IDisposable)_fontMemory).Dispose();
+            }
+
+            public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream)
+            {
+                var memory = _fontMemory.Memory;
+
+                var handle = memory.Pin();
+                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 - 0
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@@ -11,6 +11,7 @@
     <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.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="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
     <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
     <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
   </ItemGroup>
   </ItemGroup>

+ 2 - 1
tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs

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

+ 4 - 3
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@@ -10,6 +10,7 @@ using Xunit;
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
 using System.Reactive.Subjects;
 using System.Reactive.Subjects;
 using Avalonia.Headless;
 using Avalonia.Headless;
+using Avalonia.Harfbuzz;
 using Avalonia.Input;
 using Avalonia.Input;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Moq;
 using Moq;
@@ -367,7 +368,7 @@ namespace Avalonia.Controls.UnitTests
                 Assert.Equal(textbox.Text, control.Text);
                 Assert.Equal(textbox.Text, control.Text);
             });
             });
         }
         }
-        
+
         [Fact]
         [Fact]
         public void Custom_TextSelector()
         public void Custom_TextSelector()
         {
         {
@@ -386,7 +387,7 @@ namespace Avalonia.Controls.UnitTests
                 Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString()));
                 Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString()));
             });
             });
         }
         }
-        
+
         [Fact]
         [Fact]
         public void Custom_ItemSelector()
         public void Custom_ItemSelector()
         {
         {
@@ -1272,7 +1273,7 @@ namespace Avalonia.Controls.UnitTests
             keyboardNavigation: () => new KeyboardNavigationHandler(),
             keyboardNavigation: () => new KeyboardNavigationHandler(),
             inputManager: new InputManager(),
             inputManager: new InputManager(),
             standardCursorFactory: Mock.Of<ICursorFactory>(),
             standardCursorFactory: Mock.Of<ICursorFactory>(),
-            textShaperImpl: new HeadlessTextShaperStub(),
+            textShaperImpl: new HarfBuzzTextShaper(),
             fontManagerImpl: new HeadlessFontManagerStub());
             fontManagerImpl: new HeadlessFontManagerStub());
 
 
         private class TestContextMenu : ContextMenu
         private class TestContextMenu : ContextMenu

+ 1 - 0
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@@ -15,6 +15,7 @@
   <Import Project="..\..\build\HarfBuzzSharp.props" />
   <Import Project="..\..\build\HarfBuzzSharp.props" />
   <Import Project="..\..\build\NullableEnable.props" />
   <Import Project="..\..\build\NullableEnable.props" />
   <ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\src\HarfBuzz\Avalonia.HarfBuzz\Avalonia.HarfBuzz.csproj" />
     <ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.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.Xaml\Avalonia.Markup.Xaml.csproj" />
     <ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
     <ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />

+ 2 - 1
tests/Avalonia.Controls.UnitTests/DatePickerTests.cs

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

+ 2 - 0
tests/Avalonia.Controls.UnitTests/GridTests.cs

@@ -1817,6 +1817,7 @@ namespace Avalonia.Controls.UnitTests
                         [Grid.ColumnSpanProperty] = 3,
                         [Grid.ColumnSpanProperty] = 3,
                         Content = new TextBlock()
                         Content = new TextBlock()
                         {
                         {
+                            FontSize = 10,
                             Text = @"0: 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890
                             Text = @"0: 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890
 1: 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890
 1: 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890
 2: 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890
 2: 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890
@@ -1877,6 +1878,7 @@ namespace Avalonia.Controls.UnitTests
                     {
                     {
                         [Grid.ColumnProperty] = 1,
                         [Grid.ColumnProperty] = 1,
                         Height = 20,
                         Height = 20,
+                        Width = 100,
                         Text="1234567890"
                         Text="1234567890"
                     }
                     }
                 }
                 }

+ 2 - 1
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

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

+ 2 - 2
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@@ -1216,10 +1216,10 @@ namespace Avalonia.Controls.UnitTests
 
 
             var panel = Assert.IsType<VirtualizingStackPanel>(target.ItemsPanelRoot);
             var panel = Assert.IsType<VirtualizingStackPanel>(target.ItemsPanelRoot);
             Assert.Equal(0, panel.FirstRealizedIndex);
             Assert.Equal(0, panel.FirstRealizedIndex);
-            Assert.Equal(9, panel.LastRealizedIndex);
+            Assert.Equal(6, panel.LastRealizedIndex);
 
 
             Assert.Equal(
             Assert.Equal(
-                Enumerable.Range(0, 10).Select(x => $"Item{x}"),
+                Enumerable.Range(0, 7).Select(x => $"Item{x}"),
                 data.GetRealizedItems());
                 data.GetRealizedItems());
         }
         }
 
 

+ 4 - 3
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

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

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