diff --git a/Avalonia.sln b/Avalonia.sln index 5dfd11b6719..8c7aa63c712 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 fbf22358ed5..bbcf5564c98 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -3,21 +3,165 @@ CP0002 - M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType) - baseline/Avalonia/lib/net6.0/Avalonia.Dialogs.dll - current/Avalonia/lib/net6.0/Avalonia.Dialogs.dll + 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.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType) - baseline/Avalonia/lib/net8.0/Avalonia.Dialogs.dll - current/Avalonia/lib/net8.0/Avalonia.Dialogs.dll + 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.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType) - baseline/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll - current/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll + 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 @@ -43,6 +187,24 @@ 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) @@ -51,7 +213,55 @@ CP0006 - M:Avalonia.Platform.Storage.IStorageProvider.SaveFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerSaveOptions) + 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 @@ -97,6 +307,24 @@ 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) @@ -105,7 +333,55 @@ CP0006 - M:Avalonia.Platform.Storage.IStorageProvider.SaveFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerSaveOptions) + 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 @@ -151,6 +427,24 @@ 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) @@ -159,7 +453,55 @@ CP0006 - M:Avalonia.Platform.Storage.IStorageProvider.SaveFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerSaveOptions) + 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 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/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 @@ + 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..051d23fc120 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..9b3aa3a4aef 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..532a15b46ab 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -10,6 +10,7 @@ using System.Collections.ObjectModel; using System.Reactive.Subjects; using Avalonia.Headless; +using Avalonia.Harfbuzz; using Avalonia.Input; using Avalonia.Platform; using Moq; @@ -371,7 +372,7 @@ public void Item_Search() Assert.Equal(textbox.Text, control.Text); }); } - + [Fact] public void Custom_TextSelector() { @@ -388,7 +389,7 @@ public void Custom_TextSelector() Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString())); }); } - + [Fact] public void Custom_ItemSelector() { @@ -1265,7 +1266,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/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.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/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" } } 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..ecffcbac0f8 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -1213,10 +1213,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()); } diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index ce85984f1c0..f9eb31d2165 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; @@ -926,13 +927,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..92d2d44558f 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; @@ -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..fc0b1818d56 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 { @@ -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); } } @@ -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.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 301b96e0e85..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,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..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,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 00000000000..17767980183 Binary files /dev/null and b/tests/Avalonia.RenderTests/Assets/NotoSansTamil-Regular.ttf differ 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 faacaa7d6dd..939d086418d 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); @@ -1349,8 +1349,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..e3b89125e88 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 5c62bfd1797..03355927787 100644 Binary files a/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png and b/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png differ