From 628d2e4ff89e4b5b17526b6a88ce6d7aafabeb96 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 24 Oct 2025 12:55:04 +0200 Subject: [PATCH 1/5] Introduce a universal IGlyphTypeface implementation that does not rely on any platform implementation --- Avalonia.sln | 13 +- api/Avalonia.nupkg.xml | 378 ++++++++++++++++ samples/RenderDemo/Pages/CustomSkiaPage.cs | 4 +- samples/RenderDemo/Pages/GlyphRunPage.xaml.cs | 4 +- src/Avalonia.Base/Media/FontManager.cs | 11 +- .../Media/Fonts/EmbeddedFontCollection.cs | 19 +- .../Media/Fonts/FontCollectionBase.cs | 21 +- src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs | 2 +- .../Media/Fonts/SystemFontCollection.cs | 41 +- .../Fonts/Tables/BigEndianBinaryReader.cs | 336 ++++++--------- .../Media/Fonts/Tables/Cmap/CmapEncoding.cs | 34 ++ .../Media/Fonts/Tables/Cmap/CmapFormat.cs | 16 + .../Fonts/Tables/Cmap/CmapFormat12Table.cs | 183 ++++++++ .../Fonts/Tables/Cmap/CmapFormat4Table.cs | 316 ++++++++++++++ .../Fonts/Tables/Cmap/CmapSubtableEntry.cs | 42 ++ .../Media/Fonts/Tables/Cmap/CmapTable.cs | 166 +++++++ .../Media/Fonts/Tables/FeatureListTable.cs | 21 +- .../Media/Fonts/Tables/HeadTable.cs | 118 +++++ ...lHeadTable.cs => HorizontalHeaderTable.cs} | 57 ++- .../Media/Fonts/Tables/MaxpTable.cs | 37 ++ .../Tables/Metrics/HorizontalGlyphMetric.cs | 26 ++ .../Tables/Metrics/HorizontalMetricsTable.cs | 113 +++++ .../Tables/Metrics/VerticalGlyphMetric.cs | 24 ++ .../Tables/Metrics/VerticalMetricsTable.cs | 110 +++++ .../Media/Fonts/Tables/Name/NameRecord.cs | 54 ++- .../Media/Fonts/Tables/Name/NameTable.cs | 51 +-- .../Media/Fonts/Tables/OS2Table.cs | 31 +- .../Tables/{PlatformIDs.cs => PlatformID.cs} | 2 +- .../Media/Fonts/Tables/PostTable.cs | 46 ++ .../Media/Fonts/Tables/StringLoader.cs | 38 -- .../Media/Fonts/Tables/VerticalHeaderTable.cs | 128 ++++++ .../Media/Fonts/UnmanagedFontMemory.cs | 407 ++++++++++++++++++ src/Avalonia.Base/Media/GlyphMetrics.cs | 6 +- src/Avalonia.Base/Media/GlyphRun.cs | 13 +- src/Avalonia.Base/Media/GlyphTypeface.cs | 395 +++++++++++++++++ src/Avalonia.Base/Media/IGlyphTypeface.cs | 169 +++++--- src/Avalonia.Base/Media/IGlyphTypeface2.cs | 39 -- .../Media/TextFormatting/TextCharacters.cs | 10 +- .../Media/TextFormatting/TextFormatterImpl.cs | 2 +- .../Platform/IFontManagerImpl.cs | 8 +- src/Avalonia.Base/Platform/IGlyphRunImpl.cs | 4 - src/Avalonia.Base/Platform/ITextShaperImpl.cs | 11 +- .../Server/DiagnosticTextRenderer.cs | 7 +- .../AppBuilderDesktopExtensions.cs | 9 +- src/Avalonia.Desktop/Avalonia.Desktop.csproj | 1 + .../Avalonia.Harfbuzz.csproj | 22 + .../HarfBuzzApplicationExtensions.cs | 27 ++ .../Avalonia.HarfBuzz/HarfBuzzTextShaper.cs} | 34 +- .../Avalonia.HarfBuzz/HarfBuzzTypeface.cs | 67 +++ .../Avalonia.Headless.csproj | 1 + .../HeadlessPlatformRenderInterface.cs | 7 +- .../HeadlessPlatformStubs.cs | 367 +++++++--------- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 24 +- src/Skia/Avalonia.Skia/GlyphRunImpl.cs | 6 +- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 393 ----------------- .../Avalonia.Skia/PlatformRenderInterface.cs | 10 +- src/Skia/Avalonia.Skia/SkiaPlatform.cs | 3 +- src/Skia/Avalonia.Skia/SkiaTypeface.cs | 81 ++++ .../Avalonia.Direct2D1/Direct2D1Platform.cs | 7 +- .../Media/DWriteTypeface.cs | 111 +++++ .../Media/FontManagerImpl.cs | 17 +- .../Avalonia.Direct2D1/Media/GlyphRunImpl.cs | 10 +- .../Media/GlyphTypefaceImpl.cs | 216 ---------- .../Media/TextShaperImpl.cs | 204 --------- .../Media/FontManagerTests.cs | 3 +- .../Media/Fonts/UnmanagedFontMemoryTests.cs | 185 ++++++++ .../Media/GlyphRunTests.cs | 4 +- .../Media/GlyphTypefaceTests.cs | 131 ++++++ .../Avalonia.Benchmarks.csproj | 1 + .../Text/HugeTextLayout.cs | 3 +- .../AutoCompleteBoxTests.cs | 32 +- .../DatePickerTests.cs | 3 +- .../ItemsControlTests.cs | 3 +- .../ListBoxTests.cs | 19 +- .../MaskedTextBoxTests.cs | 20 +- .../SelectingItemsControlTests_Multiple.cs | 11 +- .../TextBlockTests.cs | 28 +- .../TextBoxTests.cs | 5 +- .../TextBoxTests_DataValidation.cs | 3 +- .../TimePickerTests.cs | 3 +- .../TransitioningContentControlTests.cs | 3 +- .../TreeViewTests.cs | 5 +- .../Avalonia.Headless.NUnit.UnitTests.csproj | 1 + .../TestApplication.cs | 4 +- .../Avalonia.Headless.XUnit.UnitTests.csproj | 1 + .../Avalonia.Markup.Xaml.UnitTests.csproj | 1 + .../MarkupExtensions/BindingExtensionTests.cs | 3 +- .../DynamicResourceExtensionTests.cs | 3 +- .../StaticResourceExtensionTests.cs | 1 - .../Assets/NotoSansTamil-Regular.ttf | Bin 0 -> 78988 bytes .../Media/GlyphRunTests.cs | 6 +- .../Avalonia.RenderTests/TestRenderHelper.cs | 3 + .../Media/CustomFontManagerImpl.cs | 31 +- .../Media/EmbeddedFontCollectionTests.cs | 6 +- .../Media/FontCollectionTests.cs | 2 +- .../Media/FontManagerTests.cs | 8 +- .../Media/GlyphRunTests.cs | 1 - .../TextFormatting/Tables/CmapTableTests.cs | 113 +++++ .../TextFormatting/TextFormatterTests.cs | 5 +- .../Media/TextFormatting/TextLayoutTests.cs | 10 +- .../Media/TextFormatting/TextLineTests.cs | 1 - .../Media/TextFormatting/TextShaperTests.cs | 1 - .../Avalonia.UnitTests.csproj | 1 + .../HarfBuzzGlyphTypefaceImpl.cs | 189 -------- .../HarfBuzzTextShaperImpl.cs | 180 -------- ...agerImpl.cs => HeadlessFontManagerImpl.cs} | 33 +- tests/Avalonia.UnitTests/TestServices.cs | 30 +- .../Should_Draw_TextDecorations.expected.png | Bin 1376 -> 1457 bytes 108 files changed, 4152 insertions(+), 2074 deletions(-) create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapEncoding.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat4Table.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapSubtableEntry.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/HeadTable.cs rename src/Avalonia.Base/Media/Fonts/Tables/{HorizontalHeadTable.cs => HorizontalHeaderTable.cs} (80%) create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/MaxpTable.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalGlyphMetric.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalMetricsTable.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalGlyphMetric.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalMetricsTable.cs rename src/Avalonia.Base/Media/Fonts/Tables/{PlatformIDs.cs => PlatformID.cs} (95%) create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/PostTable.cs delete mode 100644 src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/VerticalHeaderTable.cs create mode 100644 src/Avalonia.Base/Media/Fonts/UnmanagedFontMemory.cs create mode 100644 src/Avalonia.Base/Media/GlyphTypeface.cs delete mode 100644 src/Avalonia.Base/Media/IGlyphTypeface2.cs create mode 100644 src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.Harfbuzz.csproj create mode 100644 src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzApplicationExtensions.cs rename src/{Skia/Avalonia.Skia/TextShaperImpl.cs => HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTextShaper.cs} (88%) create mode 100644 src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTypeface.cs delete mode 100644 src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs create mode 100644 src/Skia/Avalonia.Skia/SkiaTypeface.cs create mode 100644 src/Windows/Avalonia.Direct2D1/Media/DWriteTypeface.cs delete mode 100644 src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs delete mode 100644 src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs create mode 100644 tests/Avalonia.Base.UnitTests/Media/Fonts/UnmanagedFontMemoryTests.cs create mode 100644 tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs create mode 100644 tests/Avalonia.RenderTests/Assets/NotoSansTamil-Regular.ttf create mode 100644 tests/Avalonia.Skia.UnitTests/Media/TextFormatting/Tables/CmapTableTests.cs delete mode 100644 tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs delete mode 100644 tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs rename tests/Avalonia.UnitTests/{HarfBuzzFontManagerImpl.cs => HeadlessFontManagerImpl.cs} (67%) diff --git a/Avalonia.sln b/Avalonia.sln index 5dfd11b6719..ee00694efc7 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -42,8 +42,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs - src\Shared\StringCompatibilityExtensions.cs = src\Shared\StringCompatibilityExtensions.cs src\Shared\StreamCompatibilityExtensions.cs = src\Shared\StreamCompatibilityExtensions.cs + src\Shared\StringCompatibilityExtensions.cs = src\Shared\StringCompatibilityExtensions.cs EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}" @@ -92,6 +92,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}" ProjectSection(SolutionItems) = preProject + build\AnalyzerProject.targets = build\AnalyzerProject.targets build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props build\Base.props = build\Base.props build\Binding.props = build\Binding.props @@ -122,7 +123,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\UnitTests.NetFX.props = build\UnitTests.NetFX.props build\WarnAsErrors.props = build\WarnAsErrors.props build\XUnit.props = build\XUnit.props - build\AnalyzerProject.targets = build\AnalyzerProject.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}" @@ -302,6 +302,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.MacCatalyst" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.tvOS", "samples\ControlCatalog.tvOS\ControlCatalog.tvOS.csproj", "{14342787-B4EF-4076-8C91-BA6C523DE8DF}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Harfbuzz", "Harfbuzz", "{7670D720-6E84-4AFC-8331-A5C399481905}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Harfbuzz", "src\Harfbuzz\Avalonia.Harfbuzz\Avalonia.Harfbuzz.csproj", "{E2BFA463-6402-4EF8-8945-FD9A10A914D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -704,6 +708,10 @@ Global {14342787-B4EF-4076-8C91-BA6C523DE8DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {14342787-B4EF-4076-8C91-BA6C523DE8DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {14342787-B4EF-4076-8C91-BA6C523DE8DF}.Release|Any CPU.Build.0 = Release|Any CPU + {E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -793,6 +801,7 @@ Global {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4} = {9B9E3891-2366-4253-A952-D08BCEB71098} {DE3C28DD-B602-4750-831D-345102A54CA0} = {9B9E3891-2366-4253-A952-D08BCEB71098} {14342787-B4EF-4076-8C91-BA6C523DE8DF} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {E2BFA463-6402-4EF8-8945-FD9A10A914D1} = {7670D720-6E84-4AFC-8331-A5C399481905} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 5d8cdee5070..11633167a2b 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1,6 +1,168 @@ + + CP0002 + M:Avalonia.Media.IGlyphTypeface.get_GlyphCount + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyph(System.UInt32) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvances(System.ReadOnlySpan{System.UInt16}) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyphs(System.ReadOnlySpan{System.UInt32}) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.TryGetGlyph(System.UInt32,System.UInt16@) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.TryGetTable(System.UInt32,System.Byte[]@) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGlyphRunImpl.get_GlyphTypeface + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.get_GlyphCount + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyph(System.UInt32) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvances(System.ReadOnlySpan{System.UInt16}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyphs(System.ReadOnlySpan{System.UInt32}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.TryGetGlyph(System.UInt32,System.UInt16@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.TryGetTable(System.UInt32,System.Byte[]@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGlyphRunImpl.get_GlyphTypeface + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.get_GlyphCount + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyph(System.UInt32) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvances(System.ReadOnlySpan{System.UInt16}) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.GetGlyphs(System.ReadOnlySpan{System.UInt32}) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.TryGetGlyph(System.UInt32,System.UInt16@) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IGlyphTypeface.TryGetTable(System.UInt32,System.Byte[]@) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGlyphRunImpl.get_GlyphTypeface + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + CP0006 M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer) @@ -25,12 +187,84 @@ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll current/Avalonia/lib/net6.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryGetFamilyTypefaces(System.String,System.Collections.Generic.IReadOnlyList{Avalonia.Media.Typeface}@) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.IPlatformTypeface@) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64) baseline/Avalonia/lib/net6.0/Avalonia.Base.dll current/Avalonia/lib/net6.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.ITextShaperImpl.CreateTypeface(Avalonia.Media.IGlyphTypeface) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.CharacterToGlyphMap + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.FaceNames + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.FamilyNames + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.GlyphCount + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.PlatformTypeface + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.SupportedFeatures + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.TextShaperTypeface + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.TypographicFamilyName + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + CP0006 M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) @@ -73,12 +307,84 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryGetFamilyTypefaces(System.String,System.Collections.Generic.IReadOnlyList{Avalonia.Media.Typeface}@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.IPlatformTypeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.ITextShaperImpl.CreateTypeface(Avalonia.Media.IGlyphTypeface) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.CharacterToGlyphMap + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.FaceNames + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.FamilyNames + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.GlyphCount + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.PlatformTypeface + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.SupportedFeatures + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.TextShaperTypeface + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.TypographicFamilyName + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) @@ -121,12 +427,84 @@ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryGetFamilyTypefaces(System.String,System.Collections.Generic.IReadOnlyList{Avalonia.Media.Typeface}@) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.IPlatformTypeface@) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64) baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.ITextShaperImpl.CreateTypeface(Avalonia.Media.IGlyphTypeface) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.CharacterToGlyphMap + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.FaceNames + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.FamilyNames + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.GlyphCount + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.PlatformTypeface + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.SupportedFeatures + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.TextShaperTypeface + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.TypographicFamilyName + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + CP0006 M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index 85dc50aadcd..fe1636ba2ae 100644 --- a/samples/RenderDemo/Pages/CustomSkiaPage.cs +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Globalization; using System.Linq; using Avalonia; using Avalonia.Controls; @@ -9,7 +8,6 @@ using Avalonia.Rendering.SceneGraph; using Avalonia.Skia; using Avalonia.Threading; -using Avalonia.Utilities; using SkiaSharp; namespace RenderDemo.Pages @@ -21,7 +19,7 @@ public CustomSkiaPage() { ClipToBounds = true; var text = "Current rendering API is not Skia"; - var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.GetGlyph(ch)).ToArray(); + var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.CharacterToGlyphMap[ch]).ToArray(); _noSkia = new GlyphRun(Typeface.Default.GlyphTypeface, 12, text.AsMemory(), glyphs); } diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs index 5c31e138e0a..a94ec5e2621 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs @@ -69,7 +69,7 @@ public override void Render(DrawingContext context) _fontSize += _direction; - _glyphIndices[0] = _glyphTypeface.GetGlyph(c); + _glyphIndices[0] = _glyphTypeface.CharacterToGlyphMap[c]; _characters[0] = c; @@ -128,7 +128,7 @@ public override void Render(DrawingContext context) _fontSize += _direction; - _glyphIndices[0] = _glyphTypeface.GetGlyph(c); + _glyphIndices[0] = _glyphTypeface.CharacterToGlyphMap[c]; _characters[0] = c; diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index c8d8042e83f..9c0cede7de1 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -262,7 +262,7 @@ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fon { typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); - if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.CharacterToGlyphMap.TryGetValue(codepoint, out _)) { return true; } @@ -289,6 +289,11 @@ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fon if (TryGetFontCollection(source, out var fontCollection) && fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) { + if (typeface.FontFamily.Name == DefaultFontFamily.Name && i + 1 < compositeKey.Keys.Count) + { + continue; + } + return true; } } @@ -306,8 +311,8 @@ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fon } } - //Try to find a match with the system font manager - return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface); + //Try to find a match with the system fonts + return SystemFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily?.Name, culture, out typeface); } internal IReadOnlyList GetFamilyTypefaces(FontFamily fontFamily) diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index 1d706e9360d..c59fd794dc8 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -105,22 +105,15 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon private void AddGlyphTypeface(IGlyphTypeface glyphTypeface) { - if (glyphTypeface is IGlyphTypeface2 glyphTypeface2) + //Add the TypographicFamilyName to the cache + if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName)) { - //Add the TypographicFamilyName to the cache - if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) - { - AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, glyphTypeface); - } - - foreach (var kvp in glyphTypeface2.FamilyNames) - { - AddGlyphTypefaceByFamilyName(kvp.Value, glyphTypeface); - } + AddGlyphTypefaceByFamilyName(glyphTypeface.TypographicFamilyName, glyphTypeface); } - else + + foreach (var kvp in glyphTypeface.FamilyNames) { - AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface); + AddGlyphTypefaceByFamilyName(kvp.Value, glyphTypeface); } return; diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index 222a514ed13..eade5855a24 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -33,7 +33,7 @@ public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight { if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface)) { - if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + if (glyphTypeface.CharacterToGlyphMap.TryGetValue(codepoint, out _)) { match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), style, weight, stretch); @@ -55,7 +55,7 @@ public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface)) { - if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + if (glyphTypeface.CharacterToGlyphMap.TryGetValue(codepoint, out _)) { match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), style, weight, stretch); @@ -94,36 +94,31 @@ public virtual bool TryCreateSyntheticGlyphTypeface( return false; } - if (glyphTypeface is not IGlyphTypeface2 glyphTypeface2) - { - return false; - } - var fontSimulations = FontSimulations.None; - if (style != FontStyle.Normal && glyphTypeface2.Style != style) + if (style != FontStyle.Normal && glyphTypeface.Style != style) { fontSimulations |= FontSimulations.Oblique; } - if ((int)weight >= 600 && glyphTypeface2.Weight < weight) + if ((int)weight >= 600 && glyphTypeface.Weight < weight) { fontSimulations |= FontSimulations.Bold; } - if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream)) + if (fontSimulations != FontSimulations.None && glyphTypeface.PlatformTypeface.TryGetStream(out var stream)) { using (stream) { if (fontManager.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface)) { //Add the TypographicFamilyName to the cache - if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) + if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName)) { - AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, syntheticGlyphTypeface); + AddGlyphTypefaceByFamilyName(glyphTypeface.TypographicFamilyName, syntheticGlyphTypeface); } - foreach (var kvp in glyphTypeface2.FamilyNames) + foreach (var kvp in glyphTypeface.FamilyNames) { AddGlyphTypefaceByFamilyName(kvp.Value, syntheticGlyphTypeface); } diff --git a/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs b/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs index b0c725ca92e..6988c57bce1 100644 --- a/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs +++ b/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs @@ -2,7 +2,7 @@ namespace Avalonia.Media.Fonts { - internal readonly record struct OpenTypeTag + public readonly record struct OpenTypeTag { public static readonly OpenTypeTag None = new OpenTypeTag(0, 0, 0, 0); public static readonly OpenTypeTag Max = new OpenTypeTag(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index 3a98a30b90f..3c194c9d92d 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using Avalonia.Platform; @@ -135,9 +136,9 @@ private void LoadGlyphTypefaces(IFontManagerImpl fontManager, Uri source) } //Add TypographicFamilyName to the cache - if (glyphTypeface is IGlyphTypeface2 glyphTypeface2 && !string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) + if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName)) { - AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, glyphTypeface); + AddGlyphTypefaceByFamilyName(glyphTypeface.TypographicFamilyName, glyphTypeface); } AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface); @@ -161,13 +162,41 @@ void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypefac } } - public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) => + _fontManager.PlatformImpl.TryGetFamilyTypefaces(familyName, out familyTypefaces); + + public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName, CultureInfo? culture, out Typeface typeface) { - familyTypefaces = null; + if (base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out typeface)) + { + return true; + } - if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2) + if (_fontManager.PlatformImpl.TryMatchCharacter(codepoint, style, weight, stretch, culture, out var match)) { - return fontManagerImpl2.TryGetFamilyTypefaces(familyName, out familyTypefaces); + var createdKey = + new FontCollectionKey(match.Style, match.Weight, match.Stretch); + + var glyphTypeface = new GlyphTypeface(match, FontSimulations.None); + + if (_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) + { + if (!glyphTypefaces.TryAdd(createdKey, glyphTypeface)) + { + return false; + } + } + else + { + if (!_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, new ConcurrentDictionary() { [createdKey] = glyphTypeface })) + { + return false; + } + } + + typeface = new Typeface(glyphTypeface.FamilyName, match.Style, match.Weight, match.Stretch); + + return true; } return false; diff --git a/src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs b/src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs index bca46b7e8c9..1f3e74e0386 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs @@ -5,104 +5,92 @@ using System; using System.Buffers.Binary; using System.Diagnostics; -using System.IO; using System.Runtime.CompilerServices; using System.Text; namespace Avalonia.Media.Fonts.Tables { /// - /// BinaryReader using big-endian encoding. + /// BinaryReader using big-endian encoding for ReadOnlySpan<byte>. /// - [DebuggerDisplay("Start: {StartOfStream}, Position: {BaseStream.Position}")] - internal class BigEndianBinaryReader : IDisposable + [DebuggerDisplay("Start: {StartOfSpan}, Position: {Position}")] + internal ref struct BigEndianBinaryReader { - /// - /// Buffer used for temporary storage before conversion into primitives - /// - private readonly byte[] _buffer = new byte[16]; - - private readonly bool _leaveOpen; + private readonly ReadOnlySpan _span; + private int _position; + private readonly int _startOfSpan; /// /// Initializes a new instance of the class. - /// Constructs a new binary reader with the given bit converter, reading - /// to the given stream, using the given encoding. /// - /// Stream to read data from - /// if set to true [leave open]. - public BigEndianBinaryReader(Stream stream, bool leaveOpen) + /// Span to read data from + public BigEndianBinaryReader(ReadOnlySpan span) { - BaseStream = stream; - StartOfStream = stream.Position; - _leaveOpen = leaveOpen; + _span = span; + _position = 0; + _startOfSpan = 0; } - private long StartOfStream { get; } + private readonly int StartOfSpan => _startOfSpan; /// - /// Gets the underlying stream of the EndianBinaryReader. + /// Gets the current position in the span. /// - public Stream BaseStream { get; } + public readonly int Position => _position; /// - /// Seeks within the stream. + /// Seeks within the span. /// /// Offset to seek to. - /// Origin of seek operation. If SeekOrigin.Begin, the offset will be set to the start of stream position. - 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; } - /// - /// Reads a single byte from the stream. - /// - /// The byte read public byte ReadByte() { - ReadInternal(_buffer, 1); - return _buffer[0]; + EnsureAvailable(1); + + return _span[_position++]; } - /// - /// Reads a single signed byte from the stream. - /// - /// The byte read public sbyte ReadSByte() { - ReadInternal(_buffer, 1); - return unchecked((sbyte)_buffer[0]); + EnsureAvailable(1); + + return unchecked((sbyte)_span[_position++]); } public float ReadF2dot14() { const float f2Dot14ToFloat = 16384.0f; + return ReadInt16() / f2Dot14ToFloat; } - /// - /// Reads a 16-bit signed integer from the stream, using the bit converter - /// for this reader. 2 bytes are read. - /// - /// The 16-bit integer read public short ReadInt16() { - ReadInternal(_buffer, 2); + EnsureAvailable(2); - return BinaryPrimitives.ReadInt16BigEndian(_buffer); + short value = BinaryPrimitives.ReadInt16BigEndian(_span.Slice(_position, 2)); + + _position += 2; + + return value; } public TEnum ReadInt16() where TEnum : struct, Enum { TryConvert(ReadUInt16(), out TEnum value); + return value; } @@ -112,77 +100,64 @@ public TEnum ReadInt16() public ushort ReadUFWORD() => ReadUInt16(); - /// - /// Reads a fixed 32-bit value from the stream. - /// 4 bytes are read. - /// - /// The 32-bit value read. 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; } - /// - /// Reads a 32-bit signed integer from the stream, using the bit converter - /// for this reader. 4 bytes are read. - /// - /// The 32-bit integer read public int ReadInt32() { - ReadInternal(_buffer, 4); + EnsureAvailable(4); + + int value = BinaryPrimitives.ReadInt32BigEndian(_span.Slice(_position, 4)); - return BinaryPrimitives.ReadInt32BigEndian(_buffer); + _position += 4; + + return value; } - /// - /// Reads a 64-bit signed integer from the stream. - /// 8 bytes are read. - /// - /// The 64-bit integer read. public long ReadInt64() { - ReadInternal(_buffer, 8); + EnsureAvailable(8); - return BinaryPrimitives.ReadInt64BigEndian(_buffer); + long value = BinaryPrimitives.ReadInt64BigEndian(_span.Slice(_position, 8)); + + _position += 8; + + return value; } - /// - /// Reads a 16-bit unsigned integer from the stream. - /// 2 bytes are read. - /// - /// The 16-bit unsigned integer read. public ushort ReadUInt16() { - ReadInternal(_buffer, 2); + EnsureAvailable(2); + + ushort value = BinaryPrimitives.ReadUInt16BigEndian(_span.Slice(_position, 2)); - return BinaryPrimitives.ReadUInt16BigEndian(_buffer); + _position += 2; + + return value; } - /// - /// Reads a 16-bit unsigned integer from the stream representing an offset position. - /// 2 bytes are read. - /// - /// The 16-bit unsigned integer read. public ushort ReadOffset16() => ReadUInt16(); public TEnum ReadUInt16() where TEnum : struct, Enum { TryConvert(ReadUInt16(), out TEnum value); + return value; } - /// - /// Reads array of 16-bit unsigned integers from the stream. - /// - /// The length. - /// - /// The 16-bit unsigned integer read. - /// public ushort[] ReadUInt16Array(int length) { ushort[] data = new ushort[length]; + for (int i = 0; i < length; i++) { data[i] = ReadUInt16(); @@ -191,10 +166,6 @@ public ushort[] ReadUInt16Array(int length) return data; } - /// - /// Reads array of 16-bit unsigned integers from the stream to the buffer. - /// - /// The buffer to read to. public void ReadUInt16Array(Span buffer) { for (int i = 0; i < buffer.Length; i++) @@ -203,16 +174,10 @@ public void ReadUInt16Array(Span buffer) } } - /// - /// Reads array or 32-bit unsigned integers from the stream. - /// - /// The length. - /// - /// The 32-bit unsigned integer read. - /// public uint[] ReadUInt32Array(int length) { uint[] data = new uint[length]; + for (int i = 0; i < length; i++) { data[i] = ReadUInt32(); @@ -225,21 +190,15 @@ public byte[] ReadUInt8Array(int length) { byte[] data = new byte[length]; - ReadInternal(data, length); + ReadBytesInternal(data, length); return data; } - /// - /// Reads array of 16-bit unsigned integers from the stream. - /// - /// The length. - /// - /// The 16-bit signed integer read. - /// public short[] ReadInt16Array(int length) { short[] data = new short[length]; + for (int i = 0; i < length; i++) { data[i] = ReadInt16(); @@ -248,10 +207,6 @@ public short[] ReadInt16Array(int length) return data; } - /// - /// Reads an array of 16-bit signed integers from the stream to the buffer. - /// - /// The buffer to read to. public void ReadInt16Array(Span buffer) { for (int i = 0; i < buffer.Length; i++) @@ -260,110 +215,104 @@ public void ReadInt16Array(Span buffer) } } - /// - /// Reads a 8-bit unsigned integer from the stream, using the bit converter - /// for this reader. 1 bytes are read. - /// - /// The 8-bit unsigned integer read. public byte ReadUInt8() { - ReadInternal(_buffer, 1); - return _buffer[0]; + EnsureAvailable(1); + + return _span[_position++]; } - /// - /// Reads a 24-bit unsigned integer from the stream, using the bit converter - /// for this reader. 3 bytes are read. - /// - /// The 24-bit unsigned integer read. public int ReadUInt24() { byte highByte = ReadByte(); + return (highByte << 16) | ReadUInt16(); } - /// - /// Reads a 32-bit unsigned integer from the stream, using the bit converter - /// for this reader. 4 bytes are read. - /// - /// The 32-bit unsigned integer read. public uint ReadUInt32() { - ReadInternal(_buffer, 4); + EnsureAvailable(4); + + uint value = BinaryPrimitives.ReadUInt32BigEndian(_span.Slice(_position, 4)); + + _position += 4; - return BinaryPrimitives.ReadUInt32BigEndian(_buffer); + return value; } - /// - /// Reads a 32-bit unsigned integer from the stream representing an offset position. - /// 4 bytes are read. - /// - /// The 32-bit unsigned integer read. public uint ReadOffset32() => ReadUInt32(); - /// - /// 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. - /// - /// The number of bytes to read. - /// The bytes read. 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; } - /// - /// 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. - /// - /// The bytes to read. - /// The encoding. - /// - /// The string read from the stream. - /// public string ReadString(int bytesToRead, Encoding encoding) { - byte[] data = new byte[bytesToRead]; - ReadInternal(data, bytesToRead); - return encoding.GetString(data, 0, data.Length); + EnsureAvailable(bytesToRead); + +#if NETSTANDARD2_0 + byte[] buffer = System.Buffers.ArrayPool.Shared.Rent(bytesToRead); + + try + { + _span.Slice(_position, bytesToRead).CopyTo(buffer); + + string result = encoding.GetString(buffer, 0, bytesToRead); + + _position += bytesToRead; + + return result; + } + finally + { + System.Buffers.ArrayPool.Shared.Return(buffer); + } +#else + string result = encoding.GetString(_span.Slice(_position, bytesToRead)); + + _position += bytesToRead; + + return result; +#endif } - /// - /// Reads the uint32 string. - /// - /// a 4 character long UTF8 encoded string. public string ReadTag() { - ReadInternal(_buffer, 4); + EnsureAvailable(4); + +#if NETSTANDARD2_0 + byte[] buffer = System.Buffers.ArrayPool.Shared.Rent(4); + + try + { + _span.Slice(_position, 4).CopyTo(buffer); + + string tag = Encoding.UTF8.GetString(buffer, 0, 4); - return Encoding.UTF8.GetString(_buffer, 0, 4); + _position += 4; + + return tag; + } + finally + { + System.Buffers.ArrayPool.Shared.Return(buffer); + } +#else + string tag = Encoding.UTF8.GetString(_span.Slice(_position, 4)); + + _position += 4; + + return tag; +#endif } - /// - /// Reads an offset consuming the given nuber of bytes. - /// - /// The offset size in bytes. - /// The 32-bit signed integer representing the offset. - /// Size is not in range. public int ReadOffset(int size) => size switch { @@ -374,33 +323,20 @@ public int ReadOffset(int size) _ => throw new InvalidOperationException(), }; - /// - /// Reads the given number of bytes from the stream, throwing an exception - /// if they can't all be read. - /// - /// Buffer to read into. - /// Number of bytes to read. - 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."); } } diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapEncoding.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapEncoding.cs new file mode 100644 index 00000000000..8979e21d7a3 --- /dev/null +++ b/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) + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat.cs new file mode 100644 index 00000000000..667498bd436 --- /dev/null +++ b/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 + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs new file mode 100644 index 00000000000..5859e141965 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs @@ -0,0 +1,183 @@ +using System; +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Media.Fonts.Tables.Cmap +{ + internal sealed class CmapFormat12Table : IReadOnlyDictionary + { + private readonly ReadOnlyMemory _table; + private readonly int _groupCount; + private readonly ReadOnlyMemory _groups; + + private int? _count; + + public CmapFormat12Table(ReadOnlyMemory table) + { + var reader = new BigEndianBinaryReader(table.Span); + + ushort format = reader.ReadUInt16(); + Debug.Assert(format == 12, "Format must be 12."); + + ushort reserved = reader.ReadUInt16(); + Debug.Assert(reserved == 0, "Reserved field must be 0."); + + uint length = reader.ReadUInt32(); + + _table = table.Slice(0, (int)length); + + uint language = reader.ReadUInt32(); + + _groupCount = (int)reader.ReadUInt32(); + + int groupsOffset = reader.Position; + int groupsLength = _groupCount * 12; + + Debug.Assert(length >= groupsOffset + groupsLength, "Length must cover all groups."); + + _groups = _table.Slice(groupsOffset, groupsLength); + } + + private static uint ReadUInt32BE(ReadOnlyMemory mem, int groupIndex, int fieldOffset) + { + var span = mem.Span; + int byteIndex = groupIndex * 12 + fieldOffset; + return BinaryPrimitives.ReadUInt32BigEndian(span.Slice(byteIndex, 4)); + } + + // Binary search to find the group containing the code point + private int FindGroupIndex(int codePoint) + { + int lo = 0; + int hi = _groupCount - 1; + + while (lo <= hi) + { + int mid = (lo + hi) >> 1; + uint start = ReadUInt32BE(_groups, mid, 0); + uint end = ReadUInt32BE(_groups, mid, 4); + + if (codePoint < start) + { + hi = mid - 1; + } + else if (codePoint > end) + { + lo = mid + 1; + } + else + { + return mid; + } + } + + // Not found + return -1; + } + + public ushort this[int codePoint] + { + get + { + int groupIndex = FindGroupIndex(codePoint); + + if (groupIndex < 0) + { + return 0; + } + + uint start = ReadUInt32BE(_groups, groupIndex, 0); + uint startGlyph = ReadUInt32BE(_groups, groupIndex, 8); + + // Calculate glyph index + return (ushort)(startGlyph + (codePoint - start)); + } + } + + public int Count + { + get + { + if (_count.HasValue) + { + return _count.Value; + } + + long total = 0; + + for (int g = 0; g < _groupCount; g++) + { + uint start = ReadUInt32BE(_groups, g, 0); + uint end = ReadUInt32BE(_groups, g, 4); + total += (end - start + 1); + } + + _count = (int)total; + + return _count.Value; + } + } + + public IEnumerable Keys + { + get + { + for (int g = 0; g < _groupCount; g++) + { + uint start = ReadUInt32BE(_groups, g, 0); + uint end = ReadUInt32BE(_groups, g, 4); + + for (uint cp = start; cp <= end; cp++) + { + yield return (int)cp; + } + } + } + } + + public IEnumerable Values + { + get + { + for (int g = 0; g < _groupCount; g++) + { + uint start = ReadUInt32BE(_groups, g, 0); + uint end = ReadUInt32BE(_groups, g, 4); + uint startGlyph = ReadUInt32BE(_groups, g, 8); + + for (uint cp = start; cp <= end; cp++) + { + yield return (ushort)(startGlyph + (cp - start)); + } + } + } + } + + public bool ContainsKey(int key) => this[key] != 0; + + public bool TryGetValue(int key, out ushort value) + { + value = this[key]; + return value != 0; + } + + public IEnumerator> GetEnumerator() + { + for (int g = 0; g < _groupCount; g++) + { + uint start = ReadUInt32BE(_groups, g, 0); + uint end = ReadUInt32BE(_groups, g, 4); + uint startGlyph = ReadUInt32BE(_groups, g, 8); + + for (uint cp = start; cp <= end; cp++) + { + yield return new KeyValuePair((int)cp, (ushort)(startGlyph + (cp - start))); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat4Table.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat4Table.cs new file mode 100644 index 00000000000..bd8ec8337d9 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat4Table.cs @@ -0,0 +1,316 @@ +using System; +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Media.Fonts.Tables.Cmap +{ + internal sealed class CmapFormat4Table : IReadOnlyDictionary + { + private readonly ReadOnlyMemory _table; + + private readonly int _segCount; + private readonly ReadOnlyMemory _endCodes; + private readonly ReadOnlyMemory _startCodes; + private readonly ReadOnlyMemory _idDeltas; + private readonly ReadOnlyMemory _idRangeOffsets; + private readonly ReadOnlyMemory _glyphIdArray; + + private int? _count; + + public CmapFormat4Table(ReadOnlyMemory table) + { + var reader = new BigEndianBinaryReader(table.Span); + + ushort format = reader.ReadUInt16(); // must be 4 + + Debug.Assert(format == 4, "Format must be 4."); + + ushort length = reader.ReadUInt16(); // length in bytes of this subtable + + _table = table.Slice(0, length); + + ushort language = reader.ReadUInt16(); // language code, 0 for non-language-specific + + ushort segCountX2 = reader.ReadUInt16(); // 2 * segCount + _segCount = segCountX2 / 2; + + ushort searchRange = reader.ReadUInt16(); // searchRange = 2 * (2^floor(log2(segCount))) + ushort entrySelector = reader.ReadUInt16(); // entrySelector = log2(searchRange/2) + ushort rangeShift = reader.ReadUInt16(); // rangeShift = segCountX2 - searchRange + + // Spec sanity checks + Debug.Assert(searchRange == (ushort)(2 * (1 << (int)Math.Floor(Math.Log(_segCount, 2)))), + "searchRange must equal 2 * (2^floor(log2(segCount)))."); + Debug.Assert(entrySelector == (ushort)Math.Floor(Math.Log(_segCount, 2)), + "entrySelector must equal log2(searchRange/2)."); + Debug.Assert(rangeShift == (ushort)(segCountX2 - searchRange), + "rangeShift must equal segCountX2 - searchRange."); + + // Compute offsets + int endCodeOffset = reader.Position; + int startCodeOffset = endCodeOffset + _segCount * 2 + 2; // + reservedPad + int idDeltaOffset = startCodeOffset + _segCount * 2; // after startCodes + int idRangeOffsetOffset = idDeltaOffset + _segCount * 2; // after idDeltas + int glyphIdArrayOffset = idRangeOffsetOffset + _segCount * 2; // after idRangeOffsets + + // Ensure declared length is consistent + Debug.Assert(length >= glyphIdArrayOffset, + "Subtable length must be at least large enough to contain glyphIdArray."); + + // Slice directly + _endCodes = _table.Slice(endCodeOffset, _segCount * 2); + + _startCodes = _table.Slice(startCodeOffset, _segCount * 2); + + _idDeltas = _table.Slice(idDeltaOffset, _segCount * 2); + + _idRangeOffsets = _table.Slice(idRangeOffsetOffset, _segCount * 2); + + int glyphCount = (length - glyphIdArrayOffset) / 2; + + Debug.Assert(glyphCount >= 0, "GlyphIdArray length must not be negative."); + + _glyphIdArray = _table.Slice(glyphIdArrayOffset, glyphCount * 2); + } + + // Reads a big-endian UInt16 from the specified word index in the given memory + private static ushort ReadUInt16BE(ReadOnlyMemory mem, int wordIndex) + { + var span = mem.Span; + int byteIndex = wordIndex * 2; + + // Ensure we don't go out of bounds + return BinaryPrimitives.ReadUInt16BigEndian(span.Slice(byteIndex, 2)); + } + + public int Count + { + get + { + if (_count.HasValue) + { + return _count.Value; + } + + int count = 0; + + for (int seg = 0; seg < _segCount; seg++) + { + // Get start and end of segment + int start = ReadUInt16BE(_startCodes, seg); + int end = ReadUInt16BE(_endCodes, seg); + + for (int cp = start; cp <= end; cp++) + { + // Only count if maps to non-zero glyph + if (this[cp] != 0) + { + count++; + } + } + } + + _count = count; + + return count; + } + } + + public ushort this[int codePoint] + { + get + { + // Find the segment containing the codePoint + int segmentIndex = FindSegmentIndex(codePoint); + + if (segmentIndex < 0) + { + return 0; + } + + ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets, segmentIndex); + ushort idDelta = ReadUInt16BE(_idDeltas, segmentIndex); + + // If idRangeOffset is 0, glyphId = (codePoint + idDelta) % 65536 + if (idRangeOffset == 0) + { + return (ushort)((codePoint + idDelta) & 0xFFFF); + } + else + { + int start = ReadUInt16BE(_startCodes, segmentIndex); + int ro = idRangeOffset / 2; // words + // The index into the glyphIdArray + int idx = (codePoint - start) + ro - (_segCount - segmentIndex); + + // Ensure index is within bounds of glyphIdArray + int glyphArrayWords = _glyphIdArray.Length / 2; + + if ((uint)idx < (uint)glyphArrayWords) + { + ushort glyphId = ReadUInt16BE(_glyphIdArray, idx); + + // If glyphId is not 0, apply idDelta + if (glyphId != 0) + { + glyphId = (ushort)((glyphId + idDelta) & 0xFFFF); + } + + return glyphId; + } + } + + // Not found or maps to missing glyph + return 0; + } + } + + public bool ContainsKey(int key) => this[key] != 0; + + public bool TryGetValue(int key, out ushort value) + { + value = this[key]; + + return value != 0; + } + + public IEnumerable Keys + { + get + { + for (int seg = 0; seg < _segCount; seg++) + { + int start = ReadUInt16BE(_startCodes, seg); + int end = ReadUInt16BE(_endCodes, seg); + + for (int cp = start; cp <= end; cp++) + { + ushort gid = ResolveGlyph(seg, cp); + + // Only yield code points that map to non-zero glyphs + if (gid != 0) + { + yield return cp; + } + } + } + } + } + + public IEnumerable Values + { + get + { + for (int seg = 0; seg < _segCount; seg++) + { + int start = ReadUInt16BE(_startCodes, seg); + int end = ReadUInt16BE(_endCodes, seg); + + for (int cp = start; cp <= end; cp++) + { + ushort gid = ResolveGlyph(seg, cp); + + // Only yield non-zero glyphs + if (gid != 0) + { + yield return gid; + } + } + } + } + } + + public IEnumerator> GetEnumerator() + { + for (int seg = 0; seg < _segCount; seg++) + { + int start = ReadUInt16BE(_startCodes, seg); + int end = ReadUInt16BE(_endCodes, seg); + + for (int cp = start; cp <= end; cp++) + { + ushort gid = ResolveGlyph(seg, cp); + + // Only yield mappings to non-zero glyphs + if (gid != 0) + { + yield return new KeyValuePair(cp, gid); + } + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + // Resolves the glyph ID for a given code point within a specific segment + private ushort ResolveGlyph(int segmentIndex, int codePoint) + { + ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets, segmentIndex); + ushort idDelta = ReadUInt16BE(_idDeltas, segmentIndex); + + if (idRangeOffset == 0) + { + return (ushort)((codePoint + idDelta) & 0xFFFF); + } + else + { + int start = ReadUInt16BE(_startCodes, segmentIndex); + int ro = idRangeOffset / 2; // words + int idx = (codePoint - start) + ro - (_segCount - segmentIndex); + int glyphArrayWords = _glyphIdArray.Length / 2; + + if ((uint)idx < (uint)glyphArrayWords) + { + ushort glyphId = ReadUInt16BE(_glyphIdArray, idx); + + if (glyphId != 0) + { + glyphId = (ushort)((glyphId + idDelta) & 0xFFFF); + } + + return glyphId; + } + } + + // Not found or maps to missing glyph + return 0; + } + + private int FindSegmentIndex(int codePoint) + { + int lo = 0; + int hi = _segCount - 1; + + // Binary search over endCodes (sorted ascending) + while (lo <= hi) + { + int mid = (lo + hi) >> 1; + int end = ReadUInt16BE(_endCodes, mid); + + if (codePoint > end) + { + lo = mid + 1; + } + else + { + hi = mid - 1; + } + } + + // lo is now the first segment whose endCode >= codePoint + if (lo < _segCount) + { + int start = ReadUInt16BE(_startCodes, lo); + + if (codePoint >= start) + { + return lo; + } + } + + return -1; // not found + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapSubtableEntry.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapSubtableEntry.cs new file mode 100644 index 00000000000..e7945acaa73 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapSubtableEntry.cs @@ -0,0 +1,42 @@ +using System; + +namespace Avalonia.Media.Fonts.Tables.Cmap +{ + // Representation of a subtable entry in the 'cmap' table directory + internal readonly record struct CmapSubtableEntry + { + public CmapSubtableEntry(PlatformID platform, CmapEncoding encoding, int offset, CmapFormat format) : this() + { + Platform = platform; + Encoding = encoding; + Offset = offset; + Format = format; + } + + /// + /// Gets the platform identifier for the current environment. + /// + public PlatformID Platform { get; init; } + + /// + /// Gets the character map (CMap) encoding associated with this instance. + /// + /// + public CmapEncoding Encoding { get; init; } + + /// + /// Gets the offset of the sub table. + /// + public int Offset { get; init; } + + /// + /// Gets the format of the character-to-glyph mapping (cmap) table. + /// + public CmapFormat Format { get; init; } + + public ReadOnlyMemory GetSubtableMemory(ReadOnlyMemory table) + { + return table.Slice(Offset); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs new file mode 100644 index 00000000000..70fbd98d8bc --- /dev/null +++ b/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 +{ + /// + /// Represents the 'cmap' table in an OpenType font, which maps character codes to glyph indices. + /// + /// 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. + internal sealed class CmapTable + { + internal const string TableName = "cmap"; + internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); + + public static IReadOnlyDictionary Load(IGlyphTypeface glyphTypeface) + { + if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) + { + throw new InvalidOperationException("No cmap table found."); + } + + var reader = new BigEndianBinaryReader(table.Span); + + reader.ReadUInt16(); // version + + var numTables = reader.ReadUInt16(); + + var entries = new CmapSubtableEntry[numTables]; + + for (var i = 0; i < numTables; i++) + { + var platformID = (PlatformID)reader.ReadUInt16(); + var encodingID = (CmapEncoding)reader.ReadUInt16(); + var offset = (int)reader.ReadUInt32(); + + var position = reader.Position; + + reader.Seek(offset); + + var format = (CmapFormat)reader.ReadUInt16(); + + reader.Seek(position); + + var entry = new CmapSubtableEntry(platformID, encodingID, offset, format); + + entries[i] = entry; + } + + // Try to find the best Format 12 subtable entry + if (TryFindFormat12Entry(entries, out var format12Entry)) + { + // Prefer Format 12 if available + return new CmapFormat12Table(format12Entry.GetSubtableMemory(table)); + } + + // Fallback to Format 4 + if (TryFindFormat4Entry(entries, out var format4Entry)) + { + return new CmapFormat4Table(format4Entry.GetSubtableMemory(table)); + } + + throw new InvalidOperationException("No suitable cmap subtable found."); + + // Tries to find the best Format 12 subtable entry based on platform and encoding preferences + static bool TryFindFormat12Entry(CmapSubtableEntry[] entries, out CmapSubtableEntry result) + { + result = default; + var foundPlatformScore = int.MaxValue; + var foundEncodingScore = int.MaxValue; + + foreach (var entry in entries) + { + if (entry.Format != CmapFormat.Format12) + { + continue; + } + + var platformScore = entry.Platform switch + { + PlatformID.Unicode => 0, + PlatformID.Windows => 1, + _ => 2 + }; + + var encodingScore = 2; // Default: lowest preference + + switch (entry.Platform) + { + case PlatformID.Unicode when entry.Encoding == CmapEncoding.Unicode_2_0_full: + encodingScore = 0; // non-BMP preferred + break; + case PlatformID.Unicode when entry.Encoding == CmapEncoding.Unicode_2_0_BMP: + encodingScore = 1; // BMP + break; + case PlatformID.Windows when entry.Encoding == CmapEncoding.Microsoft_UCS4 && platformScore != 0: + encodingScore = 0; // non-BMP preferred + break; + case PlatformID.Windows when entry.Encoding == CmapEncoding.Microsoft_UnicodeBMP && platformScore != 0: + encodingScore = 1; // BMP + break; + } + + if (encodingScore < foundEncodingScore || encodingScore == foundEncodingScore && platformScore < foundPlatformScore) + { + result = entry; + foundEncodingScore = encodingScore; + foundPlatformScore = platformScore; + } + else + { + if (platformScore < foundPlatformScore) + { + result = entry; + foundEncodingScore = encodingScore; + foundPlatformScore = platformScore; + } + } + + if (foundPlatformScore == 0 && foundEncodingScore == 0) + { + break; // Best possible match found + } + } + + return result.Format != CmapFormat.Format0; + } + + // Tries to find the best Format 4 subtable entry based on platform preferences + static bool TryFindFormat4Entry(CmapSubtableEntry[] entries, out CmapSubtableEntry result) + { + result = default; + var foundPlatformScore = int.MaxValue; + + foreach (var entry in entries) + { + if (entry.Format != CmapFormat.Format4) + { + continue; + } + + var platformScore = entry.Platform switch + { + PlatformID.Unicode => 0, + PlatformID.Windows => 1, + _ => 2 + }; + + if (platformScore < foundPlatformScore) + { + result = entry; + foundPlatformScore = platformScore; + } + + if (foundPlatformScore == 0) + { + break; // Best possible match found + } + } + + return result.Format != CmapFormat.Format0; + } + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs index 0a916c7ed06..e988e6c1ef3 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs @@ -3,7 +3,6 @@ // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts using System.Collections.Generic; -using System.IO; namespace Avalonia.Media.Fonts.Tables { @@ -17,8 +16,8 @@ namespace Avalonia.Media.Fonts.Tables /// 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 features) { @@ -29,26 +28,24 @@ private FeatureListTable(IReadOnlyList features) public static FeatureListTable? LoadGSub(IGlyphTypeface glyphTypeface) { - if (!glyphTypeface.TryGetTable(GSubTag, out var gPosTable)) + if (!glyphTypeface.PlatformTypeface.TryGetTable(GSubTag, out var gPosTable)) { return null; } - using var stream = new MemoryStream(gPosTable); - using var reader = new BigEndianBinaryReader(stream, false); + var reader = new BigEndianBinaryReader(gPosTable.Span); return Load(reader); } public static FeatureListTable? LoadGPos(IGlyphTypeface glyphTypeface) { - if (!glyphTypeface.TryGetTable(GPosTag, out var gSubTable)) + if (!glyphTypeface.PlatformTypeface.TryGetTable(GPosTag, out var gSubTable)) { return null; } - using var stream = new MemoryStream(gSubTable); - using var reader = new BigEndianBinaryReader(stream, false); + var reader = new BigEndianBinaryReader(gSubTable.Span); return Load(reader); @@ -73,14 +70,14 @@ private static FeatureListTable Load(BigEndianBinaryReader reader) reader.ReadUInt16(); reader.ReadUInt16(); - reader.ReadOffset16(); + var featureListOffset = reader.ReadOffset16(); return Load(reader, featureListOffset); } - private static FeatureListTable Load(BigEndianBinaryReader reader, long offset) + private static FeatureListTable Load(BigEndianBinaryReader reader, int offset) { // FeatureList // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+ @@ -90,7 +87,7 @@ private static FeatureListTable Load(BigEndianBinaryReader reader, long offset) // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+ // | 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(); diff --git a/src/Avalonia.Base/Media/Fonts/Tables/HeadTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/HeadTable.cs new file mode 100644 index 00000000000..ac4720ebcdd --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/HeadTable.cs @@ -0,0 +1,118 @@ +using System; + +namespace Avalonia.Media.Fonts.Tables +{ + internal sealed class HeadTable + { + internal const string TableName = "head"; + internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); + + public float Version { get; } + public float FontRevision { get; } + public uint CheckSumAdjustment { get; } + public uint MagicNumber { get; } + public ushort Flags { get; } + public ushort UnitsPerEm { get; } + public long Created { get; } + public long Modified { get; } + public short XMin { get; } + public short YMin { get; } + public short XMax { get; } + public short YMax { get; } + public ushort MacStyle { get; } + public ushort LowestRecPPEM { get; } + public short FontDirectionHint { get; } + public short IndexToLocFormat { get; } + public short GlyphDataFormat { get; } + + private HeadTable( + float version, + float fontRevision, + uint checkSumAdjustment, + uint magicNumber, + ushort flags, + ushort unitsPerEm, + long created, + long modified, + short xMin, + short yMin, + short xMax, + short yMax, + ushort macStyle, + ushort lowestRecPPEM, + short fontDirectionHint, + short indexToLocFormat, + short glyphDataFormat) + { + Version = version; + FontRevision = fontRevision; + CheckSumAdjustment = checkSumAdjustment; + MagicNumber = magicNumber; + Flags = flags; + UnitsPerEm = unitsPerEm; + Created = created; + Modified = modified; + XMin = xMin; + YMin = yMin; + XMax = xMax; + YMax = yMax; + MacStyle = macStyle; + LowestRecPPEM = lowestRecPPEM; + FontDirectionHint = fontDirectionHint; + IndexToLocFormat = indexToLocFormat; + GlyphDataFormat = glyphDataFormat; + } + + public static HeadTable Load(IGlyphTypeface glyphTypeface) + { + if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) + { + throw new InvalidOperationException("Could not load the 'head' table."); + } + + var reader = new BigEndianBinaryReader(table.Span); + + return Load(reader); + } + + private static HeadTable Load(BigEndianBinaryReader reader) + { + float version = reader.ReadFixed(); + float fontRevision = reader.ReadFixed(); + uint checkSumAdjustment = reader.ReadUInt32(); + uint magicNumber = reader.ReadUInt32(); + ushort flags = reader.ReadUInt16(); + ushort unitsPerEm = reader.ReadUInt16(); + long created = reader.ReadInt64(); + long modified = reader.ReadInt64(); + short xMin = reader.ReadInt16(); + short yMin = reader.ReadInt16(); + short xMax = reader.ReadInt16(); + short yMax = reader.ReadInt16(); + ushort macStyle = reader.ReadUInt16(); + ushort lowestRecPPEM = reader.ReadUInt16(); + short fontDirectionHint = reader.ReadInt16(); + short indexToLocFormat = reader.ReadInt16(); + short glyphDataFormat = reader.ReadInt16(); + + return new HeadTable( + version, + fontRevision, + checkSumAdjustment, + magicNumber, + flags, + unitsPerEm, + created, + modified, + xMin, + yMin, + xMax, + yMax, + macStyle, + lowestRecPPEM, + fontDirectionHint, + indexToLocFormat, + glyphDataFormat); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeaderTable.cs similarity index 80% rename from src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs rename to src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeaderTable.cs index 0942296536e..dd16085ab9d 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeaderTable.cs @@ -2,16 +2,18 @@ // Licensed under the Apache License, Version 2.0. // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts -using System.IO; - namespace Avalonia.Media.Fonts.Tables { - internal class HorizontalHeadTable + internal class HorizontalHeaderTable { internal const string TableName = "hhea"; - internal static OpenTypeTag Tag = OpenTypeTag.Parse(TableName); - public HorizontalHeadTable( + /// + /// Gets the OpenType tag identifying this table ("hhea"). + /// + internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); + + public HorizontalHeaderTable( short ascender, short descender, short lineGap, @@ -37,43 +39,74 @@ public HorizontalHeadTable( NumberOfHMetrics = numberOfHMetrics; } + /// + /// Gets the maximum advance width value for all glyphs in the font. + /// public ushort AdvanceWidthMax { get; } + /// + /// Distance from the baseline to the highest ascender. + /// public short Ascender { get; } + /// + /// Offset of the caret for slanted fonts. Set to 0 for non-slanted fonts. + /// public short CaretOffset { get; } + /// + /// Rise component used to calculate the slope of the caret (rise/run). + /// public short CaretSlopeRise { get; } + /// + /// Run component used to calculate the slope of the caret (rise/run). + /// public short CaretSlopeRun { get; } + /// + /// Distance from the baseline to the lowest descender. + /// public short Descender { get; } + /// + /// Typographic line gap. + /// public short LineGap { get; } + /// + /// Minimum left side bearing value. Must be consistent with horizontal metrics. + /// public short MinLeftSideBearing { get; } + /// + /// Minimum right side bearing value. Must be consistent with horizontal metrics. + /// public short MinRightSideBearing { get; } + /// + /// Number of advance widths in the horizontal metrics table (numOfLongHorMetrics). + /// public ushort NumberOfHMetrics { get; } + /// + /// Maximum horizontal extent: max(lsb + (xMax - xMin)). + /// public short XMaxExtent { get; } - public static HorizontalHeadTable? Load(IGlyphTypeface glyphTypeface) + public static HorizontalHeaderTable? Load(IGlyphTypeface fontFace) { - if (!glyphTypeface.TryGetTable(Tag, out var table)) + if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table)) { return null; } - using var stream = new MemoryStream(table); - using var binaryReader = new BigEndianBinaryReader(stream, false); + var binaryReader = new BigEndianBinaryReader(table.Span); - // Move to start of table. return Load(binaryReader); } - public static HorizontalHeadTable Load(BigEndianBinaryReader reader) + private static HorizontalHeaderTable Load(BigEndianBinaryReader reader) { // +--------+---------------------+---------------------------------------------------------------------------------+ // | Type | Name | Description | @@ -136,7 +169,7 @@ public static HorizontalHeadTable Load(BigEndianBinaryReader reader) ushort numberOfHMetrics = reader.ReadUInt16(); - return new HorizontalHeadTable( + return new HorizontalHeaderTable( ascender, descender, lineGap, diff --git a/src/Avalonia.Base/Media/Fonts/Tables/MaxpTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/MaxpTable.cs new file mode 100644 index 00000000000..4a24406ddd8 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/MaxpTable.cs @@ -0,0 +1,37 @@ +namespace Avalonia.Media.Fonts.Tables +{ + internal readonly struct MaxpTable + { + internal const string TableName = "maxp"; + internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); + + public ushort NumGlyphs { get; } + + private MaxpTable(ushort numGlyphs) + { + NumGlyphs = numGlyphs; + } + + public static MaxpTable? Load(IGlyphTypeface fontFace) + { + if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table)) + { + return null; + } + + var binaryReader = new BigEndianBinaryReader(table.Span); + + return Load(binaryReader); + } + + private static MaxpTable Load(BigEndianBinaryReader reader) + { + // Skip version (4 bytes) + reader.ReadUInt32(); + + var numGlyphs = reader.ReadUInt16(); + + return new MaxpTable(numGlyphs); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalGlyphMetric.cs b/src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalGlyphMetric.cs new file mode 100644 index 00000000000..7de0a0a2785 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalGlyphMetric.cs @@ -0,0 +1,26 @@ +namespace Avalonia.Media.Fonts.Tables.Metrics +{ + /// + /// Represents a single horizontal metric record from the 'hmtx' table. + /// + internal readonly record struct HorizontalGlyphMetric + { + /// + /// The advance width of the glyph. + /// + public ushort AdvanceWidth { get; } + + /// + /// The left side bearing of the glyph. + /// + public short LeftSideBearing { get; } + + public HorizontalGlyphMetric(ushort advanceWidth, short leftSideBearing) + { + AdvanceWidth = advanceWidth; + LeftSideBearing = leftSideBearing; + } + + public override string ToString() => $"Advance={AdvanceWidth}, LSB={LeftSideBearing}"; + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalMetricsTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalMetricsTable.cs new file mode 100644 index 00000000000..3e02fc0dcc0 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalMetricsTable.cs @@ -0,0 +1,113 @@ +using System; + +namespace Avalonia.Media.Fonts.Tables.Metrics +{ + internal class HorizontalMetricsTable + { + public const string TagName = "hmtx"; + public static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TagName); + + private readonly ReadOnlyMemory _data; + private readonly ushort _numOfHMetrics; + private readonly uint _numGlyphs; + + private HorizontalMetricsTable(ReadOnlyMemory data, ushort numOfHMetrics, uint numGlyphs) + { + _data = data; + _numOfHMetrics = numOfHMetrics; + _numGlyphs = numGlyphs; + } + + internal static HorizontalMetricsTable? Load(IGlyphTypeface glyphTypeface, ushort numberOfHMetrics, uint glyphCount) + { + if (glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) + { + return new HorizontalMetricsTable(table, numberOfHMetrics, glyphCount); + } + + return null; + } + + /// + /// Retrieves the horizontal glyph metrics for the specified glyph index. + /// + /// This method retrieves the horizontal metrics for a single glyph by its index. The + /// returned metrics include information such as advance width, left side bearing, and other glyph-specific + /// data. + /// The index of the glyph for which to retrieve metrics. Must be a valid glyph index within the font. + /// A object containing the horizontal metrics for the specified glyph. + public HorizontalGlyphMetric GetMetrics(ushort glyphIndex) + { + // Validate glyph index + if (glyphIndex >= _numGlyphs) + { + throw new ArgumentOutOfRangeException(nameof(glyphIndex), $"Glyph index {glyphIndex} is out of range."); + } + + var reader = new BigEndianBinaryReader(_data.Span); + + if (glyphIndex < _numOfHMetrics) + { + // Each record is 4 bytes + reader.Seek(glyphIndex * 4); + + ushort advanceWidth = reader.ReadUInt16(); + short leftSideBearing = reader.ReadInt16(); + + return new HorizontalGlyphMetric(advanceWidth, leftSideBearing); + } + else + { + // Last advance width + reader.Seek((_numOfHMetrics - 1) * 4); + + ushort lastAdvanceWidth = reader.ReadUInt16(); + + // Offset into trailing LSB array + int lsbIndex = glyphIndex - _numOfHMetrics; + int lsbOffset = _numOfHMetrics * 4 + lsbIndex * 2; + + reader.Seek(lsbOffset); + + short leftSideBearing = reader.ReadInt16(); + + return new HorizontalGlyphMetric(lastAdvanceWidth, leftSideBearing); + } + } + + /// + /// Retrieves the advance width for a single glyph. + /// + /// Glyph index to query. + /// Advance width for the glyph. + public ushort GetAdvance(ushort glyphIndex) + { + // Validate glyph index + if (glyphIndex >= _numGlyphs) + { + throw new ArgumentOutOfRangeException(nameof(glyphIndex)); + } + + var reader = new BigEndianBinaryReader(_data.Span); + + if (glyphIndex < _numOfHMetrics) + { + // Each record is 4 bytes + reader.Seek(glyphIndex * 4); + + ushort advanceWidth = reader.ReadUInt16(); + + return advanceWidth; + } + else + { + // Last advance width + reader.Seek((_numOfHMetrics - 1) * 4); + + ushort lastAdvanceWidth = reader.ReadUInt16(); + + return lastAdvanceWidth; + } + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalGlyphMetric.cs b/src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalGlyphMetric.cs new file mode 100644 index 00000000000..ef252ad6eb0 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalGlyphMetric.cs @@ -0,0 +1,24 @@ +namespace Avalonia.Media.Fonts.Tables.Metrics +{ + /// + /// Represents a single vertical metric record from the 'vmtx' table. + /// + internal readonly record struct VerticalGlyphMetric + { + public VerticalGlyphMetric(ushort advanceHeight, short topSideBearing) + { + AdvanceHeight = advanceHeight; + TopSideBearing = topSideBearing; + } + + /// + /// The advance height of the glyph. + /// + public ushort AdvanceHeight { get; } + + /// + /// The top side bearing of the glyph. + /// + public short TopSideBearing { get; } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalMetricsTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalMetricsTable.cs new file mode 100644 index 00000000000..5f9c11ebb79 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalMetricsTable.cs @@ -0,0 +1,110 @@ +using System; + +namespace Avalonia.Media.Fonts.Tables.Metrics +{ + internal class VerticalMetricsTable + { + public const string TagName = "vmtx"; + public static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TagName); + + private readonly ReadOnlyMemory _data; + private readonly ushort _numOfVMetrics; + private readonly uint _numGlyphs; + + private VerticalMetricsTable(ReadOnlyMemory data, ushort numOfVMetrics, uint numGlyphs) + { + _data = data; + _numOfVMetrics = numOfVMetrics; + _numGlyphs = numGlyphs; + } + + public static VerticalMetricsTable? Load(IGlyphTypeface glyphTypeface, ushort numberOfVMetrics, uint glyphCount) + { + if (glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) + { + return new VerticalMetricsTable(table, numberOfVMetrics, glyphCount); + } + + return null; + } + + /// + /// Retrieves the vertical glyph metrics for the specified glyph index. + /// + /// The index of the glyph for which to retrieve metrics. + /// A containing the vertical metrics for the specified glyph. + public VerticalGlyphMetric GetMetrics(ushort glyphIndex) + { + // Validate glyph index + if (glyphIndex >= _numGlyphs) + { + throw new ArgumentOutOfRangeException(nameof(glyphIndex), $"Glyph index {glyphIndex} is out of range."); + } + + var reader = new BigEndianBinaryReader(_data.Span); + + if (glyphIndex < _numOfVMetrics) + { + // Each record is 4 bytes + reader.Seek(glyphIndex * 4); + + ushort advanceHeight = reader.ReadUInt16(); + short topSideBearing = reader.ReadInt16(); + + return new VerticalGlyphMetric(advanceHeight, topSideBearing); + } + else + { + // Last advance height + reader.Seek((_numOfVMetrics - 1) * 4); + + ushort lastAdvanceHeight = reader.ReadUInt16(); + + // Offset into trailing TSB array + int tsbIndex = glyphIndex - _numOfVMetrics; + int tsbOffset = _numOfVMetrics * 4 + tsbIndex * 2; + + reader.Seek(tsbOffset); + + short tsb = reader.ReadInt16(); + + return new VerticalGlyphMetric(lastAdvanceHeight, tsb); + } + } + + /// + /// Retrieves the advance height for a single glyph. + /// + /// Glyph index to query. + /// Advance height for the glyph. + public ushort GetAdvance(ushort glyphIndex) + { + // Validate glyph index + if (glyphIndex >= _numGlyphs) + { + throw new ArgumentOutOfRangeException(nameof(glyphIndex)); + } + + var reader = new BigEndianBinaryReader(_data.Span); + + if (glyphIndex < _numOfVMetrics) + { + // Each record is 4 bytes + reader.Seek(glyphIndex * 4); + + ushort advanceHeight = reader.ReadUInt16(); + + return advanceHeight; + } + else + { + // Last advance height + reader.Seek((_numOfVMetrics - 1) * 4); + + ushort lastAdvanceHeight = reader.ReadUInt16(); + + return lastAdvanceHeight; + } + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs index 7a7ad71995d..fe596da40f4 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs @@ -2,44 +2,64 @@ // Licensed under the Apache License, Version 2.0. // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts +using System; + namespace Avalonia.Media.Fonts.Tables.Name { internal class NameRecord { - private readonly string value; + private readonly ReadOnlyMemory _stringStorage; - public NameRecord(PlatformIDs platform, ushort languageId, KnownNameIds nameId, string value) + public NameRecord( + ReadOnlyMemory stringStorage, + PlatformID platform, + ushort languageId, + KnownNameIds nameId, + ushort offset, + ushort length, + System.Text.Encoding encoding) { + _stringStorage = stringStorage; + Platform = platform; LanguageID = languageId; NameID = nameId; - this.value = value; + Offset = offset; + Length = length; + Encoding = encoding; } - public PlatformIDs Platform { get; } + public PlatformID Platform { get; } public ushort LanguageID { get; } public KnownNameIds NameID { get; } - internal StringLoader? StringReader { get; private set; } + public ushort Offset { get; } - public string Value => StringReader?.Value ?? value; + public ushort Length { get; } - public static NameRecord Read(BigEndianBinaryReader reader) + public System.Text.Encoding Encoding { get; } + + public string Value { - var platform = reader.ReadUInt16(); - var encodingId = reader.ReadUInt16(); - var encoding = encodingId.AsEncoding(); - var languageID = reader.ReadUInt16(); - var nameID = reader.ReadUInt16(); + get + { + if (Length == 0) + { + return string.Empty; + } - var stringReader = StringLoader.Create(reader, encoding); + var reader = new BigEndianBinaryReader(_stringStorage.Span); - return new NameRecord(platform, languageID, nameID, string.Empty) - { - StringReader = stringReader - }; + reader.Seek(Offset); + + byte[] data = reader.ReadBytes(Length); + + var value = Encoding.GetString(data); + + return value; + } } } } diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs index c0c1048e519..418ead53238 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Collections.Generic; -using System.IO; using Avalonia.Utilities; namespace Avalonia.Media.Fonts.Tables.Name @@ -69,7 +68,7 @@ public string GetNameById(ushort culture, KnownNameIds nameId) { // Get just the first one, just in case. first ??= name; - if (name.Platform == PlatformIDs.Windows) + if (name.Platform == PlatformID.Windows) { // If us not found return the first windows one. firstWindows ??= name; @@ -99,46 +98,30 @@ public string GetNameById(ushort culture, ushort nameId) public static NameTable? Load(IGlyphTypeface glyphTypeface) { - if (!glyphTypeface.TryGetTable(Tag, out var table)) + if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) { return null; } - using var stream = new MemoryStream(table); - using var binaryReader = new BigEndianBinaryReader(stream, false); + var reader = new BigEndianBinaryReader(table.Span); - // Move to start of table. - return Load(binaryReader); - } - - public static NameTable Load(BigEndianBinaryReader reader) - { - var strings = new List(); - var format = reader.ReadUInt16(); - var nameCount = reader.ReadUInt16(); - var stringOffset = reader.ReadUInt16(); - - var names = new NameRecord[nameCount]; - - for (var i = 0; i < nameCount; i++) - { - names[i] = NameRecord.Read(reader); + reader.ReadUInt16(); // version + var count = reader.ReadUInt16(); + var storageOffset = reader.ReadUInt16(); - var sr = names[i].StringReader; + var names = new NameRecord[count]; - if (sr is not null) - { - strings.Add(sr); - } - } - - foreach (var readable in strings) + for (var i = 0; i < count; i++) { - var readableStartOffset = stringOffset + readable.Offset; - - reader.Seek(readableStartOffset, SeekOrigin.Begin); - - readable.LoadValue(reader); + var platform = reader.ReadUInt16(); + var encodingId = reader.ReadUInt16(); + var encoding = encodingId.AsEncoding(); + var languageID = reader.ReadUInt16(); + var nameID = reader.ReadUInt16(); + 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); diff --git a/src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs b/src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs index 9dc41ef0838..9b8de5d7915 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs @@ -3,14 +3,13 @@ // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts using System; -using System.IO; namespace Avalonia.Media.Fonts.Tables { internal sealed class OS2Table { internal const string TableName = "OS/2"; - internal static OpenTypeTag Tag = OpenTypeTag.Parse(TableName); + internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); private readonly byte[] panose; private readonly short capHeight; @@ -54,7 +53,7 @@ public OS2Table( uint unicodeRange3, uint unicodeRange4, string tag, - FontStyleSelection fontStyle, + FontSelectionFlags fontStyle, ushort firstCharIndex, ushort lastCharIndex, short typoAscender, @@ -66,7 +65,7 @@ public OS2Table( this.averageCharWidth = averageCharWidth; WeightClass = weightClass; WidthClass = widthClass; - StyleType = styleType; + Type = styleType; SubscriptXSize = subscriptXSize; SubscriptYSize = subscriptYSize; SubscriptXOffset = subscriptXOffset; @@ -84,7 +83,7 @@ public OS2Table( this.unicodeRange3 = unicodeRange3; this.unicodeRange4 = unicodeRange4; this.tag = tag; - FontStyle = fontStyle; + Selection = fontStyle; this.firstCharIndex = firstCharIndex; this.lastCharIndex = lastCharIndex; TypoAscender = typoAscender; @@ -107,7 +106,7 @@ public OS2Table( version0Table.averageCharWidth, version0Table.WeightClass, version0Table.WidthClass, - version0Table.StyleType, + version0Table.Type, version0Table.SubscriptXSize, version0Table.SubscriptYSize, version0Table.SubscriptXOffset, @@ -125,7 +124,7 @@ public OS2Table( version0Table.unicodeRange3, version0Table.unicodeRange4, version0Table.tag, - version0Table.FontStyle, + version0Table.Selection, version0Table.firstCharIndex, version0Table.lastCharIndex, version0Table.TypoAscender, @@ -159,7 +158,7 @@ public OS2Table(OS2Table versionLessThan5Table, ushort lowerOpticalPointSize, us } [Flags] - internal enum FontStyleSelection : ushort + internal enum FontSelectionFlags : ushort { /// /// Font contains italic or oblique characters, otherwise they are upright. @@ -214,7 +213,7 @@ internal enum FontStyleSelection : ushort // 10–15 Reserved; set to 0. } - public FontStyleSelection FontStyle { get; } + public FontSelectionFlags Selection { get; } public short TypoAscender { get; } @@ -246,27 +245,25 @@ internal enum FontStyleSelection : ushort public short SuperscriptYSize { get; } - public ushort StyleType { get; } + public ushort Type { get; } public ushort WeightClass { get; } public ushort WidthClass { get; } - public static OS2Table? Load(IGlyphTypeface glyphTypeface) + public static OS2Table? Load(IGlyphTypeface fontFace) { - if (!glyphTypeface.TryGetTable(Tag, out var table)) + if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table)) { return null; } - using var stream = new MemoryStream(table); - using var binaryReader = new BigEndianBinaryReader(stream, false); + var binaryReader = new BigEndianBinaryReader(table.Span); - // Move to start of table. return Load(binaryReader); } - public static OS2Table Load(BigEndianBinaryReader reader) + private static OS2Table Load(BigEndianBinaryReader reader) { // Version 1.0 // Type | Name | Comments @@ -334,7 +331,7 @@ public static OS2Table Load(BigEndianBinaryReader reader) uint unicodeRange3 = reader.ReadUInt32(); // Bits 64–95 uint unicodeRange4 = reader.ReadUInt32(); // Bits 96–127 string tag = reader.ReadTag(); - FontStyleSelection fontStyle = reader.ReadUInt16(); + FontSelectionFlags fontStyle = reader.ReadUInt16(); ushort firstCharIndex = reader.ReadUInt16(); ushort lastCharIndex = reader.ReadUInt16(); short typoAscender = reader.ReadInt16(); diff --git a/src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs b/src/Avalonia.Base/Media/Fonts/Tables/PlatformID.cs similarity index 95% rename from src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs rename to src/Avalonia.Base/Media/Fonts/Tables/PlatformID.cs index c57c4e2726a..05c864b5355 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/PlatformID.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media.Fonts.Tables /// /// platforms ids /// - internal enum PlatformIDs : ushort + internal enum PlatformID : ushort { /// /// Unicode platform diff --git a/src/Avalonia.Base/Media/Fonts/Tables/PostTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/PostTable.cs new file mode 100644 index 00000000000..218031aac60 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/PostTable.cs @@ -0,0 +1,46 @@ +namespace Avalonia.Media.Fonts.Tables +{ + internal readonly struct PostTable + { + internal const string TableName = "post"; + internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); + + public float Version { get; } + public float ItalicAngle { get; } + public short UnderlinePosition { get; } + public short UnderlineThickness { get; } + public bool IsFixedPitch { get; } + + private PostTable(float version, float italicAngle, short underlinePosition, short underlineThickness, uint isFixedPitch) + { + Version = version; + ItalicAngle = italicAngle; + UnderlinePosition = underlinePosition; + UnderlineThickness = underlineThickness; + IsFixedPitch = isFixedPitch != 0; + } + + public static PostTable Load(IGlyphTypeface glyphTypeface) + { + if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) + { + return default; + } + + var binaryReader = new BigEndianBinaryReader(table.Span); + + return Load(binaryReader); + } + + private static PostTable Load(BigEndianBinaryReader reader) + { + float version = reader.ReadFixed(); + float italicAngle = reader.ReadFixed(); + short underlinePosition = reader.ReadFWORD(); + short underlineThickness = reader.ReadFWORD(); + uint isFixedPitch = reader.ReadUInt32(); + + return new PostTable(version, italicAngle, underlinePosition, underlineThickness, isFixedPitch); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs b/src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs deleted file mode 100644 index a42c87b5bd4..00000000000 --- a/src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs +++ /dev/null @@ -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); - } -} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/VerticalHeaderTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/VerticalHeaderTable.cs new file mode 100644 index 00000000000..acff2b08e69 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/VerticalHeaderTable.cs @@ -0,0 +1,128 @@ +namespace Avalonia.Media.Fonts.Tables +{ + internal class VerticalHeaderTable + { + internal const string TableName = "vhea"; + internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); + + public VerticalHeaderTable( + short ascender, + short descender, + short lineGap, + ushort advanceHeightMax, + short minTopSideBearing, + short minBottomSideBearing, + short yMaxExtent, + short caretSlopeRise, + short caretSlopeRun, + short caretOffset, + ushort numberOfVMetrics) + { + Ascender = ascender; + Descender = descender; + LineGap = lineGap; + AdvanceHeightMax = advanceHeightMax; + MinTopSideBearing = minTopSideBearing; + MinBottomSideBearing = minBottomSideBearing; + YMaxExtent = yMaxExtent; + CaretSlopeRise = caretSlopeRise; + CaretSlopeRun = caretSlopeRun; + CaretOffset = caretOffset; + NumberOfVMetrics = numberOfVMetrics; + } + + public ushort AdvanceHeightMax { get; } + + public short Ascender { get; } + + public short CaretOffset { get; } + + public short CaretSlopeRise { get; } + + public short CaretSlopeRun { get; } + + public short Descender { get; } + + public short LineGap { get; } + + public short MinTopSideBearing { get; } + + public short MinBottomSideBearing { get; } + + public ushort NumberOfVMetrics { get; } + + public short YMaxExtent { get; } + + public static VerticalHeaderTable? Load(IGlyphTypeface fontFace) + { + if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table)) + { + return null; + } + + var binaryReader = new BigEndianBinaryReader(table.Span); + + // Move to start of table. + return Load(binaryReader); + } + + private static VerticalHeaderTable Load(BigEndianBinaryReader reader) + { + // See OpenType spec for vhea: + // | Fixed | version | 0x00010000 (1.0) | + // | FWord | ascender | Distance from baseline of highest ascender (vertical) | + // | FWord | descender | Distance from baseline of lowest descender (vertical) | + // | FWord | lineGap | typographic line gap (vertical) | + // | uFWord | advanceHeightMax | must be consistent with vertical metrics | + // | FWord | minTopSideBearing | must be consistent with vertical metrics | + // | FWord | minBottomSideBearing| must be consistent with vertical metrics | + // | FWord | yMaxExtent | max(tsb + (yMax-yMin)) | + // | int16 | caretSlopeRise | used to calculate the slope of the caret (rise/run) set to 1 for vertical caret | + // | int16 | caretSlopeRun | 0 for vertical | + // | FWord | caretOffset | set value to 0 for non-slanted fonts | + // | int16 | reserved | set value to 0 | + // | int16 | reserved | set value to 0 | + // | int16 | reserved | set value to 0 | + // | int16 | reserved | set value to 0 | + // | int16 | metricDataFormat | 0 for current format | + // | uint16 | numOfLongVerMetrics | number of advance heights in vertical metrics table | + + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + short ascender = reader.ReadFWORD(); + short descender = reader.ReadFWORD(); + short lineGap = reader.ReadFWORD(); + ushort advanceHeightMax = reader.ReadUFWORD(); + short minTopSideBearing = reader.ReadFWORD(); + short minBottomSideBearing = reader.ReadFWORD(); + short yMaxExtent = reader.ReadFWORD(); + short caretSlopeRise = reader.ReadInt16(); + short caretSlopeRun = reader.ReadInt16(); + short caretOffset = reader.ReadInt16(); + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + short metricDataFormat = reader.ReadInt16(); // 0 + if (metricDataFormat != 0) + { + throw new InvalidFontTableException($"Expected metricDataFormat = 0 found {metricDataFormat}", TableName); + } + + ushort numberOfVMetrics = reader.ReadUInt16(); + + return new VerticalHeaderTable( + ascender, + descender, + lineGap, + advanceHeightMax, + minTopSideBearing, + minBottomSideBearing, + yMaxExtent, + caretSlopeRise, + caretSlopeRun, + caretOffset, + numberOfVMetrics); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/UnmanagedFontMemory.cs b/src/Avalonia.Base/Media/Fonts/UnmanagedFontMemory.cs new file mode 100644 index 00000000000..8f96ded9d8d --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/UnmanagedFontMemory.cs @@ -0,0 +1,407 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Avalonia.Media.Fonts +{ + /// + /// Represents a memory manager for unmanaged font data, providing functionality to access and manage font memory + /// and OpenType table data. + /// + /// 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. + internal sealed unsafe class UnmanagedFontMemory : MemoryManager, IFontMemory + { + private IntPtr _ptr; + private int _length; + private bool _disposed; + private int _pinCount; + + // Reader/writer lock to protect lifetime and cache access. + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.NoRecursion); + + /// + /// Represents a cache of font table data, where each entry maps an OpenType tag to its corresponding byte data. + /// + /// 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. + private readonly Dictionary> _tableCache = []; + + private UnmanagedFontMemory(IntPtr ptr, int length) + { + _ptr = ptr; + _length = length; + } + + /// + /// Attempts to retrieve the memory region corresponding to the specified OpenType table tag. + /// + /// 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 . + /// The identifying the table to retrieve. Must not be . + /// When this method returns, contains the memory region of the requested table if the operation succeeds; + /// otherwise, contains the default value. + /// if the table memory was successfully retrieved; otherwise, . + /// Thrown if the font memory has been disposed. + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory table) + { + table = default; + + // Validate tag + if (tag == OpenTypeTag.None) + { + return false; + } + + _lock.EnterUpgradeableReadLock(); + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(UnmanagedFontMemory)); + } + + if (_ptr == IntPtr.Zero || _length < 12) + { + return false; + } + + // Create a span over the unmanaged memory (read-only view) + var fontData = Memory.Span; + + // Minimal SFNT header: 4 (sfnt) + 2 (numTables) + 6 (rest) = 12 + if (fontData.Length < 12) + { + return false; + } + + // Check cache first + if (_tableCache.TryGetValue(tag, out var cached)) + { + table = cached; + + return true; + } + + // Parse table directory + var numTables = BinaryPrimitives.ReadUInt16BigEndian(fontData.Slice(4, 2)); + var recordsStart = 12; + var requiredDirectoryBytes = checked(recordsStart + numTables * 16); + + if (fontData.Length < requiredDirectoryBytes) + { + return false; + } + + for (int i = 0; i < numTables; i++) + { + var entryOffset = recordsStart + i * 16; + var entrySlice = fontData.Slice(entryOffset, 16); + var entryTag = (OpenTypeTag)BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(0, 4)); + + if (entryTag != tag) + { + continue; + } + + var offset = (int)BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(8, 4)); + var length = (int)BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(12, 4)); + + // Bounds checks - ensure values fit within the span + if (offset > fontData.Length || length > fontData.Length) + { + return false; + } + + if (offset + length > fontData.Length) + { + return false; + } + + if (offset < 0 || length < 0 || offset + length > fontData.Length) + { + return false; + } + + table = Memory.Slice(offset, length); + + // Acquire write lock to update cache + _lock.EnterWriteLock(); + + try + { + // Cache the result for faster subsequent lookups + _tableCache[tag] = table; + + return true; + } + finally + { + // Release write lock + _lock.ExitWriteLock(); + } + } + + return false; + } + finally + { + // Release upgradeable read lock + _lock.ExitUpgradeableReadLock(); + } + } + + /// + /// Loads font data from the specified stream into unmanaged memory. + /// + 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.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.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(buffer, 0, len)); + } + } + + /// + /// Creates an instance of from the specified byte data. + /// + /// The method allocates unmanaged memory to store the provided byte data. The caller is + /// responsible for ensuring that the returned instance is properly disposed + /// to release the allocated memory. + /// A read-only span of bytes representing the font data. The span must not be empty. + /// An instance of that encapsulates the unmanaged memory containing the font + /// data. + private static UnmanagedFontMemory CreateFromBytes(ReadOnlySpan data) + { + var len = data.Length; + var ptr = Marshal.AllocHGlobal(len); + + try + { + if (len > 0) + { + unsafe + { + fixed (byte* src = &MemoryMarshal.GetReference(data)) + { + Buffer.MemoryCopy(src, (void*)ptr, len, len); + } + } + } + + return new UnmanagedFontMemory(ptr, len); + } + catch + { + Marshal.FreeHGlobal(ptr); + throw; + } + } + + // Implement MemoryManager members on the owner + public override Span GetSpan() + { + _lock.EnterReadLock(); + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(UnmanagedFontMemory)); + } + + if (_ptr == IntPtr.Zero || _length <= 0) + { + return Span.Empty; + } + + unsafe + { + return new Span((void*)_ptr.ToPointer(), _length); + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public override MemoryHandle Pin(int elementIndex = 0) + { + if (elementIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + } + + // Increment pin count first to prevent dispose racing with pin. + Interlocked.Increment(ref _pinCount); + + // Validate state under lock + _lock.EnterReadLock(); + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(UnmanagedFontMemory)); + } + + if (_ptr == IntPtr.Zero || _length == 0) + { + return new MemoryHandle(); + } + + if (elementIndex > _length) + { + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + } + + unsafe + { + var p = (byte*)_ptr.ToPointer() + elementIndex; + return new MemoryHandle(p); + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public override void Unpin() + { + // Decrement pin count + Interlocked.Decrement(ref _pinCount); + } + + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + // Explicit dispose: use lock to synchronize with other threads and dispose managed resources. + _lock.EnterWriteLock(); + + try + { + if (_disposed) + { + return; + } + + if (Volatile.Read(ref _pinCount) > 0) + { + throw new InvalidOperationException("Cannot dispose while memory is pinned."); + } + + if (_ptr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_ptr); + _ptr = IntPtr.Zero; + } + + _length = 0; + + _disposed = true; + } + finally + { + _lock.ExitWriteLock(); + // Dispose the lock (managed resource) only on explicit dispose. + _lock.Dispose(); + } + } + else + { + // Finalizer: do not touch managed objects. Free only unmanaged memory. + var ptr = Interlocked.Exchange(ref _ptr, IntPtr.Zero); + + if (ptr != IntPtr.Zero) + { + Marshal.FreeHGlobal(ptr); + } + + Interlocked.Exchange(ref _length, 0); + + // Mark as disposed to prevent further attempts to use the memory. + _disposed = true; + } + } + } +} diff --git a/src/Avalonia.Base/Media/GlyphMetrics.cs b/src/Avalonia.Base/Media/GlyphMetrics.cs index e9b5a112ac1..11a4a3fa23a 100644 --- a/src/Avalonia.Base/Media/GlyphMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphMetrics.cs @@ -10,15 +10,15 @@ public readonly record struct GlyphMetrics /// /// Distance from the top extremum of the glyph to the y-origin. /// - public int YBearing{ get; init; } + public int YBearing { get; init; } /// /// Distance from the left extremum of the glyph to the right extremum. /// - public int Width{ get; init; } + public ushort Width { get; init; } /// /// Distance from the top extremum of the glyph to the bottom extremum. /// - public int Height{ get; init; } + public ushort Height { get; init; } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 489dcb7a402..8c773b18e0e 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -93,14 +93,17 @@ private static IReadOnlyList CreateGlyphInfos(IReadOnlyList g double fontRenderingEmSize, IGlyphTypeface glyphTypeface) { var glyphIndexSpan = ListToSpan(glyphIndices); - var glyphAdvances = glyphTypeface.GetGlyphAdvances(glyphIndexSpan); var glyphInfos = new GlyphInfo[glyphIndexSpan.Length]; var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; for (var i = 0; i < glyphIndexSpan.Length; ++i) { - glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale); + var glyphIndex = glyphIndexSpan[i]; + + var advance = glyphTypeface.GetGlyphAdvance(glyphIndex) * scale; + + glyphInfos[i] = new GlyphInfo(glyphIndex, i, advance); } return glyphInfos; @@ -205,7 +208,7 @@ public int BiDiLevel } /// - /// Gets the scale of the current + /// Gets the scale of the current /// internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight; @@ -270,7 +273,7 @@ public double GetDistanceFromCharacterHit(CharacterHit characterHit) //For in cluster hits we need to move to the start of the next cluster. if (inClusterHit) { - for(; glyphIndex < _glyphInfos.Count; glyphIndex++) + for (; glyphIndex < _glyphInfos.Count; glyphIndex++) { if (_glyphInfos[glyphIndex].GlyphCluster > characterIndex) { @@ -367,7 +370,7 @@ public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInsi characterIndex = glyphInfo.GlyphCluster; if (currentX + advance > distance) - { + { break; } diff --git a/src/Avalonia.Base/Media/GlyphTypeface.cs b/src/Avalonia.Base/Media/GlyphTypeface.cs new file mode 100644 index 00000000000..23563fb2331 --- /dev/null +++ b/src/Avalonia.Base/Media/GlyphTypeface.cs @@ -0,0 +1,395 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Media.Fonts; +using Avalonia.Media.Fonts.Tables; +using Avalonia.Media.Fonts.Tables.Cmap; +using Avalonia.Media.Fonts.Tables.Metrics; +using Avalonia.Media.Fonts.Tables.Name; +using Avalonia.Platform; + +namespace Avalonia.Media +{ + /// + /// Represents a glyph typeface, providing access to font metrics, glyph mappings, and other font-related + /// properties. + /// + /// The 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. + internal class GlyphTypeface : IGlyphTypeface + { + private bool _isDisposed; + + private readonly NameTable? _nameTable; + private readonly OS2Table? _os2Table; + private readonly IReadOnlyDictionary _cmapTable; + private readonly HorizontalHeaderTable? _hhTable; + private readonly VerticalHeaderTable? _vhTable; + private readonly HorizontalMetricsTable? _hmTable; + private readonly VerticalMetricsTable? _vmTable; + + private IReadOnlyList? _supportedFeatures; + private ITextShaperTypeface? _textShaperTypeface; + + /// + /// Initializes a new instance of the class with the specified platform typeface and + /// font simulations. + /// + /// 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. + /// The platform-specific typeface to be used for this instance. This parameter + /// cannot be null. + /// The font simulations to apply, such as bold or oblique. The default is . + /// Thrown if required font tables (e.g., 'maxp') cannot be loaded. + public GlyphTypeface(IPlatformTypeface typeface, FontSimulations fontSimulations = FontSimulations.None) + { + PlatformTypeface = typeface; + + _os2Table = OS2Table.Load(this); + _cmapTable = CmapTable.Load(this); + + var maxpTable = MaxpTable.Load(this) ?? throw new InvalidOperationException("Could not load the 'maxp' table."); + + GlyphCount = maxpTable.NumGlyphs; + + _hhTable = HorizontalHeaderTable.Load(this); + + if (_hhTable is not null) + { + _hmTable = HorizontalMetricsTable.Load(this, _hhTable.NumberOfHMetrics, GlyphCount); + } + + _vhTable = VerticalHeaderTable.Load(this); + + if (_vhTable is not null) + { + _vmTable = VerticalMetricsTable.Load(this, _vhTable.NumberOfVMetrics, GlyphCount); + } + + var ascent = 0; + var descent = 0; + var lineGap = 0; + + if (_os2Table != null && (_os2Table.Selection & OS2Table.FontSelectionFlags.USE_TYPO_METRICS) != 0) + { + ascent = -_os2Table.TypoAscender; + descent = -_os2Table.TypoDescender; + lineGap = _os2Table.TypoLineGap; + } + else + { + if (_hhTable != null) + { + ascent = -_hhTable.Ascender; + descent = -_hhTable.Descender; + lineGap = _hhTable.LineGap; + } + } + + if (_os2Table != null && (ascent == 0 || descent == 0)) + { + if (_os2Table.TypoAscender != 0 || _os2Table.TypoDescender != 0) + { + ascent = -_os2Table.TypoAscender; + descent = -_os2Table.TypoDescender; + lineGap = _os2Table.TypoLineGap; + } + else + { + ascent = -_os2Table.WinAscent; + descent = _os2Table.WinDescent; + } + } + + var headTable = HeadTable.Load(this); + var postTable = PostTable.Load(this); + + var isFixedPitch = postTable.IsFixedPitch; + var underlineOffset = postTable.UnderlinePosition; + var underlineSize = postTable.UnderlineThickness; + + Metrics = new FontMetrics + { + DesignEmHeight = (short)headTable.UnitsPerEm, + Ascent = ascent, + Descent = descent, + LineGap = lineGap, + UnderlinePosition = -underlineOffset, + UnderlineThickness = underlineSize, + StrikethroughPosition = -_os2Table?.StrikeoutPosition ?? 0, + StrikethroughThickness = _os2Table?.StrikeoutSize ?? 0, + IsFixedPitch = isFixedPitch + }; + + FontSimulations = fontSimulations; + + var fontWeight = _os2Table != null ? (FontWeight)_os2Table.WeightClass : FontWeight.Normal; + + Weight = (fontSimulations & FontSimulations.Bold) != 0 ? FontWeight.Bold : fontWeight; + + var style = _os2Table != null ? GetFontStyle(_os2Table, headTable, postTable) : FontStyle.Normal; + + Style = (fontSimulations & FontSimulations.Oblique) != 0 ? FontStyle.Italic : style; + + var stretch = _os2Table != null ? (FontStretch)_os2Table.WidthClass : FontStretch.Normal; + + Stretch = stretch; + + _nameTable = NameTable.Load(this); + + FamilyName = _nameTable?.FontFamilyName((ushort)CultureInfo.InvariantCulture.LCID) ?? "unknown"; + + TypographicFamilyName = _nameTable?.GetNameById((ushort)CultureInfo.InvariantCulture.LCID, KnownNameIds.TypographicFamilyName) ?? FamilyName; + + if (_nameTable != null) + { + var familyNames = new Dictionary(1); + var faceNames = new Dictionary(1); + + foreach (var nameRecord in _nameTable) + { + if (nameRecord.NameID == KnownNameIds.FontFamilyName) + { + if (nameRecord.Platform != Fonts.Tables.PlatformID.Windows || nameRecord.LanguageID == 0) + { + continue; + } + + var culture = GetCulture(nameRecord.LanguageID); + + if (!familyNames.ContainsKey(culture)) + { + familyNames[culture] = nameRecord.Value; + } + + } + + if (nameRecord.NameID == KnownNameIds.FontSubfamilyName) + { + if (nameRecord.Platform != Fonts.Tables.PlatformID.Windows || nameRecord.LanguageID == 0) + { + continue; + } + + var culture = GetCulture(nameRecord.LanguageID); + + if (!faceNames.ContainsKey(culture)) + { + faceNames[culture] = nameRecord.Value; + } + } + } + + FamilyNames = familyNames; + FaceNames = faceNames; + } + else + { + FamilyNames = new Dictionary { { CultureInfo.InvariantCulture, FamilyName } }; + FaceNames = new Dictionary { { CultureInfo.InvariantCulture, Weight.ToString() } }; + } + + static CultureInfo GetCulture(int lcid) + { + if (lcid == ushort.MaxValue) + { + return CultureInfo.InvariantCulture; + } + + try + { + return CultureInfo.GetCultureInfo(lcid) ?? CultureInfo.InvariantCulture; + } + catch (CultureNotFoundException) + { + return CultureInfo.InvariantCulture; + } + } + } + + public string TypographicFamilyName { get; } + + public IReadOnlyDictionary FamilyNames { get; } + + public IReadOnlyDictionary FaceNames { get; } + + public IReadOnlyList SupportedFeatures + { + get + { + if (_supportedFeatures != null) + { + return _supportedFeatures; + } + + var gPosFeatures = FeatureListTable.LoadGPos(this); + var gSubFeatures = FeatureListTable.LoadGSub(this); + + var supportedFeatures = new List(gPosFeatures?.Features.Count ?? 0 + gSubFeatures?.Features.Count ?? 0); + + if (gPosFeatures != null) + { + foreach (var gPosFeature in gPosFeatures.Features) + { + if (supportedFeatures.Contains(gPosFeature)) + { + continue; + } + + supportedFeatures.Add(gPosFeature); + } + } + + if (gSubFeatures != null) + { + foreach (var gSubFeature in gSubFeatures.Features) + { + if (supportedFeatures.Contains(gSubFeature)) + { + continue; + } + + supportedFeatures.Add(gSubFeature); + } + } + + _supportedFeatures = supportedFeatures; + + return supportedFeatures; + } + } + + public FontSimulations FontSimulations { get; } + + public int ReplacementCodepoint { get; } + + public FontMetrics Metrics { get; } + + public uint GlyphCount { get; } + + public string FamilyName { get; } + + public FontWeight Weight { get; } + + public FontStyle Style { get; } + + public FontStretch Stretch { get; } + + public IReadOnlyDictionary CharacterToGlyphMap => _cmapTable; + + public IPlatformTypeface PlatformTypeface { get; } + + public ITextShaperTypeface TextShaperTypeface + { + get + { + if (_textShaperTypeface != null) + { + return _textShaperTypeface; + } + + var textShaper = AvaloniaLocator.Current.GetRequiredService(); + + _textShaperTypeface = textShaper.CreateTypeface(this); + + return _textShaperTypeface; + } + } + + private static FontStyle GetFontStyle(OS2Table oS2Table, HeadTable headTable, PostTable postTable) + { + var isItalic = (oS2Table.Selection & OS2Table.FontSelectionFlags.ITALIC) != 0 || (headTable.MacStyle & 0x02) != 0; + + var isOblique = (oS2Table.Selection & OS2Table.FontSelectionFlags.OBLIQUE) != 0; + + var italicAngle = postTable.ItalicAngle; + + if (isOblique) + { + return FontStyle.Oblique; + } + + if (Math.Abs(italicAngle) > 0.01f && !isItalic) + { + return FontStyle.Oblique; + } + + if (isItalic) + { + return FontStyle.Italic; + } + + return FontStyle.Normal; + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!disposing) + { + return; + } + + PlatformTypeface.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public ushort GetGlyphAdvance(ushort glyphId) + { + if (_hmTable is null) + { + return 0; + } + + return _hmTable.GetAdvance(glyphId); + } + + public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) + { + metrics = default; + + HorizontalGlyphMetric hMetric = default; + VerticalGlyphMetric vMetric = default; + + if (_hmTable != null) + { + hMetric = _hmTable.GetMetrics(glyph); + } + + if (_vmTable != null) + { + vMetric = _vmTable.GetMetrics(glyph); + } + + if (hMetric.Equals(default) && vMetric.Equals(default)) + { + return false; + } + + metrics = new GlyphMetrics + { + XBearing = hMetric.LeftSideBearing, + YBearing = vMetric.TopSideBearing, + Width = hMetric.AdvanceWidth, + Height = vMetric.AdvanceHeight + }; + + return true; + } + } +} diff --git a/src/Avalonia.Base/Media/IGlyphTypeface.cs b/src/Avalonia.Base/Media/IGlyphTypeface.cs index 09740aac811..9440a6a91bd 100644 --- a/src/Avalonia.Base/Media/IGlyphTypeface.cs +++ b/src/Avalonia.Base/Media/IGlyphTypeface.cs @@ -1,16 +1,74 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using Avalonia.Media.Fonts; using Avalonia.Metadata; namespace Avalonia.Media { + /// + /// Represents a typeface that provides access to font-related information and operations, such as glyph metrics, + /// supported OpenType features, and culture-specific names. + /// + /// The interface is designed for advanced text rendering and layout + /// scenarios. It provides detailed information about a font's characteristics, including its family name, style, + /// weight, and stretch, as well as mappings between Unicode code points and glyph indices. This interface + /// also supports retrieving culture-specific names for font families and faces, accessing OpenType features, and + /// obtaining glyph metrics for precise text shaping and rendering. Implementations of this interface + /// are expected to be disposable, as they may hold unmanaged resources related to font handling. [Unstable] public interface IGlyphTypeface : IDisposable { /// - /// Gets the family name for the object. + /// Gets the family name. /// string FamilyName { get; } + /// + /// Gets the typographic family name. + /// + /// + /// The typographic family name is an alternate family name that may be used for stylistic or typographic purposes. + /// + /// Example: For the fonts "Inter Light" and "Inter Condensed", the FamilyName values are "Inter Light" and "Inter Condensed" respectively, + /// but both share the same TypographicFamilyName of "Inter". + /// + /// + string TypographicFamilyName { get; } + + /// + /// Gets a read-only dictionary that maps culture-specific information to the family name. + /// + /// This property provides localized family names for different cultures. The dictionary is never empty. + /// If a specific culture is not present in the dictionary, the caller may need to handle fallback logic to a default culture + /// or name. + IReadOnlyDictionary FamilyNames { get; } + + /// + /// Gets a read-only list of supported OpenType features. + /// + IReadOnlyList SupportedFeatures { get; } + + /// + /// Gets a read-only dictionary that maps culture-specific information to corresponding face names. + /// + /// + /// The dictionary provides a way to retrieve face names localized for specific cultures. + /// If a culture is not present in the dictionary, it indicates that no face name is defined for that + /// culture. + /// + /// Example: For a font family "Arial", common face names include "Regular", "Bold", "Italic", "Bold Italic". + /// The dictionary might contain entries such as: + /// + /// en-US: "Bold Italic" + /// de-DE: "Fett Kursiv" + /// + /// + /// + IReadOnlyDictionary FaceNames { get; } + /// /// Gets the designed weight of the font represented by the object. /// @@ -27,22 +85,45 @@ public interface IGlyphTypeface : IDisposable FontStretch Stretch { get; } /// - /// Gets the number of glyphs held by this glyph typeface. + /// Gets the number of glyphs held by this object. /// - int GlyphCount { get; } + uint GlyphCount { get; } /// - /// Gets the font metrics. + /// Gets the algorithmic style simulations applied to object. + /// + FontSimulations FontSimulations { get; } + + /// + /// Gets the font metrics associated with the current font. /// - /// - /// The font metrics. - /// FontMetrics Metrics { get; } /// - /// Gets the algorithmic style simulations applied to this glyph typeface. + /// Gets the nominal mapping of a Unicode code point to a glyph index as defined by the font 'CMAP' table. /// - FontSimulations FontSimulations { get; } + IReadOnlyDictionary CharacterToGlyphMap { get; } + + /// + /// Gets the glyph typeface associated with the . + /// + IPlatformTypeface PlatformTypeface { get; } + + /// + /// Gets the typeface used for text shaping operations. + /// + /// The typeface is used to determine glyphs and their positioning during text shaping. + /// This property is typically used in scenarios involving advanced text layout or rendering. + ITextShaperTypeface TextShaperTypeface { get; } + + /// + /// Returns the glyph advance for the specified glyph. + /// + /// The glyph. + /// + /// The advance. + /// + ushort GetGlyphAdvance(ushort glyph); /// /// Tries to get a glyph's metrics in em units. @@ -53,62 +134,48 @@ public interface IGlyphTypeface : IDisposable /// true if an glyph's metrics was found, false otherwise. /// bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics); - - /// - /// Returns an glyph index for the specified codepoint. - /// - /// - /// Returns 0 if a glyph isn't found. - /// - /// The codepoint. - /// - /// A glyph index. - /// - ushort GetGlyph(uint codepoint); + } + public interface IPlatformTypeface : IFontMemory + { /// - /// Tries to get an glyph index for specified codepoint. + /// Gets the designed weight of the font represented by the object. /// - /// The codepoint. - /// A glyph index. - /// - /// true if an glyph index was found, false otherwise. - /// - bool TryGetGlyph(uint codepoint, out ushort glyph); + FontWeight Weight { get; } /// - /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0. + /// Gets the style for the object. /// - /// The codepoints to map. - /// - /// An array of glyph indices. - /// - ushort[] GetGlyphs(ReadOnlySpan codepoints); + FontStyle Style { get; } /// - /// Returns the glyph advance for the specified glyph. + /// Gets the value for the object. /// - /// The glyph. - /// - /// The advance. - /// - int GetGlyphAdvance(ushort glyph); + FontStretch Stretch { get; } /// - /// Returns an array of glyph advances in design em size. + /// Returns the font file stream represented by the . /// - /// The glyph indices. - /// - /// An array of glyph advances. - /// - int[] GetGlyphAdvances(ReadOnlySpan glyphs); + /// The stream. + /// Returns true if the stream can be obtained, otherwise false. + bool TryGetStream([NotNullWhen(true)] out Stream? stream); + } + + public interface ITextShaperTypeface : IDisposable + { + + } + public interface IFontMemory : IDisposable + { /// - /// Returns the contents of the table data for the specified tag. + /// Attempts to retrieve the memory block associated with the specified OpenType table tag. /// - /// The table tag to get the data for. - /// The contents of the table data for the specified tag. - /// Returns true if the content exists, otherwise false. - bool TryGetTable(uint tag, out byte[] table); + /// The OpenType table tag identifying the table to retrieve. + /// 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. + /// if the memory block for the specified table tag was successfully retrieved; + /// otherwise, . + bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory table); } } diff --git a/src/Avalonia.Base/Media/IGlyphTypeface2.cs b/src/Avalonia.Base/Media/IGlyphTypeface2.cs deleted file mode 100644 index 3bd2b1e7676..00000000000 --- a/src/Avalonia.Base/Media/IGlyphTypeface2.cs +++ /dev/null @@ -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 - { - /// - /// Returns the font file stream represented by the object. - /// - /// The stream. - /// Returns true if the stream can be obtained, otherwise false. - bool TryGetStream([NotNullWhen(true)] out Stream? stream); - - /// - /// Gets the typographic family name. - /// - string TypographicFamilyName { get; } - - /// - /// Gets the localized family names. - /// Keys are culture identifiers. - /// - IReadOnlyDictionary FamilyNames { get; } - - /// - /// Gets supported font features. - /// - IReadOnlyList SupportedFeatures { get; } - - /// - /// Gets the localized face names. - /// Keys are culture identifiers. - /// - IReadOnlyDictionary FaceNames { get; } - } -} diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index acfffb68fa2..b5bd1f4da55 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -146,7 +146,7 @@ private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, //Move forward until we reach the next base character while (enumerator.MoveNext(out grapheme)) { - if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) + if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.CharacterToGlyphMap.TryGetValue(grapheme.FirstCodepoint, out _)) { break; } @@ -194,15 +194,15 @@ internal static bool TryGetShapeableLength( if (!currentCodepoint.IsWhiteSpace && defaultGlyphTypeface != null - && defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _)) + && defaultGlyphTypeface.CharacterToGlyphMap.TryGetValue(currentCodepoint, out _)) { break; } //Stop at the first missing glyph - if (!currentCodepoint.IsBreakChar && - currentCodepoint.GeneralCategory != GeneralCategory.Control && - !glyphTypeface.TryGetGlyph(currentCodepoint, out _)) + if (!currentCodepoint.IsBreakChar && + currentCodepoint.GeneralCategory != GeneralCategory.Control && + !glyphTypeface.CharacterToGlyphMap.TryGetValue(currentCodepoint, out _)) { break; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 3e7500c3073..ae2ed3003ae 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -701,7 +701,7 @@ public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; var glyphTypeface = properties.CachedGlyphTypeface; - var glyph = glyphTypeface.GetGlyph(s_empty[0]); + var glyph = glyphTypeface.CharacterToGlyphMap[s_empty[0]]; var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index ce9f85a5e2f..ca49161781d 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.IO; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Metadata; namespace Avalonia.Platform @@ -30,12 +29,12 @@ public interface IFontManagerImpl /// The font weight. /// The font stretch. /// The culture. - /// The matching typeface. + /// The matching typeface. /// /// True, if the could match the character to specified parameters, False otherwise. /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface); + FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, [NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface); /// /// Tries to get a glyph typeface for specified parameters. @@ -61,10 +60,7 @@ bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weigh /// True, if the could create the glyph typeface, False otherwise. /// bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); - } - internal interface IFontManagerImpl2 : IFontManagerImpl - { /// /// Tries to get a list of typefaces for the specified family name. /// diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index 2342f323073..620f22b77ca 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -11,10 +11,6 @@ namespace Avalonia.Platform [Unstable] public interface IGlyphRunImpl : IDisposable { - /// - /// Gets the for the . - /// - IGlyphTypeface GlyphTypeface { get; } /// /// Gets the em size used for rendering the . diff --git a/src/Avalonia.Base/Platform/ITextShaperImpl.cs b/src/Avalonia.Base/Platform/ITextShaperImpl.cs index a651b49e64f..9cd19932ae8 100644 --- a/src/Avalonia.Base/Platform/ITextShaperImpl.cs +++ b/src/Avalonia.Base/Platform/ITextShaperImpl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; @@ -17,5 +18,13 @@ public interface ITextShaperImpl /// Text shaper options to customize the shaping process. /// A shaped glyph run. ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options); - } + + /// + /// Creates a text shaper typeface based on the specified glyph typeface. + /// + /// The glyph typeface to use as the basis for the text shaper typeface. + /// An instance of that represents the text shaping functionality for the + /// specified glyph typeface. + ITextShaperTypeface CreateTypeface(IGlyphTypeface glyphTypeface); + } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs index 986692176bd..087bce5a14a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Media; -using Avalonia.Platform; namespace Avalonia.Rendering.Composition.Server { @@ -30,15 +29,15 @@ public double GetMaxHeight() return maxHeight; } - public DiagnosticTextRenderer(IGlyphTypeface typeface, double fontRenderingEmSize) + public DiagnosticTextRenderer(IGlyphTypeface glyphTypeface, double fontRenderingEmSize) { var chars = new char[LastChar - FirstChar + 1]; for (var c = FirstChar; c <= LastChar; c++) { var index = c - FirstChar; chars[index] = c; - var glyph = typeface.GetGlyph(c); - _runs[index] = new GlyphRun(typeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph }); + var glyph = glyphTypeface.CharacterToGlyphMap[c]; + _runs[index] = new GlyphRun(glyphTypeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph }); } } diff --git a/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs b/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs index 7574c550429..6c85ca40fcd 100644 --- a/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs +++ b/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs @@ -1,5 +1,4 @@ using Avalonia.Compatibility; -using Avalonia.Controls; using Avalonia.Logging; namespace Avalonia @@ -8,6 +7,9 @@ public static class AppBuilderDesktopExtensions { public static AppBuilder UsePlatformDetect(this AppBuilder builder) { + // Always load HarfBuzz on desktop platforms + LoadHarfBuzz(builder); + // We don't have the ability to load every assembly right now, so we are // stuck with manual configuration here // Helpers are extracted to separate methods to take the advantage of the fact @@ -20,7 +22,7 @@ public static AppBuilder UsePlatformDetect(this AppBuilder builder) LoadWin32(builder); LoadSkia(builder); } - else if(OperatingSystemEx.IsMacOS()) + else if (OperatingSystemEx.IsMacOS()) { LoadAvaloniaNative(builder); LoadSkia(builder); @@ -49,5 +51,8 @@ static void LoadX11(AppBuilder builder) static void LoadSkia(AppBuilder builder) => builder.UseSkia(); + + static void LoadHarfBuzz(AppBuilder builder) + => builder.UseHarfBuzz(); } } diff --git a/src/Avalonia.Desktop/Avalonia.Desktop.csproj b/src/Avalonia.Desktop/Avalonia.Desktop.csproj index dd7f8fab635..7e1966611f9 100644 --- a/src/Avalonia.Desktop/Avalonia.Desktop.csproj +++ b/src/Avalonia.Desktop/Avalonia.Desktop.csproj @@ -9,6 +9,7 @@ + diff --git a/src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.Harfbuzz.csproj b/src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.Harfbuzz.csproj new file mode 100644 index 00000000000..ebc01222728 --- /dev/null +++ b/src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.Harfbuzz.csproj @@ -0,0 +1,22 @@ + + + $(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks);netstandard2.0 + true + true + true + + $(WarningsAsErrors);CS0618 + + + + + + + + + + + + + + diff --git a/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzApplicationExtensions.cs b/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzApplicationExtensions.cs new file mode 100644 index 00000000000..508c9ef525a --- /dev/null +++ b/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzApplicationExtensions.cs @@ -0,0 +1,27 @@ +using Avalonia.Harfbuzz; +using Avalonia.Platform; + +namespace Avalonia +{ + + /// + /// Configures the application to use HarfBuzz for text shaping. + /// + /// This method adds a HarfBuzz-based text shaper implementation to the application, enabling + /// advanced text shaping capabilities. + public static class HarfBuzzApplicationExtensions + { + /// + /// Configures the application to use HarfBuzz for text shaping. + /// + /// This method integrates HarfBuzz, a text shaping engine, into the application, + /// enabling advanced text layout and rendering capabilities. + /// The instance to configure. + /// The configured instance. + public static AppBuilder UseHarfBuzz(this AppBuilder builder) + { + return builder.With(new HarfBuzzTextShaper()); + } + } + +} diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTextShaper.cs similarity index 88% rename from src/Skia/Avalonia.Skia/TextShaperImpl.cs rename to src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTextShaper.cs index efce67e90b2..1d779e10152 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTextShaper.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Globalization; using System.Runtime.InteropServices; +using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -10,9 +11,9 @@ using Buffer = HarfBuzzSharp.Buffer; using GlyphInfo = HarfBuzzSharp.GlyphInfo; -namespace Avalonia.Skia +namespace Avalonia.Harfbuzz { - internal class TextShaperImpl : ITextShaperImpl + public class HarfBuzzTextShaper : ITextShaperImpl { [ThreadStatic] private static Buffer? s_buffer; @@ -22,7 +23,14 @@ internal class TextShaperImpl : ITextShaperImpl public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var textSpan = text.Span; - var typeface = options.Typeface; + + var glyphTypeface = options.Typeface; + + if (glyphTypeface.TextShaperTypeface is not HarfBuzzTypeface harfBuzzTypeface) + { + throw new NotSupportedException("The provided GlyphTypeface is not supported by this text shaper."); + } + var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; var culture = options.Culture; @@ -45,7 +53,7 @@ public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions optio buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); - var font = ((GlyphTypefaceImpl)typeface).Font; + var font = harfBuzzTypeface.HBFont; font.Shape(buffer, GetFeatures(options)); @@ -60,7 +68,7 @@ public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions optio var bufferLength = buffer.Length; - var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var shapedBuffer = new ShapedBuffer(text, bufferLength, glyphTypeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); @@ -80,11 +88,11 @@ public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions optio if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') { - glyphIndex = typeface.GetGlyph(' '); + glyphIndex = glyphTypeface.CharacterToGlyphMap[' ']; glyphAdvance = options.IncrementalTabWidth > 0 ? options.IncrementalTabWidth : - 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; + 4 * glyphTypeface.GetGlyphAdvance(glyphIndex) * textScale; } shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); @@ -93,6 +101,12 @@ public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions optio return shapedBuffer; } + + public ITextShaperTypeface CreateTypeface(IGlyphTypeface glyphTypeface) + { + return new HarfBuzzTypeface(glyphTypeface); + } + private static void MergeBreakPair(Buffer buffer) { var length = buffer.Length; @@ -190,18 +204,18 @@ private static Feature[] GetFeatures(TextShaperOptions options) } var features = new Feature[options.FontFeatures.Count]; - + for (var i = 0; i < options.FontFeatures.Count; i++) { var fontFeature = options.FontFeatures[i]; features[i] = new Feature( - Tag.Parse(fontFeature.Tag), + Tag.Parse(fontFeature.Tag), (uint)fontFeature.Value, (uint)fontFeature.Start, (uint)fontFeature.End); } - + return features; } } diff --git a/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTypeface.cs b/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTypeface.cs new file mode 100644 index 00000000000..6b8208fca5f --- /dev/null +++ b/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTypeface.cs @@ -0,0 +1,67 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Media; +using HarfBuzzSharp; + +namespace Avalonia.Harfbuzz +{ + internal class HarfBuzzTypeface : ITextShaperTypeface + { + public HarfBuzzTypeface(IGlyphTypeface glyphTypeface) + { + GlyphTypeface = glyphTypeface; + + HBFace = new Face(GetTable) { UnitsPerEm = glyphTypeface.Metrics.DesignEmHeight }; + + HBFont = new Font(HBFace); + + HBFont.SetFunctionsOpenType(); + } + + public IGlyphTypeface GlyphTypeface { get; } + public Face HBFace { get; } + public Font HBFont { get; } + + private Blob? GetTable(Face face, Tag tag) + { + if (!GlyphTypeface.PlatformTypeface.TryGetTable((uint)tag, out var table)) + { + return null; + } + + // If table is backed by managed array, pin it and avoid copy. + if (MemoryMarshal.TryGetArray(table, out var seg)) + { + var handle = GCHandle.Alloc(seg.Array!, GCHandleType.Pinned); + var basePtr = handle.AddrOfPinnedObject(); + var ptr = IntPtr.Add(basePtr, seg.Offset); + + var release = new ReleaseDelegate(() => handle.Free()); + + return new Blob(ptr, seg.Count, MemoryMode.ReadOnly, release); + } + + // Fallback: allocate native memory and copy + var nativePtr = Marshal.AllocHGlobal(table.Length); + + unsafe + { + fixed (byte* src = table.Span) + { + System.Buffer.MemoryCopy(src, (void*)nativePtr, table.Length, table.Length); + } + } + + var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeHGlobal(nativePtr)); + + return new Blob(nativePtr, table.Length, MemoryMode.ReadOnly, releaseDelegate); + } + + public void Dispose() + { + HBFont.Dispose(); + HBFace.Dispose(); + } + + } +} diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index f025274371c..403bd06ffce 100644 --- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -5,6 +5,7 @@ + diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 2774bf63fe0..a280deb8a5d 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -5,11 +5,9 @@ using System.Linq; using System.Runtime.InteropServices; using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Utilities; using Avalonia.Media.Imaging; using Avalonia.Media.TextFormatting; +using Avalonia.Platform; namespace Avalonia.Headless { @@ -19,8 +17,7 @@ public static void Initialize() { AvaloniaLocator.CurrentMutable .Bind().ToConstant(new HeadlessPlatformRenderInterface()) - .Bind().ToConstant(new HeadlessFontManagerStub()) - .Bind().ToConstant(new HeadlessTextShaperStub()); + .Bind().ToConstant(new HeadlessFontManagerStub()); } public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) => this; diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 4286904d4db..5f7bd97931c 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -3,14 +3,14 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; +using Avalonia; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Media; -using Avalonia.Media.TextFormatting; -using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Media.Fonts; using Avalonia.Platform; namespace Avalonia.Headless @@ -66,34 +66,20 @@ public void Dispose() { } } } - internal class HeadlessGlyphTypefaceImpl : IGlyphTypeface + internal class HeadlessPlatformTypeface : IPlatformTypeface { - public HeadlessGlyphTypefaceImpl(string familyName, FontStyle style, FontWeight weight, FontStretch stretch) - { - FamilyName = familyName; - Style = style; - Weight = weight; - Stretch = stretch; - } + private readonly UnmanagedFontMemory _fontMemory; - public FontMetrics Metrics => new FontMetrics + public HeadlessPlatformTypeface(Stream stream) { - DesignEmHeight = 10, - Ascent = 2, - Descent = 10, - IsFixedPitch = true, - LineGap = 0, - UnderlinePosition = 2, - UnderlineThickness = 1, - StrikethroughPosition = 2, - StrikethroughThickness = 1 - }; + _fontMemory = UnmanagedFontMemory.LoadFromStream(stream); - public int GlyphCount => 1337; + var dummy = new GlyphTypeface(this, FontSimulations.None); - public FontSimulations FontSimulations => FontSimulations.None; - - public string FamilyName { get; } + Weight = dummy.Weight; + Style = dummy.Style; + Stretch = dummy.Stretch; + } public FontWeight Weight { get; } @@ -103,260 +89,217 @@ public HeadlessGlyphTypefaceImpl(string familyName, FontStyle style, FontWeight public void Dispose() { + _fontMemory.Dispose(); } - public ushort GetGlyph(uint codepoint) + public bool TryGetStream([NotNullWhen(true)] out Stream? stream) { - return (ushort)codepoint; - } + var data = _fontMemory.Memory.Span; - public bool TryGetGlyph(uint codepoint, out ushort glyph) - { - glyph = 8; + stream = new MemoryStream(data.ToArray()); return true; } - public int GetGlyphAdvance(ushort glyph) - { - return 8; - } + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory table) => _fontMemory.TryGetTable(tag, out table); + } +} - public int[] GetGlyphAdvances(ReadOnlySpan glyphs) - { - var advances = new int[glyphs.Length]; +internal class HeadlessGlyphTypeface : IGlyphTypeface +{ + private readonly IGlyphTypeface _inner; - for (var i = 0; i < advances.Length; i++) - { - advances[i] = 8; - } + public HeadlessGlyphTypeface(IGlyphTypeface inner, string familyName) + { + _inner = inner; + FamilyName = familyName; + } - return advances; - } + public string FamilyName { get; } - public ushort[] GetGlyphs(ReadOnlySpan codepoints) - { - return codepoints.ToArray().Select(x => (ushort)x).ToArray(); - } + public string TypographicFamilyName => FamilyName; - public bool TryGetTable(uint tag, out byte[] table) - { - table = null!; - return false; - } + public IReadOnlyDictionary FamilyNames => _inner.FamilyNames; - public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) - { - metrics = new GlyphMetrics - { - Width = 10, - Height = 10 - }; + public IReadOnlyList SupportedFeatures => _inner.SupportedFeatures; - return true; - } - } + public IReadOnlyDictionary FaceNames => _inner.FaceNames; - internal class HeadlessTextShaperStub : ITextShaperImpl - { - public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) - { - var typeface = options.Typeface; - var fontRenderingEmSize = options.FontRenderingEmSize; - var bidiLevel = options.BidiLevel; - var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); - var textSpan = text.Span; - var textStartIndex = TextTestHelper.GetStartCharIndex(text); - - for (var i = 0; i < shapedBuffer.Length;) - { - var glyphCluster = i + textStartIndex; + public FontWeight Weight => _inner.Weight; - var codepoint = Codepoint.ReadAt(textSpan, i, out var count); + public FontStyle Style => _inner.Style; - var glyphIndex = typeface.GetGlyph(codepoint); + public FontStretch Stretch => _inner.Stretch; - for (var j = 0; j < count; ++j) - { - shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); - } + public uint GlyphCount => _inner.GlyphCount; - i += count; - } + public FontSimulations FontSimulations => _inner.FontSimulations; - return shapedBuffer; - } - } + public FontMetrics Metrics => _inner.Metrics; - internal class HeadlessFontManagerStub : IFontManagerImpl - { - private readonly string _defaultFamilyName; + public IReadOnlyDictionary CharacterToGlyphMap => _inner.CharacterToGlyphMap; - public HeadlessFontManagerStub(string defaultFamilyName = "Default") - { - _defaultFamilyName = defaultFamilyName; - } + public IPlatformTypeface PlatformTypeface => _inner.PlatformTypeface; - public int TryCreateGlyphTypefaceCount { get; private set; } + public ITextShaperTypeface TextShaperTypeface => _inner.TextShaperTypeface; - public string GetDefaultFontFamilyName() - { - return _defaultFamilyName; - } + public void Dispose() => _inner.Dispose(); - string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) - { - return new[] { _defaultFamilyName }; - } - - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, - CultureInfo? culture, out Typeface fontKey) - { - fontKey = new Typeface(_defaultFamilyName); - - return false; - } - - public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) - { - glyphTypeface = null; + public ushort GetGlyphAdvance(ushort glyph) => _inner.GetGlyphAdvance(glyph); - TryCreateGlyphTypefaceCount++; + public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) => _inner.TryGetGlyphMetrics(glyph, out metrics); +} - if (familyName == "Unknown") - { - return false; - } +internal class HeadlessFontManagerStub : IFontManagerImpl +{ + private readonly string _defaultFamilyName = "avares://Avalonia.Fonts.Inter/Assets#Inter"; - glyphTypeface = new HeadlessGlyphTypefaceImpl(familyName, style, weight, stretch); + public int TryCreateGlyphTypefaceCount { get; private set; } - return true; - } + public string GetDefaultFontFamilyName() => _defaultFamilyName; - public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) - { - glyphTypeface = new HeadlessGlyphTypefaceImpl( - FontFamily.DefaultFontFamilyName, - fontSimulations.HasFlag(FontSimulations.Oblique) ? FontStyle.Italic : FontStyle.Normal, - fontSimulations.HasFlag(FontSimulations.Bold) ? FontWeight.Bold : FontWeight.Normal, - FontStretch.Normal); + string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) + { + return new[] { _defaultFamilyName }; + } - TryCreateGlyphTypefaceCount++; + public bool TryMatchCharacter( + int codepoint, + FontStyle fontStyle, + FontWeight fontWeight, + FontStretch fontStretch, + CultureInfo? culture, + out IPlatformTypeface platformTypeface) + { + platformTypeface = null!; - return true; - } + return false; } - internal class HeadlessFontManagerWithMultipleSystemFontsStub : IFontManagerImpl + public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - private readonly string[] _installedFontFamilyNames; - private readonly string _defaultFamilyName; + glyphTypeface = null; - public HeadlessFontManagerWithMultipleSystemFontsStub( - string[] installedFontFamilyNames, - string defaultFamilyName = "Default") + if (familyName == "MyFont") { - _installedFontFamilyNames = installedFontFamilyNames; - _defaultFamilyName = defaultFamilyName; + glyphTypeface = new HeadlessGlyphTypeface(Typeface.Default.GlyphTypeface, familyName); } - public int TryCreateGlyphTypefaceCount { get; private set; } + TryCreateGlyphTypefaceCount++; - public string GetDefaultFontFamilyName() - { - return _defaultFamilyName; - } + return glyphTypeface != null; + } - string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) - { - return _installedFontFamilyNames; - } + public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) + { + glyphTypeface = new GlyphTypeface(new HeadlessPlatformTypeface(stream), fontSimulations); - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, - CultureInfo? culture, out Typeface fontKey) - { - fontKey = new Typeface(_defaultFamilyName); + TryCreateGlyphTypefaceCount++; - return false; - } + return true; + } - public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) - { - glyphTypeface = null; + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + { + throw new NotImplementedException(); + } +} - TryCreateGlyphTypefaceCount++; +internal class HeadlessFontManagerWithMultipleSystemFontsStub : IFontManagerImpl +{ + private readonly string[] _installedFontFamilyNames; + private readonly string _defaultFamilyName; - if (familyName == "Unknown") - { - return false; - } + public HeadlessFontManagerWithMultipleSystemFontsStub( + string[] installedFontFamilyNames, + string defaultFamilyName = "Default") + { + _installedFontFamilyNames = installedFontFamilyNames; + _defaultFamilyName = defaultFamilyName; + } - glyphTypeface = new HeadlessGlyphTypefaceImpl(familyName, style, weight, stretch); + public int TryCreateGlyphTypefaceCount { get; private set; } - return true; - } + string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) + { + return _installedFontFamilyNames; + } - public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) - { - glyphTypeface = new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal); + public string GetDefaultFontFamilyName() + { + return _defaultFamilyName; + } - return true; - } + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + throw new NotImplementedException(); } - internal class HeadlessIconLoaderStub : IPlatformIconLoader + public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - private class IconStub : IWindowIconImpl - { - public void Save(Stream outputStream) - { + throw new NotImplementedException(); + } - } - } - public IWindowIconImpl LoadIcon(string fileName) - { - return new IconStub(); - } + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + { + throw new NotImplementedException(); + } - public IWindowIconImpl LoadIcon(Stream stream) - { - return new IconStub(); - } + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + throw new NotImplementedException(); + } +} - public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) +internal class HeadlessIconLoaderStub : IPlatformIconLoader +{ + private class IconStub : IWindowIconImpl + { + public void Save(Stream outputStream) { - return new IconStub(); + } } + public IWindowIconImpl LoadIcon(string fileName) + { + return new IconStub(); + } - internal class HeadlessScreensStub : ScreensBase + public IWindowIconImpl LoadIcon(Stream stream) { - protected override IReadOnlyList GetAllScreenKeys() => new[] { 1 }; + return new IconStub(); + } + + public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) + { + return new IconStub(); + } +} + +internal class HeadlessScreensStub : ScreensBase +{ + protected override IReadOnlyList GetAllScreenKeys() => new[] { 1 }; - protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key); + protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key); - private class PlatformScreenStub : PlatformScreen + private class PlatformScreenStub : PlatformScreen + { + public PlatformScreenStub(int key) : base(new PlatformHandle((nint)key, nameof(HeadlessScreensStub))) { - public PlatformScreenStub(int key) : base(new PlatformHandle((nint)key, nameof(HeadlessScreensStub))) - { - Scaling = 1; - Bounds = WorkingArea = new PixelRect(0, 0, 1920, 1280); - IsPrimary = true; - } + Scaling = 1; + Bounds = WorkingArea = new PixelRect(0, 0, 1920, 1280); + IsPrimary = true; } } +} - internal static class TextTestHelper +internal static class TextTestHelper +{ + public static int GetStartCharIndex(ReadOnlyMemory text) { - public static int GetStartCharIndex(ReadOnlyMemory 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; } } diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index eb1833193c7..02d923adc05 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Skia { - internal class FontManagerImpl : IFontManagerImpl, IFontManagerImpl2 + internal class FontManagerImpl : IFontManagerImpl { private SKFontManager _skFontManager = SKFontManager.Default; @@ -30,8 +30,13 @@ public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) [ThreadStatic] private static string[]? t_languageTagBuffer; - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface fontKey) + public bool TryMatchCharacter( + int codepoint, + FontStyle fontStyle, + FontWeight fontWeight, + FontStretch fontStretch, + CultureInfo? culture, + [NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface) { SKFontStyle skFontStyle; @@ -63,17 +68,12 @@ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, if (skTypeface != null) { - // ToDo: create glyph typeface here to get the correct style/weight/stretch - fontKey = new Typeface( - skTypeface.FamilyName, - skTypeface.FontStyle.Slant.ToAvalonia(), - (FontWeight)skTypeface.FontStyle.Weight, - (FontStretch)skTypeface.FontStyle.Width); + platformTypeface = new SkiaTypeface(skTypeface, FontSimulations.None); return true; } - fontKey = default; + platformTypeface = null; return false; } @@ -104,7 +104,7 @@ public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeigh fontSimulations |= FontSimulations.Oblique; } - glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations); + glyphTypeface = new GlyphTypeface(new SkiaTypeface(skTypeface, fontSimulations), fontSimulations); return true; } @@ -115,7 +115,7 @@ public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulation if (skTypeface != null) { - glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations); + glyphTypeface = new GlyphTypeface(new SkiaTypeface(skTypeface, fontSimulations), fontSimulations); return true; } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 0cc069308ff..20927bb6d1c 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Skia { internal class GlyphRunImpl : IGlyphRunImpl { - private readonly GlyphTypefaceImpl _glyphTypefaceImpl; + private readonly SkiaTypeface _glyphTypefaceImpl; private readonly ushort[] _glyphIndices; private readonly SKPoint[] _glyphPositions; @@ -36,7 +36,7 @@ public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, throw new ArgumentNullException(nameof(glyphInfos)); } - _glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; + _glyphTypefaceImpl = (SkiaTypeface)glyphTypeface.PlatformTypeface; FontRenderingEmSize = fontRenderingEmSize; var count = glyphInfos.Count; @@ -86,8 +86,6 @@ public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, Bounds = runBounds.Translate(new Vector(baselineOrigin.X, baselineOrigin.Y)); } - public IGlyphTypeface GlyphTypeface => _glyphTypefaceImpl; - public double FontRenderingEmSize { get; } public Point BaselineOrigin { get; } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs deleted file mode 100644 index 703496a8343..00000000000 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Runtime.InteropServices; -using Avalonia.Media; -using Avalonia.Media.Fonts; -using Avalonia.Media.Fonts.Tables; -using Avalonia.Media.Fonts.Tables.Name; -using HarfBuzzSharp; -using SkiaSharp; - -namespace Avalonia.Skia -{ - internal class GlyphTypefaceImpl : IGlyphTypeface2 - { - private bool _isDisposed; - private readonly NameTable? _nameTable; - private readonly OS2Table? _os2Table; - private readonly HorizontalHeadTable? _hhTable; - private IReadOnlyList? _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(1); - var faceNames = new Dictionary(1); - - foreach (var nameRecord in _nameTable) - { - if(nameRecord.NameID == KnownNameIds.FontFamilyName) - { - if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0) - { - continue; - } - - if (!familyNames.ContainsKey(nameRecord.LanguageID)) - { - familyNames[nameRecord.LanguageID] = nameRecord.Value; - } - } - - if(nameRecord.NameID == KnownNameIds.FontSubfamilyName) - { - if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0) - { - continue; - } - - if (!faceNames.ContainsKey(nameRecord.LanguageID)) - { - faceNames[nameRecord.LanguageID] = nameRecord.Value; - } - } - } - - FamilyNames = familyNames; - FaceNames = faceNames; - } - else - { - FamilyNames = new Dictionary { { (ushort)CultureInfo.InvariantCulture.LCID, FamilyName } }; - FaceNames = new Dictionary { { (ushort)CultureInfo.InvariantCulture.LCID, Weight.ToString() } }; - } - } - - public string TypographicFamilyName { get; } - - public IReadOnlyDictionary FamilyNames { get; } - - public IReadOnlyDictionary FaceNames { get; } - - public IReadOnlyList SupportedFeatures - { - get - { - if (_supportedFeatures != null) - { - return _supportedFeatures; - } - - var gPosFeatures = FeatureListTable.LoadGPos(this); - var gSubFeatures = FeatureListTable.LoadGSub(this); - - var supportedFeatures = new List(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; - } - - /// - 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; - } - - /// - public ushort[] GetGlyphs(ReadOnlySpan 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; - } - - /// - public int GetGlyphAdvance(ushort glyph) - { - return Font.GetHorizontalGlyphAdvance(glyph); - } - - /// - public int[] GetGlyphAdvances(ReadOnlySpan glyphs) - { - var glyphIndices = new uint[glyphs.Length]; - - for (var i = 0; i < glyphs.Length; i++) - { - glyphIndices[i] = glyphs[i]; - } - - return Font.GetHorizontalGlyphAdvances(glyphIndices); - } - - private static FontStyle GetFontStyle(OS2Table.FontStyleSelection styleSelection) - { - if ((styleSelection & OS2Table.FontStyleSelection.ITALIC) != 0) - { - return FontStyle.Italic; - } - - if ((styleSelection & OS2Table.FontStyleSelection.OBLIQUE) != 0) - { - return FontStyle.Oblique; - } - - return FontStyle.Normal; - } - - private Blob? GetTable(Face face, Tag tag) - { - var size = SKTypeface.GetTableSize(tag); - - var data = Marshal.AllocCoTaskMem(size); - - var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data)); - - return SKTypeface.TryGetTableData(tag, 0, size, data) ? - new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null; - } - - public SKFont CreateSKFont(float size) - => new(SKTypeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f) - { - LinearMetrics = true, - Embolden = (FontSimulations & FontSimulations.Bold) != 0 - }; - - private void Dispose(bool disposing) - { - if (_isDisposed) - { - return; - } - - _isDisposed = true; - - if (!disposing) - { - return; - } - - Font.Dispose(); - Face.Dispose(); - SKTypeface.Dispose(); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public bool TryGetTable(uint tag, out byte[] table) - { - return SKTypeface.TryGetTableData(tag, out table); - } - - public bool TryGetStream([NotNullWhen(true)] out Stream? stream) - { - try - { - var asset = SKTypeface.OpenStream(); - var size = asset.Length; - var buffer = new byte[size]; - - asset.Read(buffer, size); - - stream = new MemoryStream(buffer); - - return true; - } - catch - { - stream = null; - - return false; - } - } - } -} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 05dbd684ef1..40ef53bb212 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -2,15 +2,15 @@ using System.Collections.Generic; using System.IO; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; +using Avalonia.Metal; using Avalonia.OpenGL; using Avalonia.Platform; -using Avalonia.Media.Imaging; +using Avalonia.Skia.Metal; using Avalonia.Skia.Vulkan; using Avalonia.Vulkan; using SkiaSharp; -using Avalonia.Media.TextFormatting; -using Avalonia.Metal; -using Avalonia.Skia.Metal; namespace Avalonia.Skia { @@ -81,7 +81,7 @@ public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGe 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."); } diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index 27f2631db80..a81b6d5413e 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -21,8 +21,7 @@ public static void Initialize(SkiaOptions options) AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface) - .Bind().ToConstant(new FontManagerImpl()) - .Bind().ToConstant(new TextShaperImpl()); + .Bind().ToConstant(new FontManagerImpl()); } /// diff --git a/src/Skia/Avalonia.Skia/SkiaTypeface.cs b/src/Skia/Avalonia.Skia/SkiaTypeface.cs new file mode 100644 index 00000000000..0356a94e618 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SkiaTypeface.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using SkiaSharp; + +namespace Avalonia.Skia +{ + internal class SkiaTypeface : IPlatformTypeface + { + public SkiaTypeface(SKTypeface typeface, FontSimulations fontSimulations) + { + SKTypeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); + FontSimulations = fontSimulations; + Weight = (FontWeight)typeface.FontWeight; + Style = typeface.FontStyle.Slant.ToAvalonia(); + Stretch = (FontStretch)typeface.FontWidth; + } + + public SKTypeface SKTypeface { get; } + + public FontSimulations FontSimulations { get; } + + public FontWeight Weight { get; } + + public FontStyle Style { get; } + + public FontStretch Stretch { get; } + + public SKFont CreateSKFont(float size) + { + return new(SKTypeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f) + { + LinearMetrics = true, + Embolden = (FontSimulations & FontSimulations.Bold) != 0 + }; + } + + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory 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(); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index e3aa990f14e..9db6d52f8f9 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -9,8 +9,8 @@ using Avalonia.Media.Imaging; using Avalonia.Media.TextFormatting; using Avalonia.Platform; -using GlyphRun = Avalonia.Media.GlyphRun; using SharpDX.Mathematics.Interop; +using GlyphRun = Avalonia.Media.GlyphRun; namespace Avalonia { @@ -111,8 +111,7 @@ public static void Initialize() InitializeDirect2D(); AvaloniaLocator.CurrentMutable .Bind().ToConstant(s_instance) - .Bind().ToConstant(new FontManagerImpl()) - .Bind().ToConstant(new TextShaperImpl()); + .Bind().ToConstant(new FontManagerImpl()); SharpDX.Configuration.EnableReleaseOnFinalizer = true; } @@ -192,7 +191,7 @@ public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsCon public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { - if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface) + if (glyphRun.GlyphTypeface.PlatformTypeface is not DWriteTypeface glyphTypeface) { throw new InvalidOperationException("PlatformImpl can't be null."); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteTypeface.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteTypeface.cs new file mode 100644 index 00000000000..83256183a8d --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/DWriteTypeface.cs @@ -0,0 +1,111 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using HarfBuzzSharp; +using SharpDX.DirectWrite; + +namespace Avalonia.Direct2D1.Media +{ + internal class DWriteTypeface : IPlatformTypeface + { + private bool _isDisposed; + + public DWriteTypeface(SharpDX.DirectWrite.Font font) + { + DWFont = font; + + FontFace = new FontFace(DWFont).QueryInterface(); + + Weight = (Avalonia.Media.FontWeight)DWFont.Weight; + + Style = (Avalonia.Media.FontStyle)DWFont.Style; + + Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch; + } + + private static uint SwapBytes(uint x) + { + x = (x >> 16) | (x << 16); + + return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8); + } + + public SharpDX.DirectWrite.Font DWFont { get; } + + public FontFace1 FontFace { get; } + + public Face Face { get; } + + public HarfBuzzSharp.Font Font { get; } + + public Avalonia.Media.FontWeight Weight { get; } + + public Avalonia.Media.FontStyle Style { get; } + + public Avalonia.Media.FontStretch Stretch { get; } + + private void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!disposing) + { + return; + } + + Font?.Dispose(); + Face?.Dispose(); + FontFace?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory table) + { + table = default; + + var dwTag = (int)SwapBytes((uint)tag); + + if (FontFace.TryGetFontTable(dwTag, out var tableData, out _)) + { + table = tableData.ToArray(); + + return true; + } + + return false; + } + + public bool TryGetStream([NotNullWhen(true)] out Stream stream) + { + stream = default; + + var files = FontFace.GetFiles(); + + if (files.Length > 0) + { + var file = files[0]; + + var referenceKey = file.GetReferenceKey(); + + stream = referenceKey.ToDataStream(); + + return true; + } + + return false; + } + } +} + diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 4c3e798d4b1..3d831fffbc6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -1,4 +1,6 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using Avalonia.Media; @@ -33,7 +35,7 @@ public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, out Typeface typeface) + FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, out IPlatformTypeface typeface) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -51,7 +53,7 @@ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - typeface = new Typeface(fontFamilyName, fontStyle, fontWeight, fontStretch); + typeface = new DWriteTypeface(font); return true; } @@ -78,7 +80,7 @@ public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeigh (SharpDX.DirectWrite.FontStretch)stretch, (SharpDX.DirectWrite.FontStyle)style); - glyphTypeface = new GlyphTypefaceImpl(font); + glyphTypeface = new GlyphTypeface(new DWriteTypeface(font), FontSimulations.None); return true; } @@ -102,7 +104,7 @@ public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulation { var font = fontFamily.GetFont(0); - glyphTypeface = new GlyphTypefaceImpl(font); + glyphTypeface = new GlyphTypeface(new DWriteTypeface(font), FontSimulations.None); return true; } @@ -112,5 +114,10 @@ public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulation return false; } + + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList familyTypefaces) + { + throw new NotSupportedException(); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 729797203e4..ae3872114c8 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Direct2D1.Media { internal class GlyphRunImpl : IGlyphRunImpl { - private readonly GlyphTypefaceImpl _glyphTypefaceImpl; + private readonly DWriteTypeface _glyphTypefaceImpl; private readonly short[] _glyphIndices; private readonly float[] _glyphAdvances; @@ -22,7 +22,7 @@ internal class GlyphRunImpl : IGlyphRunImpl public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin) { - _glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; + _glyphTypefaceImpl = (DWriteTypeface)glyphTypeface.PlatformTypeface; FontRenderingEmSize = fontRenderingEmSize; BaselineOrigin = baselineOrigin; @@ -64,14 +64,14 @@ public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, AscenderOffset = (float)y }; - if (_glyphTypefaceImpl.TryGetGlyphMetrics(glyphInfos[i].GlyphIndex, out var metrics)) + if (glyphTypeface.TryGetGlyphMetrics(glyphInfos[i].GlyphIndex, out var metrics)) { // Found metrics with negative height, prefer to adjust it to positive. var ybearing = metrics.YBearing; var height = metrics.Height; if (height < 0) { - height = -height; + height = (ushort)-height; } // Not entirely sure about why we need to do this, but it seems to work @@ -111,8 +111,6 @@ public SharpDX.DirectWrite.GlyphRun GlyphRun } } - public IGlyphTypeface GlyphTypeface => _glyphTypefaceImpl; - public double FontRenderingEmSize { get; } public Point BaselineOrigin { get; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs deleted file mode 100644 index 01add0f0cb5..00000000000 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System; -using Avalonia.Media; -using HarfBuzzSharp; -using SharpDX.DirectWrite; -using FontMetrics = Avalonia.Media.FontMetrics; -using FontSimulations = Avalonia.Media.FontSimulations; -using GlyphMetrics = Avalonia.Media.GlyphMetrics; - -namespace Avalonia.Direct2D1.Media -{ - internal class GlyphTypefaceImpl : IGlyphTypeface - { - private bool _isDisposed; - - public GlyphTypefaceImpl(SharpDX.DirectWrite.Font font) - { - DWFont = font; - - FontFace = new FontFace(DWFont).QueryInterface(); - - Face = new Face(GetTable); - - Font = new HarfBuzzSharp.Font(Face); - - Font.SetFunctionsOpenType(); - - Font.GetScale(out var xScale, out _); - - if (!Font.TryGetHorizontalFontExtents(out var fontExtents)) - { - Font.TryGetVerticalFontExtents(out fontExtents); - } - - Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition); - Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness); - Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition); - Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness); - - Metrics = new FontMetrics - { - DesignEmHeight = (short)xScale, - Ascent = -fontExtents.Ascender, - Descent = -fontExtents.Descender, - LineGap = fontExtents.LineGap, - UnderlinePosition = underlinePosition, - UnderlineThickness = underlineThickness, - StrikethroughPosition = strikethroughPosition, - StrikethroughThickness = strikethroughThickness, - IsFixedPitch = FontFace.IsMonospacedFont - }; - - FamilyName = DWFont.FontFamily.FamilyNames.GetString(0); - - Weight = (Avalonia.Media.FontWeight)DWFont.Weight; - - Style = (Avalonia.Media.FontStyle)DWFont.Style; - - Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch; - } - - private Blob GetTable(Face face, Tag tag) - { - var dwTag = (int)SwapBytes(tag); - - if (FontFace.TryGetFontTable(dwTag, out var tableData, out _)) - { - return new Blob(tableData.Pointer, tableData.Size, MemoryMode.ReadOnly, () => { }); - } - - return null; - } - - private static uint SwapBytes(uint x) - { - x = (x >> 16) | (x << 16); - - return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8); - } - - public SharpDX.DirectWrite.Font DWFont { get; } - - public FontFace1 FontFace { get; } - - public Face Face { get; } - - public HarfBuzzSharp.Font Font { get; } - - public FontMetrics Metrics { get; } - - public int GlyphCount { get; set; } - - public FontSimulations FontSimulations => FontSimulations.None; - - public string FamilyName { get; } - - public Avalonia.Media.FontWeight Weight { get; } - - public Avalonia.Media.FontStyle Style { get; } - - public Avalonia.Media.FontStretch Stretch { get; } - - /// - 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; - } - - /// - public ushort[] GetGlyphs(ReadOnlySpan 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; - } - - /// - public int GetGlyphAdvance(ushort glyph) - { - return Font.GetHorizontalGlyphAdvance(glyph); - } - - /// - public int[] GetGlyphAdvances(ReadOnlySpan glyphs) - { - var glyphIndices = new uint[glyphs.Length]; - - for (var i = 0; i < glyphs.Length; i++) - { - glyphIndices[i] = glyphs[i]; - } - - return Font.GetHorizontalGlyphAdvances(glyphIndices); - } - - public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) - { - metrics = default; - - if (!Font.TryGetGlyphExtents(glyph, out var extents)) - { - return false; - } - - metrics = new GlyphMetrics - { - XBearing = extents.XBearing, - YBearing = extents.YBearing, - Width = extents.Width, - Height = extents.Height - }; - - return true; - } - - private void Dispose(bool disposing) - { - if (_isDisposed) - { - return; - } - - _isDisposed = true; - - if (!disposing) - { - return; - } - - Font?.Dispose(); - Face?.Dispose(); - FontFace?.Dispose(); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public bool TryGetTable(uint tag, out byte[] table) - { - table = null; - var blob = Face.ReferenceTable(tag); - - if (blob.Length > 0) - { - table = blob.AsSpan().ToArray(); - - return true; - } - - return false; - } - } -} - diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs deleted file mode 100644 index 2d0edb8f379..00000000000 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Concurrent; -using System.Globalization; -using System.Runtime.InteropServices; -using Avalonia.Media.TextFormatting; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Platform; -using HarfBuzzSharp; -using Buffer = HarfBuzzSharp.Buffer; -using GlyphInfo = HarfBuzzSharp.GlyphInfo; - -namespace Avalonia.Direct2D1.Media -{ - internal class TextShaperImpl : ITextShaperImpl - { - private static readonly ConcurrentDictionary s_cachedLanguage = new(); - - public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) - { - var textSpan = text.Span; - var typeface = options.Typeface; - var fontRenderingEmSize = options.FontRenderingEmSize; - var bidiLevel = options.BidiLevel; - var culture = options.Culture; - - using (var buffer = new Buffer()) - { - // HarfBuzz needs the surrounding characters to correctly shape the text - var containingText = GetContainingMemory(text, out var start, out var length).Span; - buffer.AddUtf16(containingText, start, length); - - MergeBreakPair(buffer); - - buffer.GuessSegmentProperties(); - - buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - - var usedCulture = culture ?? CultureInfo.CurrentCulture; - - buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); - - var font = ((GlyphTypefaceImpl)typeface).Font; - - font.Shape(buffer, GetFeatures(options)); - - if (buffer.Direction == Direction.RightToLeft) - { - buffer.Reverse(); - } - - font.GetScale(out var scaleX, out _); - - var textScale = fontRenderingEmSize / scaleX; - - var bufferLength = buffer.Length; - - var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); - - var glyphInfos = buffer.GetGlyphInfoSpan(); - - var glyphPositions = buffer.GetGlyphPositionSpan(); - - for (var i = 0; i < bufferLength; i++) - { - var sourceInfo = glyphInfos[i]; - - var glyphIndex = (ushort)sourceInfo.Codepoint; - - var glyphCluster = (int)(sourceInfo.Cluster); - - var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; - - var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - - if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') - { - glyphIndex = typeface.GetGlyph(' '); - - glyphAdvance = options.IncrementalTabWidth > 0 ? - options.IncrementalTabWidth : - 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; - } - - shapedBuffer[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); - } - - return shapedBuffer; - } - } - - private static void MergeBreakPair(Buffer buffer) - { - var length = buffer.Length; - - var glyphInfos = buffer.GetGlyphInfoSpan(); - - var second = glyphInfos[length - 1]; - - if (!new Codepoint(second.Codepoint).IsBreakChar) - { - return; - } - - if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') - { - var first = glyphInfos[length - 2]; - - first.Codepoint = '\u200C'; - second.Codepoint = '\u200C'; - second.Cluster = first.Cluster; - - unsafe - { - fixed (GlyphInfo* p = &glyphInfos[length - 2]) - { - *p = first; - } - - fixed (GlyphInfo* p = &glyphInfos[length - 1]) - { - *p = second; - } - } - } - else - { - second.Codepoint = '\u200C'; - - unsafe - { - fixed (GlyphInfo* p = &glyphInfos[length - 1]) - { - *p = second; - } - } - } - } - - private static Vector GetGlyphOffset(ReadOnlySpan glyphPositions, int index, double textScale) - { - var position = glyphPositions[index]; - - var offsetX = position.XOffset * textScale; - - var offsetY = -position.YOffset * textScale; - - return new Vector(offsetX, offsetY); - } - - private static double GetGlyphAdvance(ReadOnlySpan glyphPositions, int index, double textScale) - { - // Depends on direction of layout - // glyphPositions[index].YAdvance * textScale; - return glyphPositions[index].XAdvance * textScale; - } - - private static ReadOnlyMemory GetContainingMemory(ReadOnlyMemory memory, out int start, out int length) - { - if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length)) - { - return containingString.AsMemory(); - } - - if (MemoryMarshal.TryGetArray(memory, out var segment)) - { - start = segment.Offset; - length = segment.Count; - return segment.Array.AsMemory(); - } - - if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager memoryManager, out start, out length)) - { - return memoryManager.Memory; - } - - // should never happen - throw new InvalidOperationException("Memory not backed by string, array or manager"); - } - - private static Feature[] GetFeatures(TextShaperOptions options) - { - if (options.FontFeatures is null || options.FontFeatures.Count == 0) - { - return Array.Empty(); - } - - var features = new Feature[options.FontFeatures.Count]; - - for (var i = 0; i < options.FontFeatures.Count; i++) - { - var fontFeature = options.FontFeatures[i]; - - features[i] = new Feature( - Tag.Parse(fontFeature.Tag), - (uint)fontFeature.Value, - (uint)fontFeature.Start, - (uint)fontFeature.End); - } - - return features; - } - } -} diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs index fe72a9dfd1b..2d9dec64403 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Headless; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -70,7 +69,7 @@ public void Should_Use_FontManagerOptions_FontFallback() { AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); - FontManager.Current.TryMatchCharacter(1, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, + FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, FontFamily.Default, null, out var typeface); Assert.Equal("MyFont", typeface.FontFamily.Name); diff --git a/tests/Avalonia.Base.UnitTests/Media/Fonts/UnmanagedFontMemoryTests.cs b/tests/Avalonia.Base.UnitTests/Media/Fonts/UnmanagedFontMemoryTests.cs new file mode 100644 index 00000000000..c324102d7c0 --- /dev/null +++ b/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(() => 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 { } + } + } + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index ca573fae906..737594e7af0 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -180,9 +180,7 @@ private static GlyphRun CreateGlyphRun(double[] glyphAdvances, int[] glyphCluste 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() diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs new file mode 100644 index 00000000000..d6c39a53988 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs @@ -0,0 +1,131 @@ +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Platform; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media +{ + public class GlyphTypefaceTests + { + private static string s_InterFontUri = "resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests"; + + [Fact] + public void Should_Load_Inter_Font() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.Equal("Inter", typeface.FamilyName); + } + + [Fact] + public void Should_Have_CharacterToGlyphMap_For_Common_Characters() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var map = typeface.CharacterToGlyphMap; + + Assert.NotNull(map); + + Assert.True(map.ContainsKey('A')); + Assert.True(map['A'] != 0); + + Assert.True(map.ContainsKey('a')); + Assert.True(map['a'] != 0); + + Assert.True(map.ContainsKey(' ')); + Assert.True(map[' '] != 0); + } + + [Fact] + public void GetGlyphAdvance_Should_Return_Advance_For_GlyphId() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var map = typeface.CharacterToGlyphMap; + + Assert.True(map.ContainsKey('A')); + + var glyphId = map['A']; + + // Ensure metrics are available for this glyph + Assert.True(typeface.TryGetGlyphMetrics(glyphId, out var metrics)); + + var advance = typeface.GetGlyphAdvance(glyphId); + + // Advance returned by GetGlyphAdvance should match the metrics width + Assert.Equal(metrics.Width, advance); + } + + private class CustomPlatformTypeface : IPlatformTypeface + { + private readonly UnmanagedFontMemory _fontMemory; + + public CustomPlatformTypeface(Stream stream) + { + _fontMemory = UnmanagedFontMemory.LoadFromStream(stream); + } + + public FontWeight Weight => FontWeight.Normal; + + public FontStyle Style => FontStyle.Normal; + + public FontStretch Stretch => FontStretch.Normal; + + public void Dispose() + { + _fontMemory.Dispose(); + } + + public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream) + { + var memory = _fontMemory.Memory; + + var handle = memory.Pin(); // MemoryHandle merken + stream = new PinnedUnmanagedMemoryStream(handle, memory.Length); + + return true; + } + + private sealed class PinnedUnmanagedMemoryStream : UnmanagedMemoryStream + { + private MemoryHandle _handle; + + public unsafe PinnedUnmanagedMemoryStream(MemoryHandle handle, long length) + : base((byte*)handle.Pointer, length) + { + _handle = handle; + } + + protected override void Dispose(bool disposing) + { + try + { + base.Dispose(disposing); + } + finally + { + _handle.Dispose(); + } + } + } + + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory table) => _fontMemory.TryGetTable(tag, out table); + } + } +} diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index d4930bbe2a6..026219ffd9f 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -10,6 +10,7 @@ + diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index bc90abad7ea..da2519188e7 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Avalonia.Controls; +using Avalonia.Harfbuzz; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Skia; @@ -35,7 +36,7 @@ public HugeTextLayout() if (s_useSkia) { testServices = testServices.With( - textShaperImpl: new TextShaperImpl(), + textShaperImpl: new HarfBuzzTextShaper(), fontManagerImpl: new FontManagerImpl()); } diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 271d38f6a70..014b1bc01cd 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -1,18 +1,18 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive.Subjects; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; -using Avalonia.Threading; -using Avalonia.UnitTests; -using Xunit; -using System.Collections.ObjectModel; -using System.Reactive.Subjects; -using Avalonia.Headless; +using Avalonia.Harfbuzz; using Avalonia.Input; using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.UnitTests; using Moq; +using Xunit; namespace Avalonia.Controls.UnitTests { @@ -371,7 +371,7 @@ public void Item_Search() Assert.Equal(textbox.Text, control.Text); }); } - + [Fact] public void Custom_TextSelector() { @@ -388,7 +388,7 @@ public void Custom_TextSelector() Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString())); }); } - + [Fact] public void Custom_ItemSelector() { @@ -405,7 +405,7 @@ public void Custom_ItemSelector() Assert.Equal(control.Text, control.ItemSelector(input, selectedItem)); }); } - + [Fact] public void Text_Validation() { @@ -420,7 +420,7 @@ public void Text_Validation() Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true); }); } - + [Fact] public void Text_Validation_TextBox_Errors_Binding() { @@ -429,20 +429,20 @@ public void Text_Validation_TextBox_Errors_Binding() // simulate the TemplateBinding that would be used within the AutoCompleteBox control theme for the inner PART_TextBox // DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" textbox.Bind(DataValidationErrors.ErrorsProperty, control.GetBindingObservable(DataValidationErrors.ErrorsProperty)); - + var exception = new InvalidCastException("failed validation"); var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); control.Bind(AutoCompleteBox.TextProperty, textObservable); Dispatcher.UIThread.RunJobs(); - + Assert.True(DataValidationErrors.GetHasErrors(control)); Assert.Equal([exception], DataValidationErrors.GetErrors(control)); - + Assert.True(DataValidationErrors.GetHasErrors(textbox)); Assert.Equal([exception], DataValidationErrors.GetErrors(textbox)); }); } - + [Fact] public void SelectedItem_Validation() { @@ -1197,7 +1197,7 @@ private void RunTest(Action test) AutoCompleteBox control = CreateControl(); control.ItemsSource = CreateSimpleStringArray(); TextBox textBox = GetTextBox(control); - var window = new Window {Content = control}; + var window = new Window { Content = control }; window.ApplyStyling(); window.ApplyTemplate(); window.Presenter.ApplyTemplate(); @@ -1265,7 +1265,7 @@ private IControlTemplate CreateTemplate() keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), standardCursorFactory: Mock.Of(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), fontManagerImpl: new HeadlessFontManagerStub()); private class TestContextMenu : ContextMenu diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index dd34455162e..4f1fd22de67 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Platform; using Avalonia.Threading; @@ -282,7 +283,7 @@ private static void TestSelectorScrolling(string selectorName, Action TestServices.MockThreadingInterface.With( fontManagerImpl: new HeadlessFontManagerStub(), standardCursorFactory: Mock.Of(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), renderInterface: new HeadlessPlatformRenderInterface()); private static IControlTemplate CreateTemplate() diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 2ebac5b3a19..6083cdd1b93 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Layout; @@ -1238,7 +1239,7 @@ public static IDisposable Start() keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub())); + textShaperImpl: new HarfBuzzTextShaper())); } private class ItemsControlWithContainer : ItemsControl diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index ceef06a2135..0ebbbdd9b9c 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -13,7 +13,6 @@ using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; -using Avalonia.Markup.Xaml.Templates; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -775,9 +774,9 @@ public void Arrow_Keys_Should_Move_Selection_Horizontal() { Template = ListBoxTemplate(), ItemsSource = items, - ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel + ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel { - Orientation = Orientation.Horizontal + Orientation = Orientation.Horizontal }), ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), SelectedIndex = 0, @@ -1111,8 +1110,8 @@ public void Tab_Navigation_Should_Move_To_First_Item_When_No_Anchor_Element_Sele Items = { "Foo", "Bar", "Baz" }, }; - var button = new Button - { + var button = new Button + { Content = "Button", [DockPanel.DockProperty] = Dock.Top, }; @@ -1213,10 +1212,10 @@ public void Reads_Only_Realized_Items_From_ItemsSource() var panel = Assert.IsType(target.ItemsPanelRoot); Assert.Equal(0, panel.FirstRealizedIndex); - Assert.Equal(9, panel.LastRealizedIndex); + Assert.Equal(6, panel.LastRealizedIndex); Assert.Equal( - Enumerable.Range(0, 10).Select(x => $"Item{x}"), + Enumerable.Range(0, 7).Select(x => $"Item{x}"), data.GetRealizedItems()); } @@ -1347,9 +1346,9 @@ private class DataVirtualizingList : IList { private readonly List _inner = new(Enumerable.Repeat(null, 100)); - public object this[int index] - { - get => _inner[index] = $"Item{index}"; + public object this[int index] + { + get => _inner[index] = $"Item{index}"; set => throw new NotSupportedException(); } diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index ce85984f1c0..9d0cc9ad7ae 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Platform; @@ -136,7 +137,7 @@ public void Press_Ctrl_A_Select_All_Text() Template = CreateTemplate(), Text = "1234" }; - + target.ApplyTemplate(); RaiseKeyEvent(target, Key.A, KeyModifiers.Control); @@ -191,7 +192,7 @@ public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_I Text = "First Second Third Fourth", CaretIndex = 5 }; - + textBox.ApplyTemplate(); // (First| Second Third Fourth) @@ -233,7 +234,7 @@ public void Control_Delete_Should_Remove_The_Word_After_The_Caret_If_There_Is_No Text = "First Second Third Fourth", CaretIndex = 19 }; - + textBox.ApplyTemplate(); // (First Second Third |Fourth) @@ -336,7 +337,7 @@ public void Press_Enter_Add_Default_Newline() Template = CreateTemplate(), AcceptsReturn = true }; - + target.ApplyTemplate(); RaiseKeyEvent(target, Key.Enter, 0); @@ -453,7 +454,7 @@ public void Press_Enter_Add_Custom_Newline() AcceptsReturn = true, NewLine = "Test" }; - + target.ApplyTemplate(); RaiseKeyEvent(target, Key.Enter, 0); @@ -896,7 +897,8 @@ public void Invalid_Text_Is_Coerced_Without_Raising_Intermediate_Change() }; var impl = CreateMockTopLevelImpl(); - var topLevel = new TestTopLevel(impl.Object) { + var topLevel = new TestTopLevel(impl.Object) + { Template = CreateTopLevelTemplate(), Content = target }; @@ -926,13 +928,13 @@ public void Invalid_Text_Is_Coerced_Without_Raising_Intermediate_Change() inputManager: new InputManager(), renderInterface: new HeadlessPlatformRenderInterface(), fontManagerImpl: new HeadlessFontManagerStub(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), standardCursorFactory: Mock.Of()); private static TestServices Services => TestServices.MockThreadingInterface.With( renderInterface: new HeadlessPlatformRenderInterface(), - standardCursorFactory: Mock.Of(), - textShaperImpl: new HeadlessTextShaperStub(), + standardCursorFactory: Mock.Of(), + textShaperImpl: new HarfBuzzTextShaper(), fontManagerImpl: new HeadlessFontManagerStub()); private static IControlTemplate CreateTemplate() diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index c208c70b1d3..d5148e9b1f0 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Layout; @@ -1030,8 +1031,8 @@ public void Can_Change_Selection_For_Containers_Outside_Of_Viewport() { // Issue #11119 using var app = Start(); - var items = Enumerable.Range(0, 100).Select(x => new TestContainer - { + var items = Enumerable.Range(0, 100).Select(x => new TestContainer + { Content = $"Item {x}", Height = 100, }).ToList(); @@ -1092,7 +1093,7 @@ public void Selection_Is_Not_Cleared_On_Recycling_Containers() // Create a SelectingItemsControl that creates containers that raise IsSelectedChanged, // with a virtualizing stack panel. var target = CreateTarget( - itemsSource: items, + itemsSource: items, virtualizing: true); target.AutoScrollToSelectedItem = false; @@ -1186,7 +1187,7 @@ private static TestSelector CreateTarget( bool virtualizing = false) { return CreateTarget( - dataContext: dataContext, + dataContext: dataContext, items: items, itemsSource: itemsSource, itemContainerTheme: itemContainerTheme, @@ -1351,7 +1352,7 @@ public static IDisposable Start() keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub())); + textShaperImpl: new HarfBuzzTextShaper())); } private class TestSelector : SelectingItemsControl diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index 97e5d85d92b..52ca72f26a9 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -1,12 +1,10 @@ -using System; -using Avalonia.Controls.Documents; +using Avalonia.Controls.Documents; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; -using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Controls.UnitTests { @@ -171,7 +169,7 @@ public void Can_Call_Measure_Without_InvalidateTextLayout() { var target = new TextBlock(); - target.Inlines.Add(new TextBox { Text = "Hello"}); + target.Inlines.Add(new TextBox { Text = "Hello" }); target.Measure(Size.Infinity); @@ -287,7 +285,7 @@ public void Changing_InlineHost_Should_Propagate_To_Nested_Inlines() var span = new Span { Inlines = new InlineCollection { new Run { Text = "World" } } }; - var inlines = new InlineCollection{ new Run{Text = "Hello "}, span }; + var inlines = new InlineCollection { new Run { Text = "Hello " }, span }; target.Inlines = inlines; @@ -372,13 +370,13 @@ public void InlineUIContainer_Child_Should_Be_Arranged() target.Arrange(new Rect(target.DesiredSize)); Assert.True(button.IsMeasureValid); - Assert.Equal(80, button.DesiredSize.Width); + Assert.Equal(58, button.DesiredSize.Width); target.Arrange(new Rect(new Size(200, 50))); Assert.True(button.IsArrangeValid); - Assert.Equal(60, button.Bounds.Left); + Assert.Equal(43, button.Bounds.Left); } } @@ -427,7 +425,7 @@ public void Setting_Text_Should_Reset_Inlines() Assert.Equal(0, target.Inlines.Count); } } - + [Fact] public void Setting_TextDecorations_Should_Update_Inlines() { @@ -448,7 +446,7 @@ public void Setting_TextDecorations_Should_Update_Inlines() Assert.Equal(underline, target.Inlines[0].TextDecorations); } } - + [Fact] public void TextBlock_TextLines_Should_Be_Empty() { @@ -492,7 +490,7 @@ public void TextBlock_With_UseLayoutRounding_True_Should_Round_DesiredSize() target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - Assert.Equal(target.DesiredSize, new Size(40, 10)); + Assert.Equal(target.DesiredSize, new Size(28, 15)); } [Fact] @@ -504,7 +502,7 @@ public void TextBlock_With_UseLayoutRounding_True_Should_Round_Padding_And_Desir target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - Assert.Equal(target.DesiredSize, new Size(44, 14)); + Assert.Equal(target.DesiredSize, new Size(32, 19)); } [Fact] @@ -516,7 +514,7 @@ public void TextBlock_With_UseLayoutRounding_False_Should_Not_Round_DesiredSize( target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - Assert.Equal(target.DesiredSize, new Size(40, 9.6)); + Assert.Equal(target.DesiredSize, new Size(27.954545454545453, 14.522727272727273)); } [Fact] @@ -529,7 +527,7 @@ public void TextBlock_With_UseLayoutRounding_False_Should_Not_Round_Bounds() target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); target.Arrange(new Rect(default, target.DesiredSize)); - Assert.Equal(target.Bounds, new Rect(0, 0, 40, 9.6)); + Assert.Equal(target.Bounds, new Rect(0, 0, 27.954545454545453, 14.522727272727273)); } [Fact] @@ -541,7 +539,7 @@ public void TextBlock_With_UseLayoutRounding_False_Should_Not_Round_Padding_In_M target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - Assert.Equal(target.DesiredSize, new Size(44.5, 14.1)); + Assert.Equal(target.DesiredSize, new Size(32.45454545454545, 19.022727272727273)); } [Fact] @@ -554,7 +552,7 @@ public void TextBlock_With_UseLayoutRounding_False_Should_Not_Round_Padding_In_A target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); target.Arrange(new Rect(default, target.DesiredSize)); - Assert.Equal(target.Bounds, new Rect(0, 0, 44.5, 14.1)); + Assert.Equal(target.Bounds, new Rect(0, 0, 32.45454545454545, 19.022727272727273)); } private class TestTextBlock : TextBlock diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 2e94b1608fb..ab9b668e544 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Platform; @@ -2134,13 +2135,13 @@ public void Losing_Focus_Should_Not_Reset_Selection() keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), standardCursorFactory: Mock.Of(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), fontManagerImpl: new HeadlessFontManagerStub()); private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), fontManagerImpl: new HeadlessFontManagerStub()); internal static IControlTemplate CreateTemplate() diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index 431426efa8c..f5ce35f18cb 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.MarkupExtensions; @@ -149,7 +150,7 @@ public void CompiledBindings_TypeConverter_Exceptions_Should_Set_DataValidationE private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), fontManagerImpl: new HeadlessFontManagerStub()); private static IControlTemplate CreateTemplate() diff --git a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs index 3a7c07bb8d5..ab6d31c6806 100644 --- a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Platform; using Avalonia.Threading; @@ -312,7 +313,7 @@ private static void TestSelectorScrolling(string selectorName, Action TestServices.MockThreadingInterface.With( fontManagerImpl: new HeadlessFontManagerStub(), standardCursorFactory: Mock.Of(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), renderInterface: new HeadlessPlatformRenderInterface()); private static IControlTemplate CreateTemplate(bool includePopup = false) diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs index 62430deb095..3800a67aff8 100644 --- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs @@ -6,6 +6,7 @@ using Avalonia.Animation; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Layout; using Avalonia.UnitTests; @@ -325,7 +326,7 @@ private static IDisposable Start() TestServices.MockThreadingInterface.With( fontManagerImpl: new HeadlessFontManagerStub(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub())); + textShaperImpl: new HarfBuzzTextShaper())); } private static (TransitioningContentControl, TestTransition) CreateTarget(object content) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 0033f836f19..32348356152 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Platform; @@ -1464,7 +1465,7 @@ public void Selection_State_Is_Updated_Via_IsSelected_Binding_On_Expand() var target = CreateTarget( data: data, expandAll: false, - itemContainerTheme: itemTheme, + itemContainerTheme: itemTheme, multiSelect: true); var rootContainer = Assert.IsType(target.ContainerFromIndex(0)); @@ -1841,7 +1842,7 @@ private IDisposable Start() keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub())); + textShaperImpl: new HarfBuzzTextShaper())); } private class Node : NotifyingBase diff --git a/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj b/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj index 301b96e0e85..3a1dcb68e39 100644 --- a/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj +++ b/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj @@ -25,6 +25,7 @@ + diff --git a/tests/Avalonia.Headless.UnitTests/TestApplication.cs b/tests/Avalonia.Headless.UnitTests/TestApplication.cs index ae923bf52e4..db23c521d40 100644 --- a/tests/Avalonia.Headless.UnitTests/TestApplication.cs +++ b/tests/Avalonia.Headless.UnitTests/TestApplication.cs @@ -1,5 +1,4 @@ -using Avalonia.Headless.UnitTests; -using Avalonia.Themes.Simple; +using Avalonia.Themes.Simple; namespace Avalonia.Headless.UnitTests; @@ -11,6 +10,7 @@ public TestApplication() } public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseHarfBuzz() .UseSkia() .UseHeadless(new AvaloniaHeadlessPlatformOptions { diff --git a/tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj b/tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj index 322ce6bb3aa..a13220d7582 100644 --- a/tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj +++ b/tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj @@ -18,6 +18,7 @@ + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index 50e30546c77..b9110288f9c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -12,6 +12,7 @@ + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs index 27b4f151c47..127acda4042 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -134,10 +134,9 @@ private class FooBar public object Foo { get; } = null; } - private static IDisposable StyledWindow(params (string, string)[] assets) + private static IDisposable StyledWindow() { var services = TestServices.StyledWindow.With( - assetLoader: new MockAssetLoader(assets), theme: () => new Styles { WindowStyle(), diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index 1106e7bb990..120ace94cc7 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -960,10 +960,9 @@ public void Handles_Clearing_Resources_With_Dynamic_Theme_In_Dynamic_Template() window.Resources.Clear(); } - private IDisposable StyledWindow(params (string, string)[] assets) + private IDisposable StyledWindow() { var services = TestServices.StyledWindow.With( - assetLoader: new MockAssetLoader(assets), theme: () => new Styles { WindowStyle(), diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs index c48978a516b..db9b53eeeb3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs @@ -583,7 +583,6 @@ public void Automatically_Converts_Color_To_SolidColorBrush_From_Setter() private static IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( - assetLoader: new MockAssetLoader(assets), theme: () => new Styles { WindowStyle(), diff --git a/tests/Avalonia.RenderTests/Assets/NotoSansTamil-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoSansTamil-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1776798018398bf8b339a1c55f81cfb604d36315 GIT binary patch literal 78988 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 Mv822jKgIh00H_hAWdHyG literal 0 HcmV?d00001 diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs index 1b5083924a5..1ef7f897b5c 100644 --- a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -140,7 +140,7 @@ public GlyphRunGeometryControl() { var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface; - var glyphIndices = new[] { glyphTypeface.GetGlyph('A'), glyphTypeface.GetGlyph('B'), glyphTypeface.GetGlyph('C') }; + var glyphIndices = new[] { glyphTypeface.CharacterToGlyphMap['A'], glyphTypeface.CharacterToGlyphMap['B'], glyphTypeface.CharacterToGlyphMap['C'] }; var characters = new[] { 'A', 'B', 'C' }; @@ -165,7 +165,7 @@ public UnPositionedGlyphRunControl() { var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface; - var glyphIndices = new[] { glyphTypeface.GetGlyph('A'), glyphTypeface.GetGlyph('B'), glyphTypeface.GetGlyph('C') }; + var glyphIndices = new[] { glyphTypeface.CharacterToGlyphMap['A'], glyphTypeface.CharacterToGlyphMap['B'], glyphTypeface.CharacterToGlyphMap['C'] }; var characters = new[] { 'A', 'B', 'C' }; @@ -188,7 +188,7 @@ public PositionedGlyphRunControl() { var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface; - var glyphIndices = new[] { glyphTypeface.GetGlyph('A'), glyphTypeface.GetGlyph('B'), glyphTypeface.GetGlyph('C') }; + var glyphIndices = new[] { glyphTypeface.CharacterToGlyphMap['A'], glyphTypeface.CharacterToGlyphMap['B'], glyphTypeface.CharacterToGlyphMap['C'] }; var scale = 100.0 / glyphTypeface.Metrics.DesignEmHeight; diff --git a/tests/Avalonia.RenderTests/TestRenderHelper.cs b/tests/Avalonia.RenderTests/TestRenderHelper.cs index 53ef16a6e71..b73b3506aad 100644 --- a/tests/Avalonia.RenderTests/TestRenderHelper.cs +++ b/tests/Avalonia.RenderTests/TestRenderHelper.cs @@ -21,6 +21,8 @@ using Avalonia.Utilities; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; +using Avalonia.Harfbuzz; + #if AVALONIA_SKIA using Avalonia.Skia; #else @@ -50,6 +52,7 @@ static TestRenderHelper() .ToConstant(s_dispatcherImpl); AvaloniaLocator.CurrentMutable.Bind().ToConstant(new StandardAssetLoader()); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new HarfBuzzTextShaper()); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 34dc32ac6b5..5361684aa0a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -1,12 +1,13 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using System.Linq; using Avalonia.Media; using Avalonia.Media.Fonts; using Avalonia.Platform; using SkiaSharp; -using System.Diagnostics.CodeAnalysis; -using System.IO; namespace Avalonia.Skia.UnitTests.Media { @@ -39,28 +40,37 @@ public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) _isInitialized = true; } - return _customFonts.Select(x=> x.Name).ToArray(); + return _customFonts.Select(x => x.Name).ToArray(); } private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - CultureInfo culture, out Typeface typeface) + CultureInfo culture, out IPlatformTypeface typeface) { if (!_isInitialized) { _customFonts.Initialize(this); } - if(_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out typeface)) + if (_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out var match)) { + typeface = match.GlyphTypeface.PlatformTypeface; + return true; } var fallback = SKFontManager.Default.MatchCharacter(null, (SKFontStyleWeight)fontWeight, (SKFontStyleWidth)fontStretch, (SKFontStyleSlant)fontStyle, _bcp47, codepoint); - typeface = new Typeface(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); + if (fallback == null) + { + typeface = null; + + return false; + } + + typeface = new SkiaTypeface(fallback, FontSimulations.None); return true; } @@ -81,7 +91,7 @@ public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeigh var skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)style); - glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + glyphTypeface = new GlyphTypeface(new SkiaTypeface(skTypeface, FontSimulations.None), FontSimulations.None); return true; } @@ -90,7 +100,7 @@ public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulation { var skTypeface = SKTypeface.FromStream(stream); - glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations); + glyphTypeface = new GlyphTypeface(new SkiaTypeface(skTypeface, FontSimulations.None), fontSimulations); return true; } @@ -99,5 +109,10 @@ public void Dispose() { _customFonts.Dispose(); } + + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList familyTypefaces) + { + throw new NotImplementedException(); + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs index a1ba9d92f8e..555be9f292f 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs @@ -89,11 +89,7 @@ public void Should_Get_Typeface_For_TypographicFamilyName() Assert.Equal("Manrope Light", glyphTypeface.FamilyName); - Assert.True(glyphTypeface is IGlyphTypeface2); - - var glyphTypeface2 = (IGlyphTypeface2)glyphTypeface; - - Assert.Equal("Manrope", glyphTypeface2.TypographicFamilyName); + Assert.Equal("Manrope", glyphTypeface.TypographicFamilyName); } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs index 5c830df4fa8..9da5178dab0 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs @@ -164,7 +164,7 @@ public override bool TryCreateSyntheticGlyphTypeface( { foreach (var ignorable in _ignorables) { - if (glyphTypeface.FamilyName == ignorable.Name || glyphTypeface is IGlyphTypeface2 glyphTypeface2 && glyphTypeface2.TypographicFamilyName == ignorable.Name) + if (glyphTypeface.FamilyName == ignorable.Name || glyphTypeface.TypographicFamilyName == ignorable.Name) { return false; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index 2713e7133be..ffe6a33f84d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -101,12 +101,14 @@ public void Should_Only_Try_To_Create_GlyphTypeface_Once() { Assert.True(FontManager.Current.TryGetGlyphTypeface(Typeface.Default, out _)); + var countBefore = fontManagerImpl.TryCreateGlyphTypefaceCount; + for (int i = 0; i < 10; i++) { FontManager.Current.TryGetGlyphTypeface(new Typeface("Unknown"), out _); } - Assert.Equal(fontManagerImpl.TryCreateGlyphTypefaceCount, 2); + Assert.Equal(countBefore + 1, fontManagerImpl.TryCreateGlyphTypefaceCount); } } @@ -330,7 +332,7 @@ public void Should_Get_FontFeatures() Assert.Equal("Inter", glyphTypeface.FamilyName); - var features = ((IGlyphTypeface2)glyphTypeface).SupportedFeatures; + var features = glyphTypeface.SupportedFeatures; Assert.NotEmpty(features); } @@ -448,7 +450,7 @@ public void Should_Get_Regular_Font_After_Matching_Italic_Font() Assert.Equal(FontStyle.Normal, regularTypeface.Style); - Assert.NotEqual(((GlyphTypefaceImpl)italicTypeface.GlyphTypeface).SKTypeface, ((GlyphTypefaceImpl)regularTypeface.GlyphTypeface).SKTypeface); + Assert.NotEqual(((SkiaTypeface)italicTypeface.GlyphTypeface.PlatformTypeface).SKTypeface, ((SkiaTypeface)regularTypeface.GlyphTypeface.PlatformTypeface).SKTypeface); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index ab469d7d1b5..1882ef9ccbc 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -474,7 +474,6 @@ private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(renderInterface: new PlatformRenderInterface(), - textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); return disposable; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/Tables/CmapTableTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/Tables/CmapTableTests.cs new file mode 100644 index 00000000000..524267ed6cb --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/Tables/CmapTableTests.cs @@ -0,0 +1,113 @@ +using System; +using System.Buffers.Binary; +using Avalonia.Media.Fonts.Tables.Cmap; +using Xunit; + +namespace Avalonia.Skia.UnitTests.Media.TextFormatting.Tables +{ + public class CmapTableTests + { + [Fact] + public void BuildFormat4Subtable_Should_Map_Range() + { + // Build a subtable mapping U+0030–U+0039 (digits 0–9) to glyphs 1–10 + byte[] subtable = CmapTestHelper.BuildFormat4Subtable(0x0030, 0x0039, 1); + + var cmap = new CmapFormat4Table(subtable); + + for (int i = 0; i < 10; i++) + { + int cp = 0x30 + i; + ushort glyph = cmap[cp]; + var expectedGlyph = (ushort)(i + 1); + Assert.Equal(expectedGlyph, glyph); + } + + // Outside range should map to 0 + Assert.Equal((ushort)0, cmap[0x0041]); // 'A' + } + } + + public static class CmapTestHelper + { + /// + /// Builds a Format 4 subtable for a TrueType font's 'cmap' table, which maps a range of character codes to + /// glyph indices. + /// + /// The Format 4 subtable is used in TrueType fonts to define mappings from character + /// codes to glyph indices for a contiguous range of character codes. This method generates a minimal Format 4 + /// subtable with one segment for the specified range and a sentinel segment, as required by the TrueType + /// specification. The generated subtable includes the necessary header fields, segment arrays, and delta + /// values to ensure that the specified range of character codes maps correctly to the corresponding glyph + /// indices. Thrown if is less than + /// . + /// The starting character code of the range to map. + /// The ending character code of the range to map. + /// The glyph index corresponding to the . Subsequent character codes in the range + /// will map to consecutive glyph indices. + /// A byte array representing the Format 4 subtable, which can be embedded in a TrueType font's 'cmap' table. + public static byte[] BuildFormat4Subtable(ushort startCode, ushort endCode, ushort firstGlyphId = 1) + { + if (endCode < startCode) + throw new ArgumentException("endCode must be >= startCode"); + + // We will build exactly one real segment + sentinel + ushort segCount = 2; // one real + one sentinel + ushort segCountX2 = (ushort)(segCount * 2); + + // Correct search parameters (searchRange = 2 * (2^floor(log2(segCount)))) + int highestPowerOfTwo = 1; + while (highestPowerOfTwo * 2 <= segCount) + highestPowerOfTwo *= 2; + ushort searchRange = (ushort)(2 * highestPowerOfTwo); + ushort entrySelector = (ushort)(Math.Log(highestPowerOfTwo, 2)); + ushort rangeShift = (ushort)(segCountX2 - searchRange); + + // idDelta so that startCode maps to firstGlyphId + short idDelta = (short)(firstGlyphId - startCode); + + // Calculate length: header (14) + endCode(segCount*2) + reservedPad(2) + startCode(segCount*2) + // + idDelta(segCount*2) + idRangeOffset(segCount*2) + (no glyphIdArray) + int headerSize = 14; + int segArraysSize = segCount * 2 /*endCode*/ + 2 /*reservedPad*/ + segCount * 2 /*startCode*/ + segCount * 2 /*idDelta*/ + segCount * 2 /*idRangeOffset*/; + int length = headerSize + segArraysSize; + + var buffer = new byte[length]; + int pos = 0; + + void WriteUInt16(ushort v) + { BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(pos, 2), v); pos += 2; } + void WriteInt16(short v) + { BinaryPrimitives.WriteInt16BigEndian(buffer.AsSpan(pos, 2), v); pos += 2; } + + // Header + WriteUInt16(4); // format + WriteUInt16((ushort)length); // length + WriteUInt16(0); // language + WriteUInt16(segCountX2); + WriteUInt16(searchRange); + WriteUInt16(entrySelector); + WriteUInt16(rangeShift); + + // endCode[] (one real segment then sentinel) + WriteUInt16(endCode); + WriteUInt16(0xFFFF); + + WriteUInt16(0); // reservedPad + + // startCode[] + WriteUInt16(startCode); + WriteUInt16(0xFFFF); + + // idDelta[] + WriteInt16(idDelta); + WriteInt16(1); // sentinel delta (commonly 1) + + // idRangeOffset[] + WriteUInt16(0); + WriteUInt16(0); + + return buffer; + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index b75cdf96cd4..425af0e535e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -475,7 +475,7 @@ public void Should_Wrap(string text, string familyName, int numberOfCharactersPe var formatter = new TextFormatterImpl(); - var glyph = typeface.GlyphTypeface.GetGlyph('a'); + var glyph = typeface.GlyphTypeface.CharacterToGlyphMap['a']; var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) * (12.0 / typeface.GlyphTypeface.Metrics.DesignEmHeight); @@ -1253,8 +1253,7 @@ public InvisibleRun(int length) public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(), - textShaperImpl: new TextShaperImpl())); + .With(renderInterface: new PlatformRenderInterface())); AvaloniaLocator.CurrentMutable .Bind().ToConstant(new FontManager(new CustomFontManagerImpl())); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 5e1f6ea017a..b330ec99d5f 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -566,7 +566,7 @@ public void Should_Layout_Corrupted_Text() Assert.Equal(7, textRun.Length); - var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint); + var replacementGlyph = Typeface.Default.GlyphTypeface.CharacterToGlyphMap[Codepoint.ReplacementCodepoint]; foreach (var glyphInfo in textRun.GlyphRun.GlyphInfos) { @@ -1189,7 +1189,12 @@ public void Should_Measure_TextLayoutSymbolWithAndWidthIncludingTrailingWhitespa { using (Start()) { - var typeFace = new Typeface("Courier New"); + const string monospaceFont = "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"; + + var typeFace = new Typeface(monospaceFont); + + var glyphTypeface = typeFace.GlyphTypeface; + var textLayout0 = new TextLayout("aaaa", typeFace, 12.0, Brushes.White); Assert.Equal(textLayout0.WidthIncludingTrailingWhitespace, textLayout0.Width); @@ -1217,7 +1222,6 @@ private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(renderInterface: new PlatformRenderInterface(null), - textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); return disposable; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index fc298c1201f..0ead76fbf5a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -2316,7 +2316,6 @@ private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(renderInterface: new PlatformRenderInterface(null), - textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); return disposable; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index a2c477e40a1..295c0fef279 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -113,7 +113,6 @@ private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(renderInterface: new PlatformRenderInterface(null), - textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); return disposable; diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 0d49f78dd9e..1b068b25bde 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -10,6 +10,7 @@ + diff --git a/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs deleted file mode 100644 index db517ba1761..00000000000 --- a/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.IO; -using Avalonia.Media; -using HarfBuzzSharp; - -namespace Avalonia.UnitTests -{ - public class HarfBuzzGlyphTypefaceImpl : IGlyphTypeface - { - private bool _isDisposed; - private Blob _blob; - - public HarfBuzzGlyphTypefaceImpl(Stream data) - { - _blob = Blob.FromStream(data); - - Face = new Face(_blob, 0); - - Font = new Font(Face); - - Font.SetFunctionsOpenType(); - - Font.GetScale(out var scale, out _); - - const double defaultFontRenderingEmSize = 12.0; - - var metrics = Font.OpenTypeMetrics; - - Metrics = new FontMetrics - { - DesignEmHeight = (short)scale, - Ascent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalAscender) / defaultFontRenderingEmSize * scale), - Descent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalDescender) / defaultFontRenderingEmSize * scale), - LineGap = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalLineGap) / defaultFontRenderingEmSize * scale), - - UnderlinePosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineOffset) / defaultFontRenderingEmSize * scale), - - UnderlineThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineSize) / defaultFontRenderingEmSize * scale), - - StrikethroughPosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutOffset) / defaultFontRenderingEmSize * scale), - - StrikethroughThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutSize) / defaultFontRenderingEmSize * scale), - - IsFixedPitch = GetGlyphAdvance(GetGlyph('a')) == GetGlyphAdvance(GetGlyph('b')) - }; - - GlyphCount = Face.GlyphCount; - } - - public FontMetrics Metrics { get; } - - public Face Face { get; } - - public Font Font { get; } - - public int GlyphCount { get; set; } - - public FontSimulations FontSimulations { get; } - - public string FamilyName => "$Default"; - - public FontWeight Weight { get; } - - public FontStyle Style { get; } - - public FontStretch Stretch { get; } - - - /// - 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 = 0; - - if (Font.TryGetGlyph(codepoint, out var glyphId)) - { - glyph = (ushort)glyphId; - - return true; - } - - return false; - } - - /// - public ushort[] GetGlyphs(ReadOnlySpan 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; - } - - /// - public int GetGlyphAdvance(ushort glyph) - { - return Font.GetHorizontalGlyphAdvance(glyph); - } - - /// - public int[] GetGlyphAdvances(ReadOnlySpan glyphs) - { - var glyphIndices = new uint[glyphs.Length]; - - for (var i = 0; i < glyphs.Length; i++) - { - glyphIndices[i] = glyphs[i]; - } - - return Font.GetHorizontalGlyphAdvances(glyphIndices); - } - - public bool TryGetTable(uint tag, out byte[] table) - { - table = null; - var blob = Face.ReferenceTable(tag); - - if (blob.Length > 0) - { - table = blob.AsSpan().ToArray(); - - return true; - } - - return false; - } - - private void Dispose(bool disposing) - { - if (_isDisposed) - { - return; - } - - _isDisposed = true; - - if (!disposing) - { - return; - } - - Font?.Dispose(); - Face?.Dispose(); - _blob?.Dispose(); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - 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; - } - } -} diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs deleted file mode 100644 index 98f188f15a1..00000000000 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Concurrent; -using System.Globalization; -using System.Runtime.InteropServices; -using Avalonia.Media.TextFormatting; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Platform; -using HarfBuzzSharp; -using Buffer = HarfBuzzSharp.Buffer; -using GlyphInfo = HarfBuzzSharp.GlyphInfo; - -namespace Avalonia.UnitTests -{ - internal class HarfBuzzTextShaperImpl : ITextShaperImpl - { - private static readonly ConcurrentDictionary s_cachedLanguage = new(); - public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) - { - var textSpan = text.Span; - var typeface = options.Typeface; - var fontRenderingEmSize = options.FontRenderingEmSize; - var bidiLevel = options.BidiLevel; - var culture = options.Culture; - - using (var buffer = new Buffer()) - { - // HarfBuzz needs the surrounding characters to correctly shape the text - var containingText = GetContainingMemory(text, out var start, out var length).Span; - buffer.AddUtf16(containingText, start, length); - - MergeBreakPair(buffer); - - buffer.GuessSegmentProperties(); - - buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - - var usedCulture = culture ?? CultureInfo.CurrentCulture; - - buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); - - var font = ((HarfBuzzGlyphTypefaceImpl)typeface).Font; - - font.Shape(buffer); - - if (buffer.Direction == Direction.RightToLeft) - { - buffer.Reverse(); - } - - font.GetScale(out var scaleX, out _); - - var textScale = fontRenderingEmSize / scaleX; - - var bufferLength = buffer.Length; - - var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); - - var glyphInfos = buffer.GetGlyphInfoSpan(); - - var glyphPositions = buffer.GetGlyphPositionSpan(); - - for (var i = 0; i < bufferLength; i++) - { - var sourceInfo = glyphInfos[i]; - - var glyphIndex = (ushort)sourceInfo.Codepoint; - - var glyphCluster = (int)(sourceInfo.Cluster); - - var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; - - var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - - if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') - { - glyphIndex = typeface.GetGlyph(' '); - - glyphAdvance = options.IncrementalTabWidth > 0 ? - options.IncrementalTabWidth : - 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; - } - - shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); - } - - return shapedBuffer; - } - } - - private static void MergeBreakPair(Buffer buffer) - { - var length = buffer.Length; - - var glyphInfos = buffer.GetGlyphInfoSpan(); - - var second = glyphInfos[length - 1]; - - if (!new Codepoint(second.Codepoint).IsBreakChar) - { - return; - } - - if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') - { - var first = glyphInfos[length - 2]; - - first.Codepoint = '\u200C'; - second.Codepoint = '\u200C'; - second.Cluster = first.Cluster; - - unsafe - { - fixed (GlyphInfo* p = &glyphInfos[length - 2]) - { - *p = first; - } - - fixed (GlyphInfo* p = &glyphInfos[length - 1]) - { - *p = second; - } - } - } - else - { - second.Codepoint = '\u200C'; - - unsafe - { - fixed (GlyphInfo* p = &glyphInfos[length - 1]) - { - *p = second; - } - } - } - } - - private static Vector GetGlyphOffset(ReadOnlySpan glyphPositions, int index, double textScale) - { - var position = glyphPositions[index]; - - var offsetX = position.XOffset * textScale; - - var offsetY = position.YOffset * textScale; - - return new Vector(offsetX, offsetY); - } - - private static double GetGlyphAdvance(ReadOnlySpan glyphPositions, int index, double textScale) - { - // Depends on direction of layout - // glyphPositions[index].YAdvance * textScale; - return glyphPositions[index].XAdvance * textScale; - } - - private static ReadOnlyMemory GetContainingMemory(ReadOnlyMemory memory, out int start, out int length) - { - if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length)) - { - return containingString.AsMemory(); - } - - if (MemoryMarshal.TryGetArray(memory, out var segment)) - { - start = segment.Offset; - length = segment.Count; - return segment.Array.AsMemory(); - } - - if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager memoryManager, out start, out length)) - { - return memoryManager.Memory; - } - - // should never happen - throw new InvalidOperationException("Memory not backed by string, array or manager"); - } - } -} diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HeadlessFontManagerImpl.cs similarity index 67% rename from tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs rename to tests/Avalonia.UnitTests/HeadlessFontManagerImpl.cs index 24599c5a9b0..4e5f33e7e5d 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/HeadlessFontManagerImpl.cs @@ -1,26 +1,28 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; +using Avalonia.Headless; using Avalonia.Media; using Avalonia.Platform; namespace Avalonia.UnitTests { - public class HarfBuzzFontManagerImpl : IFontManagerImpl + public class HeadlessFontManagerImpl : IFontManagerImpl { private readonly Typeface[] _customTypefaces; private readonly string _defaultFamilyName; private static readonly Typeface _defaultTypeface = new Typeface(new FontFamily("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono")); - private static readonly Typeface _italicTypeface = + private static readonly Typeface _italicTypeface = new Typeface(new FontFamily("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Sans")); - private static readonly Typeface _emojiTypeface = + private static readonly Typeface _emojiTypeface = new Typeface(new FontFamily("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Twitter Color Emoji")); - public HarfBuzzFontManagerImpl(string defaultFamilyName = "Noto Mono") + public HeadlessFontManagerImpl(string defaultFamilyName = "Noto Mono") { _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; _defaultFamilyName = defaultFamilyName; @@ -37,40 +39,47 @@ string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, CultureInfo culture, out Typeface fontKey) + FontStretch fontStretch, CultureInfo culture, out IPlatformTypeface? platformTypeface) { foreach (var customTypeface in _customTypefaces) { var glyphTypeface = customTypeface.GlyphTypeface; - if (!glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + if (!glyphTypeface.CharacterToGlyphMap.TryGetValue(codepoint, out _)) { continue; } - - fontKey = customTypeface; - + + platformTypeface = glyphTypeface.PlatformTypeface; + return true; } - fontKey = default; + platformTypeface = null; return false; } public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) { - glyphTypeface = new HarfBuzzGlyphTypefaceImpl(stream); + var platformTypeface = new HeadlessPlatformTypeface(stream); + + glyphTypeface = new GlyphTypeface(platformTypeface, FontSimulations.None); return true; } - 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) { glyphTypeface = null; return false; } + + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList familyTypefaces) + { + throw new NotImplementedException(); + } } } diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index fc10bdaade3..cd035e833ef 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -1,14 +1,15 @@ using System; -using Moq; +using System.Reactive.Concurrency; +using Avalonia.Animation; +using Avalonia.Harfbuzz; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Platform; +using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Themes.Simple; -using Avalonia.Rendering; -using System.Reactive.Concurrency; -using Avalonia.Animation; -using Avalonia.Headless; using Avalonia.Threading; +using Moq; namespace Avalonia.UnitTests { @@ -22,20 +23,21 @@ public class TestServices theme: () => CreateSimpleTheme(), dispatcherImpl: new NullDispatcherImpl(), fontManagerImpl: new HeadlessFontManagerStub(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), windowingPlatform: new MockWindowingPlatform()); public static readonly TestServices MockPlatformRenderInterface = new TestServices( assetLoader: new StandardAssetLoader(), renderInterface: new HeadlessPlatformRenderInterface(), fontManagerImpl: new HeadlessFontManagerStub(), - textShaperImpl: new HeadlessTextShaperStub()); + textShaperImpl: new HarfBuzzTextShaper()); public static readonly TestServices MockPlatformWrapper = new TestServices( platform: Mock.Of()); public static readonly TestServices MockThreadingInterface = new TestServices( - dispatcherImpl: new NullDispatcherImpl()); + dispatcherImpl: new NullDispatcherImpl(), + assetLoader: new StandardAssetLoader()); public static readonly TestServices MockWindowingPlatform = new TestServices( windowingPlatform: new MockWindowingPlatform()); @@ -47,7 +49,7 @@ public class TestServices assetLoader: new StandardAssetLoader(), renderInterface: new HeadlessPlatformRenderInterface(), fontManagerImpl: new HeadlessFontManagerStub(), - textShaperImpl: new HeadlessTextShaperStub()); + textShaperImpl: new HarfBuzzTextShaper()); public static readonly TestServices FocusableWindow = new TestServices( keyboardDevice: () => new KeyboardDevice(), @@ -60,15 +62,15 @@ public class TestServices theme: () => CreateSimpleTheme(), dispatcherImpl: new NullDispatcherImpl(), fontManagerImpl: new HeadlessFontManagerStub(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), windowingPlatform: new MockWindowingPlatform()); - + public static readonly TestServices TextServices = new TestServices( assetLoader: new StandardAssetLoader(), renderInterface: new HeadlessPlatformRenderInterface(), - fontManagerImpl: new HarfBuzzFontManagerImpl(), - textShaperImpl: new HarfBuzzTextShaperImpl()); - + fontManagerImpl: new HeadlessFontManagerImpl(), + textShaperImpl: new HarfBuzzTextShaper()); + public TestServices( IAssetLoader assetLoader = null, IInputManager inputManager = null, diff --git a/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png b/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png index 5c62bfd1797ac177c6b04924608c99172dd2562f..033559277873609dbd7b60f993b46a5e948a8618 100644 GIT binary patch delta 1409 zcmV-{1%CSA3b6~2L4R9GL_t(|UhSGOXd6ishW|ZM7!nAZKTp3#~LXssU#-$6#Xk}sS!i5W?6fO*Njb?6k zB<-$rx|}(`4-DS-c4pqZng8vqB#kk~KoCS2qX}K2!RAb z5FwC22qFX$2tkCPMSc(R{JCl&i2n~CKdP1h(E=feTTJ8;%kw;Fng-AFgsoUC;@!J< zf%}O626b(S@e(NzPPJN9O~eg=F@|h5tD0{OmSv&UYN_V25lbKo3kv}7uLeR8qtIwH zRMS|AB@h58mw(GxUtd?vK=;U{X&N+5L$O#4_zc4kKF4ujSr!1mah$-tp69_ZjKDQ- zH_S1@Fbp`3BiixtI8m|6T z>TLsQx7(s^qtOr>*0L-V3WdJ0a^!Khx3>enTrMYk&1O^BQ>heu-xt18sT6B-jG7++ zSY2HO8-MWm%KG{`-oAa?H`ZdY2-~)WZ*6TY&^8R-hA{@)wgUyTP$IHwA*cc zV}D^`A?rfFhpYb$UcN7r>xy#WNObs~`nJo@E6o;-Piot>SLlcA2utF&pF`1AJqp=u7QXKXVy)F`fhs+SQGfFj7hTuE7z?bIVHklN$j4?~*G2V)5|2ul zZy3V%kIeS=HY$}0_V)Hv*HG{x8~2G6`9n0JyG;PNxF^NT<^Pfcg3P9$zMt!H*w5ddA_*%*^1$i+>m5 zdJwMbVrFIrhlhticT7GWlvve1UDw6q$B)IlWOjFV@$lh8%+1ZIE_6B_xUMU%<=jLu zYJTeVy6A($^RBLk8jsvIolfK4y?dCOo9mfJCX>O>pFc%Ba=Uy!kMr|$%+Jr`+qZ9+ zo}NZBnMA!_N4Z?a!NCCl^v#!w8h}z`?-*_V@RJK5IP)d2g{l zL$`mlK$3sNTj)4kUS8t*`Wlmyld5ZMoSvTI!Gj05d-tyDy7Byq{HR=9T;R@~JGg)U zzUsOa9RGf-ngT=$WSlsTgLb<;mUomv7>0prwTh*sCDnD~`4#z5u`COZ9(p~(>guZM zBH{>SoWE5(IXS`A)fHxEXI0nO`2PJnlF1~drlwTajptY7N9E|~2on<%NF)-f>sAo) zTg7OB5X5a}s86v7;2qX}K2!RAb5Fz{p-G%Fukdm|z P00000NkvXXu0mjf#LcYP delta 1328 zcmV-01<(4i3*ZWnL4OKKL_t(|UhSE`PwPk!$G@gP5u&*YQel~-M$k}1F~KzOYAOg4 zwnTk@01A;UL4WNPgaW7{u7o0l9~K1_qIQIm3aCy(2vM=uWcSva#CCGIckf;1BUy@P zJl@&a{qAlOjWNbR5JXqTR0BZ}e?tNxhzLj^1Q7uVgdidyfqxK01SAlGh=2q_5D{px z|AQvh#MR1ygU0CskEaBy&- znt}F_OVc!Hntz5up%C)vx-NX4=fQCt0D$Ltp=W*Hhpy|Pdmh)%F+$gMc%CQXczc|< zT;+CUf1ywa`Q%(1jYcTfevY4m)<^E6S{pSVIS%b(ncSx5c|g05{4?5Z$n}%sG))r= zrsFuGuAb+KJ|$gHqsBz`X;NPu$APA4Xti3%X0y<`jDK}6jMCLdwXC#eqT8_IKc7o zao1c6g?|EE*A>2Uxg3g(g2yn%;JR*T!L%$3K@gx^E~8ef!F64no}Px{yAd^ixESPi z`El~_@KE%h*FI_eOw+{e?QPHc$s`g8$93I~)v;c$qgX5oMOG>mF(6M*Pgq%55zi)7 z`2GDoTCJ9NXEvJ!`Z(gLZC+kp0_|LvrJt>B+kfsT>i+(I=w8NkUDX8N_c1dwgG?qP zY_@GDnjC}HCl1@TQ7jfab`G=`548qK;j6Z$X&u6vot+gmkZTb~)cnck^Jq4kp`GKO z!sk%Z?`?evWMg9kySuxpS*Vd;>ZBWlrKP3NI~ikr6@HY-=ks8Up;oIw>vC)v1iq^C zv47~cSPV+k{Nbm8QmF*3k7L`g=u04(Oa_)^;pXN>*euKHdh`MS!!X3^jrO+4nVp>t zZT<2)78e(Bb8|CrG1NBsDs5R7uB1PPDwPT>%M$m4kk9AQYPCYSRw|XyD&31g>ywmB zCIiM;XuNb?@2COCFpN-}Q43v^uOFi3kAKYd^)+nU#{K=h>go%=$R^#9V*e=o_WL*Z zzK`|wbpXKY>nqOB&taM-48s5bTwGk><>f`htgWqKYHA7qP_0(+{{9ZM&*}h>%jG(J z^Yilvf}o=v&h+#&R##WWeJ`rjD$?mRf*=6eZSwY@3?(@ zb8~a53vX|4I6FHN_i}CG7_>e|M@Qlv9A0;IJk)&T*lae7$;nB~&CPZ6V}5=f@9*y- zA34r6O*}t8BbUqJ?(PohbQ*?X;OOWG+uPfCe0&6euJuw;^X2_`dV0d`?X9q@;{^bZ zkB@kGcmTSLbuZ+(#r}w#{_z6&Wq<8A)if}lpP%^p`ohG-gz6d^Z*Ol%rBWClA6H#J zJl~@}Djy#o7#kZyDwR@QBf+2NzpADXu>wi(OBg}?v_#MT1m8gz8{Xx*9&Fp`un+T^ znbC24e9~e68Lsb9AN8_r2d?XN*hl4xBX$QNh*2hX2O)@2h6F+o5s*L#A}InA2th Date: Fri, 24 Oct 2025 13:16:14 +0200 Subject: [PATCH 2/5] Revert changes --- .../AutoCompleteBoxTests.cs | 25 ++++++++++--------- .../ListBoxTests.cs | 15 +++++------ .../MaskedTextBoxTests.cs | 13 +++++----- .../SelectingItemsControlTests_Multiple.cs | 8 +++--- .../TextBlockTests.cs | 8 +++--- 5 files changed, 35 insertions(+), 34 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 014b1bc01cd..532a15b46ab 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -1,18 +1,19 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; -using System.Reactive.Subjects; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Threading; +using Avalonia.UnitTests; +using Xunit; +using System.Collections.ObjectModel; +using System.Reactive.Subjects; +using Avalonia.Headless; using Avalonia.Harfbuzz; using Avalonia.Input; using Avalonia.Platform; -using Avalonia.Threading; -using Avalonia.UnitTests; using Moq; -using Xunit; namespace Avalonia.Controls.UnitTests { @@ -405,7 +406,7 @@ public void Custom_ItemSelector() Assert.Equal(control.Text, control.ItemSelector(input, selectedItem)); }); } - + [Fact] public void Text_Validation() { @@ -420,7 +421,7 @@ public void Text_Validation() Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true); }); } - + [Fact] public void Text_Validation_TextBox_Errors_Binding() { @@ -429,20 +430,20 @@ public void Text_Validation_TextBox_Errors_Binding() // simulate the TemplateBinding that would be used within the AutoCompleteBox control theme for the inner PART_TextBox // DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" textbox.Bind(DataValidationErrors.ErrorsProperty, control.GetBindingObservable(DataValidationErrors.ErrorsProperty)); - + var exception = new InvalidCastException("failed validation"); var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); control.Bind(AutoCompleteBox.TextProperty, textObservable); Dispatcher.UIThread.RunJobs(); - + Assert.True(DataValidationErrors.GetHasErrors(control)); Assert.Equal([exception], DataValidationErrors.GetErrors(control)); - + Assert.True(DataValidationErrors.GetHasErrors(textbox)); Assert.Equal([exception], DataValidationErrors.GetErrors(textbox)); }); } - + [Fact] public void SelectedItem_Validation() { @@ -1197,7 +1198,7 @@ private void RunTest(Action test) AutoCompleteBox control = CreateControl(); control.ItemsSource = CreateSimpleStringArray(); TextBox textBox = GetTextBox(control); - var window = new Window { Content = control }; + var window = new Window {Content = control}; window.ApplyStyling(); window.ApplyTemplate(); window.Presenter.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 0ebbbdd9b9c..ecffcbac0f8 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -13,6 +13,7 @@ using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml.Templates; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -774,9 +775,9 @@ public void Arrow_Keys_Should_Move_Selection_Horizontal() { Template = ListBoxTemplate(), ItemsSource = items, - ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel + ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel { - Orientation = Orientation.Horizontal + Orientation = Orientation.Horizontal }), ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), SelectedIndex = 0, @@ -1110,8 +1111,8 @@ public void Tab_Navigation_Should_Move_To_First_Item_When_No_Anchor_Element_Sele Items = { "Foo", "Bar", "Baz" }, }; - var button = new Button - { + var button = new Button + { Content = "Button", [DockPanel.DockProperty] = Dock.Top, }; @@ -1346,9 +1347,9 @@ private class DataVirtualizingList : IList { private readonly List _inner = new(Enumerable.Repeat(null, 100)); - public object this[int index] - { - get => _inner[index] = $"Item{index}"; + public object this[int index] + { + get => _inner[index] = $"Item{index}"; set => throw new NotSupportedException(); } diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index 9d0cc9ad7ae..f9eb31d2165 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -137,7 +137,7 @@ public void Press_Ctrl_A_Select_All_Text() Template = CreateTemplate(), Text = "1234" }; - + target.ApplyTemplate(); RaiseKeyEvent(target, Key.A, KeyModifiers.Control); @@ -192,7 +192,7 @@ public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_I Text = "First Second Third Fourth", CaretIndex = 5 }; - + textBox.ApplyTemplate(); // (First| Second Third Fourth) @@ -234,7 +234,7 @@ public void Control_Delete_Should_Remove_The_Word_After_The_Caret_If_There_Is_No Text = "First Second Third Fourth", CaretIndex = 19 }; - + textBox.ApplyTemplate(); // (First Second Third |Fourth) @@ -337,7 +337,7 @@ public void Press_Enter_Add_Default_Newline() Template = CreateTemplate(), AcceptsReturn = true }; - + target.ApplyTemplate(); RaiseKeyEvent(target, Key.Enter, 0); @@ -454,7 +454,7 @@ public void Press_Enter_Add_Custom_Newline() AcceptsReturn = true, NewLine = "Test" }; - + target.ApplyTemplate(); RaiseKeyEvent(target, Key.Enter, 0); @@ -897,8 +897,7 @@ public void Invalid_Text_Is_Coerced_Without_Raising_Intermediate_Change() }; var impl = CreateMockTopLevelImpl(); - var topLevel = new TestTopLevel(impl.Object) - { + var topLevel = new TestTopLevel(impl.Object) { Template = CreateTopLevelTemplate(), Content = target }; diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index d5148e9b1f0..92d2d44558f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1031,8 +1031,8 @@ public void Can_Change_Selection_For_Containers_Outside_Of_Viewport() { // Issue #11119 using var app = Start(); - var items = Enumerable.Range(0, 100).Select(x => new TestContainer - { + var items = Enumerable.Range(0, 100).Select(x => new TestContainer + { Content = $"Item {x}", Height = 100, }).ToList(); @@ -1093,7 +1093,7 @@ public void Selection_Is_Not_Cleared_On_Recycling_Containers() // Create a SelectingItemsControl that creates containers that raise IsSelectedChanged, // with a virtualizing stack panel. var target = CreateTarget( - itemsSource: items, + itemsSource: items, virtualizing: true); target.AutoScrollToSelectedItem = false; @@ -1187,7 +1187,7 @@ private static TestSelector CreateTarget( bool virtualizing = false) { return CreateTarget( - dataContext: dataContext, + dataContext: dataContext, items: items, itemsSource: itemsSource, itemContainerTheme: itemContainerTheme, diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index 52ca72f26a9..fc0b1818d56 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -169,7 +169,7 @@ public void Can_Call_Measure_Without_InvalidateTextLayout() { var target = new TextBlock(); - target.Inlines.Add(new TextBox { Text = "Hello" }); + target.Inlines.Add(new TextBox { Text = "Hello"}); target.Measure(Size.Infinity); @@ -285,7 +285,7 @@ public void Changing_InlineHost_Should_Propagate_To_Nested_Inlines() var span = new Span { Inlines = new InlineCollection { new Run { Text = "World" } } }; - var inlines = new InlineCollection { new Run { Text = "Hello " }, span }; + var inlines = new InlineCollection{ new Run{Text = "Hello "}, span }; target.Inlines = inlines; @@ -425,7 +425,7 @@ public void Setting_Text_Should_Reset_Inlines() Assert.Equal(0, target.Inlines.Count); } } - + [Fact] public void Setting_TextDecorations_Should_Update_Inlines() { @@ -446,7 +446,7 @@ public void Setting_TextDecorations_Should_Update_Inlines() Assert.Equal(underline, target.Inlines[0].TextDecorations); } } - + [Fact] public void TextBlock_TextLines_Should_Be_Empty() { From 9797c822bd1bb8ac0c2c9c5d4ae64f0bd3f7813e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 3 Nov 2025 12:44:07 +0100 Subject: [PATCH 3/5] Fix Android --- src/Android/Avalonia.Android/AndroidPlatform.cs | 1 + src/Android/Avalonia.Android/Avalonia.Android.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index d9e3e299bd3..5316a84570c 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -24,6 +24,7 @@ public static AppBuilder UseAndroid(this AppBuilder builder) return builder .UseAndroidRuntimePlatformSubsystem() .UseWindowingSubsystem(() => AndroidPlatform.Initialize(), "Android") + .UseHarfBuzz() .UseSkia(); } } diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 2fb849ee0de..ff499e88569 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -12,6 +12,7 @@ + From 667c898b03a0190b5d1d2f64dc284c9867738945 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 6 Nov 2025 13:55:10 +0100 Subject: [PATCH 4/5] Make the test happy --- tests/Avalonia.Controls.UnitTests/GridTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 7205db79506..3831c6faf65 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1818,6 +1818,7 @@ public void Grid_Controls_With_Spacing_With_Span_And_SharedSize() [Grid.ColumnSpanProperty] = 3, Content = new TextBlock() { + FontSize = 10, Text = @"0: 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1: 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 2: 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 @@ -1878,6 +1879,7 @@ public void Grid_Controls_With_Spacing_With_Span_And_SharedSize() { [Grid.ColumnProperty] = 1, Height = 20, + Width = 100, Text="1234567890" } } From 47a60758bce78c2f94e4365835cd9cebca7f90f9 Mon Sep 17 00:00:00 2001 From: Gillibald Date: Thu, 6 Nov 2025 18:06:56 +0100 Subject: [PATCH 5/5] Fix build --- Avalonia.sln | 4 ++-- src/Avalonia.Desktop/Avalonia.Desktop.csproj | 2 +- .../{Avalonia.Harfbuzz.csproj => Avalonia.HarfBuzz.csproj} | 0 tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj | 2 +- .../Avalonia.Controls.UnitTests.csproj | 1 + .../Avalonia.DesignerSupport.TestApp.csproj | 1 + .../Avalonia.Headless.NUnit.UnitTests.csproj | 2 +- .../Avalonia.Headless.XUnit.UnitTests.csproj | 2 +- tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj | 2 +- 9 files changed, 9 insertions(+), 7 deletions(-) rename src/HarfBuzz/Avalonia.HarfBuzz/{Avalonia.Harfbuzz.csproj => Avalonia.HarfBuzz.csproj} (100%) diff --git a/Avalonia.sln b/Avalonia.sln index ee00694efc7..8c7aa63c712 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -302,9 +302,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.MacCatalyst" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.tvOS", "samples\ControlCatalog.tvOS\ControlCatalog.tvOS.csproj", "{14342787-B4EF-4076-8C91-BA6C523DE8DF}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Harfbuzz", "Harfbuzz", "{7670D720-6E84-4AFC-8331-A5C399481905}" +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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.HarfBuzz", "src\HarfBuzz\Avalonia.HarfBuzz\Avalonia.HarfBuzz.csproj", "{E2BFA463-6402-4EF8-8945-FD9A10A914D1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Avalonia.Desktop/Avalonia.Desktop.csproj b/src/Avalonia.Desktop/Avalonia.Desktop.csproj index 7e1966611f9..051d23fc120 100644 --- a/src/Avalonia.Desktop/Avalonia.Desktop.csproj +++ b/src/Avalonia.Desktop/Avalonia.Desktop.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.Harfbuzz.csproj b/src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.HarfBuzz.csproj similarity index 100% rename from src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.Harfbuzz.csproj rename to src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.HarfBuzz.csproj diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 026219ffd9f..9b3aa3a4aef 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index dcff6ea10ab..55d7acd5216 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -14,6 +14,7 @@ + diff --git a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj index 7067b5aa458..065a9244fe8 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj +++ b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj @@ -16,6 +16,7 @@ + diff --git a/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj b/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj index 3a1dcb68e39..6b06b0b0d22 100644 --- a/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj +++ b/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj @@ -25,7 +25,7 @@ - + diff --git a/tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj b/tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj index a13220d7582..9501c7b38e9 100644 --- a/tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj +++ b/tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj @@ -18,7 +18,7 @@ - + diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 1b068b25bde..e3b89125e88 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -10,7 +10,7 @@ - +