diff --git a/Avalonia.sln b/Avalonia.sln index 52418bf205..1083462dcc 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -34,8 +34,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs src\Shared\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.Markup", "src\Markup\Avalonia.Markup\Avalonia.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}" @@ -80,6 +80,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia", "src\Skia\A EndProject 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 @@ -107,7 +108,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}" @@ -275,6 +275,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 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.PerAssembly.UnitTests", "tests\Avalonia.Headless.NUnit.PerAssembly.UnitTests\Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj", "{A175EFAE-476C-4DAA-87D5-742C18CFCC27}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.PerTest.UnitTests", "tests\Avalonia.Headless.NUnit.PerTest.UnitTests\Avalonia.Headless.NUnit.PerTest.UnitTests.csproj", "{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}" @@ -643,6 +647,10 @@ Global {14342787-B4EF-4076-8C91-BA6C523DE8DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {14342787-B4EF-4076-8C91-BA6C523DE8DF}.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 {A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Debug|Any CPU.Build.0 = Debug|Any CPU {A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -750,6 +758,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} {A175EFAE-476C-4DAA-87D5-742C18CFCC27} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {342D2657-2F84-493C-B74B-9D2CAE5D9DAB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} diff --git a/api/Avalonia.Win32.Interoperability.nupkg.xml b/api/Avalonia.Win32.Interoperability.nupkg.xml index 3672bb9b99..33fc2ac062 100644 --- a/api/Avalonia.Win32.Interoperability.nupkg.xml +++ b/api/Avalonia.Win32.Interoperability.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -37,4 +37,4 @@ baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll - + \ No newline at end of file diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 58654f7d37..cab4bf5580 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1,741 +1,879 @@ - + CP0001 - T:Avalonia.Data.IBinding + T:Avalonia.Media.IGlyphTypeface baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0001 - T:Avalonia.Data.InstancedBinding - baseline/Avalonia/lib/net10.0/Avalonia.Base.dll - current/Avalonia/lib/net10.0/Avalonia.Base.dll + T:Avalonia.Media.IGlyphTypeface + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Media.Fonts.FontFamilyLoader + CP0002 + M:Avalonia.Media.FontManager.TryGetGlyphTypeface(Avalonia.Media.Typeface,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Data.BindingBase - baseline/Avalonia/lib/net10.0/Avalonia.Markup.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.dll - - - CP0001 - T:Avalonia.Data.RelativeSourceMode - baseline/Avalonia/lib/net10.0/Avalonia.Markup.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.dll - - - CP0001 - T:Avalonia.Data.TreeType - baseline/Avalonia/lib/net10.0/Avalonia.Markup.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.dll + CP0002 + M:Avalonia.Media.FontMetrics.get_DesignEmHeight + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Media.Fonts.FontFamilyLoader - baseline/Avalonia/lib/net6.0/Avalonia.Base.dll - current/Avalonia/lib/net6.0/Avalonia.Base.dll + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.TryAddGlyphTypeface(Avalonia.Media.IGlyphTypeface) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Data.IBinding - baseline/Avalonia/lib/net8.0/Avalonia.Base.dll - current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.TryAddGlyphTypeface(System.IO.Stream,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Data.InstancedBinding - baseline/Avalonia/lib/net8.0/Avalonia.Base.dll - current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.TryAddGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.IGlyphTypeface) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Media.Fonts.FontFamilyLoader - baseline/Avalonia/lib/net8.0/Avalonia.Base.dll - current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.TryCreateSyntheticGlyphTypeface(Avalonia.Media.IGlyphTypeface,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Data.BindingBase - baseline/Avalonia/lib/net8.0/Avalonia.Markup.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.dll + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Data.RelativeSourceMode - baseline/Avalonia/lib/net8.0/Avalonia.Markup.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.dll + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Data.TreeType - baseline/Avalonia/lib/net8.0/Avalonia.Markup.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.dll + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetNearestMatch(System.Collections.Generic.IDictionary{Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.IGlyphTypeface},Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Media.Fonts.FontFamilyLoader - baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetNearestMatch(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache + M:Avalonia.Media.Fonts.IFontCollection.TryGetGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.AvaloniaObject.Bind(Avalonia.AvaloniaProperty,Avalonia.Data.IBinding) + M:Avalonia.Media.GlyphMetrics.get_Height baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.AvaloniaObject.get_Item(Avalonia.Data.IndexerDescriptor) + M:Avalonia.Media.GlyphMetrics.get_Width baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.AvaloniaObjectExtensions.Bind(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,Avalonia.Data.IBinding,System.Object) + M:Avalonia.Media.GlyphRun.#ctor(Avalonia.Media.IGlyphTypeface,System.Double,System.ReadOnlyMemory{System.Char},System.Collections.Generic.IReadOnlyList{Avalonia.Media.TextFormatting.GlyphInfo},System.Nullable{Avalonia.Point},System.Int32) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.AvaloniaObjectExtensions.ToBinding``1(System.IObservable{``0}) + M:Avalonia.Media.GlyphRun.#ctor(Avalonia.Media.IGlyphTypeface,System.Double,System.ReadOnlyMemory{System.Char},System.Collections.Generic.IReadOnlyList{System.UInt16},System.Nullable{Avalonia.Point},System.Int32) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.BindingOperations.Apply(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,Avalonia.Data.InstancedBinding,System.Object) + M:Avalonia.Media.GlyphRun.get_GlyphTypeface baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.BindingOperations.Apply(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,Avalonia.Data.InstancedBinding) + M:Avalonia.Media.IGlyphTypeface.GetGlyph(System.UInt32) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.TemplateBinding.Initiate(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,System.Object,System.Boolean) + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.TemplateBinding.ProvideValue + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvances(System.ReadOnlySpan{System.UInt16}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.TemplateBinding.Subscribe(System.IObserver{System.Object}) + M:Avalonia.Media.IGlyphTypeface.GetGlyphs(System.ReadOnlySpan{System.UInt32}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + M:Avalonia.Media.IGlyphTypeface.TryGetGlyph(System.UInt32,System.UInt16@) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + M:Avalonia.Media.IGlyphTypeface.TryGetTable(System.UInt32,System.Byte[]@) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(Avalonia.Media.IEffect) + M:Avalonia.Media.TextFormatting.ShapedBuffer.#ctor(System.ReadOnlyMemory{System.Char},System.Int32,Avalonia.Media.IGlyphTypeface,System.Double,System.SByte) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.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@) + M:Avalonia.Media.TextFormatting.ShapedBuffer.get_GlyphTypeface baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.StyledElementExtensions.BindClass(Avalonia.StyledElement,System.String,Avalonia.Data.IBinding,System.Object) + M:Avalonia.Media.TextFormatting.TextMetrics.#ctor(Avalonia.Media.IGlyphTypeface,System.Double) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.ItemsControl.DisplayMemberBindingProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.IGlyphTypeface,System.Collections.Generic.IReadOnlyList{Avalonia.Media.FontFeature},System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedValueBindingProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.IGlyphTypeface,System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.Primitives.TextSearch.TextBindingProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.TextFormatting.TextShaperOptions.get_Typeface + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.TextBlock.LetterSpacingProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.Typeface.get_GlyphTypeface + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.TextBox.LetterSpacingProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.IO.Stream,Avalonia.Media.FontSimulations,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.AutoCompleteBox.BindingEvaluator`1.#ctor(Avalonia.Data.IBinding) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.AutoCompleteBox.BindingEvaluator`1.get_ValueBinding - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.AutoCompleteBox.get_ValueMemberBinding - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Platform.IGlyphRunImpl.get_GlyphTypeface + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.CreatePreviewWithControl(System.Object) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.IGlyphTypeface,System.Double,System.Collections.Generic.IReadOnlyList{Avalonia.Media.TextFormatting.GlyphInfo},Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.GetDataContext(Avalonia.Controls.Templates.IDataTemplate) + F:Avalonia.Controls.Documents.TextElement.LetterSpacingProperty baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.GetPreviewWith(Avalonia.Controls.Templates.IDataTemplate) + F:Avalonia.Controls.Presenters.ContentPresenter.LetterSpacingProperty baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.GetPreviewWith(Avalonia.Styling.IStyle) + F:Avalonia.Controls.Primitives.TemplatedControl.LetterSpacingProperty baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetDataContext(Avalonia.Controls.Templates.IDataTemplate,System.Object) + F:Avalonia.Controls.TextBlock.LetterSpacingProperty baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.AvaloniaObject,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + M:Avalonia.Controls.Documents.TextElement.get_LetterSpacing baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.ResourceDictionary,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + M:Avalonia.Controls.Documents.TextElement.GetLetterSpacing(Avalonia.Controls.Control) baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.Templates.IDataTemplate,Avalonia.Controls.Control) + M:Avalonia.Controls.Documents.TextElement.set_LetterSpacing(System.Double) baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.Templates.IDataTemplate,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + M:Avalonia.Controls.Documents.TextElement.SetLetterSpacing(Avalonia.Controls.Control,System.Double) baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Styling.IStyle,Avalonia.Controls.Control) + M:Avalonia.Controls.Presenters.ContentPresenter.get_LetterSpacing baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Styling.IStyle,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + M:Avalonia.Controls.Presenters.ContentPresenter.set_LetterSpacing(System.Double) baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.ItemsControl.get_DisplayMemberBinding + M:Avalonia.Controls.Primitives.TemplatedControl.get_LetterSpacing baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.SelectingItemsControl.get_SelectedValueBinding + M:Avalonia.Controls.Primitives.TemplatedControl.set_LetterSpacing(System.Double) baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.TextSearch.GetTextBinding(Avalonia.Interactivity.Interactive) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.GlyphMetrics.get_Height + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.TextSearch.SetTextBinding(Avalonia.Interactivity.Interactive,Avalonia.Data.IBinding) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.GlyphMetrics.get_Width + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Templates.FuncTreeDataTemplate.ItemsSelector(System.Object) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.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.Controls.Templates.ITreeDataTemplate.ItemsSelector(System.Object) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + 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.Data.MultiBinding.get_Bindings - baseline/Avalonia/lib/net10.0/Avalonia.Markup.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.dll + 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.Data.MultiBinding.Initiate(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,System.Object,System.Boolean) - baseline/Avalonia/lib/net10.0/Avalonia.Markup.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.dll + 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.Markup.Xaml.MarkupExtensions.DynamicResourceExtension.ProvideValue(System.IServiceProvider) - baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll + 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.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension.ProvideValue(System.IServiceProvider) - baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll + 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.Markup.Xaml.Templates.TreeDataTemplate.ItemsSelector(System.Object) - baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll + 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 - F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.IO.Stream,Avalonia.Media.FontSimulations,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net6.0/Avalonia.Base.dll current/Avalonia/lib/net6.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net6.0/Avalonia.Base.dll current/Avalonia/lib/net6.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + 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.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + 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.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.FontManager.TryGetGlyphTypeface(Avalonia.Media.Typeface,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.FontMetrics.get_DesignEmHeight + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache + M:Avalonia.Media.Fonts.FontCollectionBase.TryAddGlyphTypeface(Avalonia.Media.IGlyphTypeface) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.AvaloniaObject.Bind(Avalonia.AvaloniaProperty,Avalonia.Data.IBinding) + M:Avalonia.Media.Fonts.FontCollectionBase.TryAddGlyphTypeface(System.IO.Stream,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.AvaloniaObject.get_Item(Avalonia.Data.IndexerDescriptor) + M:Avalonia.Media.Fonts.FontCollectionBase.TryAddGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.IGlyphTypeface) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.AvaloniaObjectExtensions.Bind(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,Avalonia.Data.IBinding,System.Object) + M:Avalonia.Media.Fonts.FontCollectionBase.TryCreateSyntheticGlyphTypeface(Avalonia.Media.IGlyphTypeface,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.AvaloniaObjectExtensions.ToBinding``1(System.IObservable{``0}) + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.BindingOperations.Apply(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,Avalonia.Data.InstancedBinding,System.Object) + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.BindingOperations.Apply(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,Avalonia.Data.InstancedBinding) + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetNearestMatch(System.Collections.Generic.IDictionary{Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.IGlyphTypeface},Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.TemplateBinding.Initiate(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,System.Object,System.Boolean) + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetNearestMatch(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.TemplateBinding.ProvideValue + M:Avalonia.Media.Fonts.IFontCollection.TryGetGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.TemplateBinding.Subscribe(System.IObserver{System.Object}) + M:Avalonia.Media.GlyphMetrics.get_Height baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + M:Avalonia.Media.GlyphMetrics.get_Width baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + M:Avalonia.Media.GlyphRun.#ctor(Avalonia.Media.IGlyphTypeface,System.Double,System.ReadOnlyMemory{System.Char},System.Collections.Generic.IReadOnlyList{Avalonia.Media.TextFormatting.GlyphInfo},System.Nullable{Avalonia.Point},System.Int32) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(Avalonia.Media.IEffect) + M:Avalonia.Media.GlyphRun.#ctor(Avalonia.Media.IGlyphTypeface,System.Double,System.ReadOnlyMemory{System.Char},System.Collections.Generic.IReadOnlyList{System.UInt16},System.Nullable{Avalonia.Point},System.Int32) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + M:Avalonia.Media.GlyphRun.get_GlyphTypeface baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.StyledElementExtensions.BindClass(Avalonia.StyledElement,System.String,Avalonia.Data.IBinding,System.Object) + M:Avalonia.Media.IGlyphTypeface.get_GlyphCount baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.ItemsControl.DisplayMemberBindingProperty - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + M:Avalonia.Media.IGlyphTypeface.GetGlyph(System.UInt32) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedValueBindingProperty - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.Primitives.TextSearch.TextBindingProperty - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + 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 - F:Avalonia.Controls.TextBlock.LetterSpacingProperty - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + 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 - F:Avalonia.Controls.TextBox.LetterSpacingProperty - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + 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.Controls.AutoCompleteBox.BindingEvaluator`1.#ctor(Avalonia.Data.IBinding) - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + 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.Controls.AutoCompleteBox.BindingEvaluator`1.get_ValueBinding - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + M:Avalonia.Media.TextFormatting.ShapedBuffer.#ctor(System.ReadOnlyMemory{System.Char},System.Int32,Avalonia.Media.IGlyphTypeface,System.Double,System.SByte) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.AutoCompleteBox.get_ValueMemberBinding - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + M:Avalonia.Media.TextFormatting.ShapedBuffer.get_GlyphTypeface + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.CreatePreviewWithControl(System.Object) - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + M:Avalonia.Media.TextFormatting.TextMetrics.#ctor(Avalonia.Media.IGlyphTypeface,System.Double) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.GetDataContext(Avalonia.Controls.Templates.IDataTemplate) - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.IGlyphTypeface,System.Collections.Generic.IReadOnlyList{Avalonia.Media.FontFeature},System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.GetPreviewWith(Avalonia.Controls.Templates.IDataTemplate) - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.IGlyphTypeface,System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.GetPreviewWith(Avalonia.Styling.IStyle) - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + M:Avalonia.Media.TextFormatting.TextShaperOptions.get_Typeface + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.SetDataContext(Avalonia.Controls.Templates.IDataTemplate,System.Object) - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + M:Avalonia.Media.Typeface.get_GlyphTypeface + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.IO.Stream,Avalonia.Media.FontSimulations,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.AvaloniaObject,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGlyphRunImpl.get_GlyphTypeface + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.IGlyphTypeface,System.Double,System.Collections.Generic.IReadOnlyList{Avalonia.Media.TextFormatting.GlyphInfo},Avalonia.Point) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + F:Avalonia.Controls.Documents.TextElement.LetterSpacingProperty baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.ResourceDictionary,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + F:Avalonia.Controls.Presenters.ContentPresenter.LetterSpacingProperty baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.Templates.IDataTemplate,Avalonia.Controls.Control) + F:Avalonia.Controls.Primitives.TemplatedControl.LetterSpacingProperty baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.Templates.IDataTemplate,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + F:Avalonia.Controls.TextBlock.LetterSpacingProperty baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Styling.IStyle,Avalonia.Controls.Control) + M:Avalonia.Controls.Documents.TextElement.get_LetterSpacing baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Styling.IStyle,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + M:Avalonia.Controls.Documents.TextElement.GetLetterSpacing(Avalonia.Controls.Control) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.ItemsControl.get_DisplayMemberBinding + M:Avalonia.Controls.Documents.TextElement.set_LetterSpacing(System.Double) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.SelectingItemsControl.get_SelectedValueBinding + M:Avalonia.Controls.Documents.TextElement.SetLetterSpacing(Avalonia.Controls.Control,System.Double) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.TextSearch.GetTextBinding(Avalonia.Interactivity.Interactive) + M:Avalonia.Controls.Presenters.ContentPresenter.get_LetterSpacing baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.TextSearch.SetTextBinding(Avalonia.Interactivity.Interactive,Avalonia.Data.IBinding) + M:Avalonia.Controls.Presenters.ContentPresenter.set_LetterSpacing(System.Double) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Templates.FuncTreeDataTemplate.ItemsSelector(System.Object) + M:Avalonia.Controls.Primitives.TemplatedControl.get_LetterSpacing baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Templates.ITreeDataTemplate.ItemsSelector(System.Object) + M:Avalonia.Controls.Primitives.TemplatedControl.set_LetterSpacing(System.Double) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.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.Media.GlyphMetrics.get_Height + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.MultiBinding.get_Bindings - baseline/Avalonia/lib/net8.0/Avalonia.Markup.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.dll + M:Avalonia.Media.GlyphMetrics.get_Width + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.MultiBinding.Initiate(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,System.Object,System.Boolean) - baseline/Avalonia/lib/net8.0/Avalonia.Markup.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.dll + 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.Markup.Xaml.MarkupExtensions.DynamicResourceExtension.ProvideValue(System.IServiceProvider) - baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + 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.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension.ProvideValue(System.IServiceProvider) - baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + 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.Markup.Xaml.Templates.TreeDataTemplate.ItemsSelector(System.Object) - baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + 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 - F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache + 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.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + 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.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + 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@) + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.IO.Stream,Avalonia.Media.FontSimulations,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.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.IFontManagerImpl.TryCreateGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IDrawingContextImplWithEffects.PopEffect - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.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/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(Avalonia.Media.IEffect) - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll + M:Avalonia.Platform.IGlyphRunImpl.get_GlyphTypeface + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll CP0006 - M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(System.Nullable{Avalonia.Rect},Avalonia.Media.IEffect) + M:Avalonia.Media.Fonts.IFontCollection.TryCreateSyntheticGlyphTypeface(Avalonia.Media.GlyphTypeface,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.GlyphTypeface@) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0006 - M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + M:Avalonia.Media.Fonts.IFontCollection.TryCreateSyntheticGlyphTypeface(Avalonia.Media.IGlyphTypeface,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0006 - M:Avalonia.Controls.Templates.ITreeDataTemplate.BindChildren(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,System.Object) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.Fonts.IFontCollection.TryGetFamilyTypefaces(System.String,System.Collections.Generic.IReadOnlyList{Avalonia.Media.Typeface}@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0006 - M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer) - baseline/Avalonia/lib/net6.0/Avalonia.Base.dll - current/Avalonia/lib/net6.0/Avalonia.Base.dll + M:Avalonia.Media.Fonts.IFontCollection.TryGetGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.GlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0006 - M:Avalonia.Input.Platform.IClipboard.TryGetDataAsync - baseline/Avalonia/lib/net6.0/Avalonia.Base.dll - current/Avalonia/lib/net6.0/Avalonia.Base.dll + M:Avalonia.Media.Fonts.IFontCollection.TryGetNearestMatch(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.GlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0006 - M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataAsync + M:Avalonia.Media.Fonts.IFontCollection.TryGetNearestMatch(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.IO.Stream,Avalonia.Media.FontSimulations,Avalonia.Media.IPlatformTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IPlatformTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryGetFamilyTypefaces(System.String,System.Collections.Generic.IReadOnlyList{Avalonia.Media.Typeface}@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.IPlatformTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphTypeface,System.Double,System.Collections.Generic.IReadOnlyList{Avalonia.Media.TextFormatting.GlyphInfo},Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.ITextShaperImpl.CreateTypeface(Avalonia.Media.GlyphTypeface) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.ITextShaperImpl.CreateTypeface(Avalonia.Media.IGlyphTypeface) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.CharacterToGlyphMap + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.FaceNames + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.FamilyNames + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.PlatformTypeface + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.SupportedFeatures + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.TextShaperTypeface + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Media.IGlyphTypeface.TypographicFamilyName + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.Platform.IClipboard.TryGetDataAsync + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataAsync baseline/Avalonia/lib/net6.0/Avalonia.Base.dll current/Avalonia/lib/net6.0/Avalonia.Base.dll @@ -747,7 +885,31 @@ CP0006 - M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + 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.TryCreateGlyphTypeface(System.IO.Stream,Avalonia.Media.FontSimulations,Avalonia.Media.IPlatformTypeface@) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IPlatformTypeface@) + 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 @@ -759,7 +921,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 @@ -807,338 +1017,296 @@ CP0006 - M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(System.Nullable{Avalonia.Rect},Avalonia.Media.IEffect) + M:Avalonia.Media.Fonts.IFontCollection.TryCreateSyntheticGlyphTypeface(Avalonia.Media.GlyphTypeface,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.GlyphTypeface@) 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.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + M:Avalonia.Media.Fonts.IFontCollection.TryCreateSyntheticGlyphTypeface(Avalonia.Media.IGlyphTypeface,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) 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) + M:Avalonia.Media.Fonts.IFontCollection.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.Storage.IStorageProvider.SaveFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerSaveOptions) + M:Avalonia.Media.Fonts.IFontCollection.TryGetGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.GlyphTypeface@) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 - M:Avalonia.Controls.Templates.ITreeDataTemplate.BindChildren(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,System.Object) - baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll - current/Avalonia/lib/net8.0/Avalonia.Controls.dll + M:Avalonia.Media.Fonts.IFontCollection.TryGetNearestMatch(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.GlyphTypeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 - M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) - baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll - current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + M:Avalonia.Media.Fonts.IFontCollection.TryGetNearestMatch(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IGlyphTypeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 - M:Avalonia.OpenGL.IGlExternalSemaphore.WaitTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) - baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll - current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + M:Avalonia.Media.IGlyphTypeface.GetGlyphAdvance(System.UInt16) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 - P:Avalonia.OpenGL.IGlExternalImageTexture.TextureType - baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll - current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.IO.Stream,Avalonia.Media.FontSimulations,Avalonia.Media.IPlatformTypeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 - M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer) - baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IPlatformTypeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 - M:Avalonia.Input.Platform.IClipboard.TryGetDataAsync - baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + 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.Input.Platform.IClipboard.TryGetInProcessDataAsync - baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + 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.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects) - baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,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.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) - baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + M:Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphTypeface,System.Double,System.Collections.Generic.IReadOnlyList{Avalonia.Media.TextFormatting.GlyphInfo},Avalonia.Point) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64) - baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 - M:Avalonia.Platform.Storage.IStorageProvider.SaveFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerSaveOptions) - baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + M:Avalonia.Platform.ITextShaperImpl.CreateTypeface(Avalonia.Media.GlyphTypeface) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 - M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) - baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll - current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll + 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 - M:Avalonia.OpenGL.IGlExternalSemaphore.WaitTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) - baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll - current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll + P:Avalonia.Media.IGlyphTypeface.CharacterToGlyphMap + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 - P:Avalonia.OpenGL.IGlExternalImageTexture.TextureType - baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll - current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll - - - CP0007 - T:Avalonia.Data.TemplateBinding - baseline/Avalonia/lib/net10.0/Avalonia.Base.dll - current/Avalonia/lib/net10.0/Avalonia.Base.dll - - - CP0007 - T:Avalonia.Data.TemplateBinding + P:Avalonia.Media.IGlyphTypeface.FaceNames baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll - CP0008 - T:Avalonia.Data.TemplateBinding - baseline/Avalonia/lib/net10.0/Avalonia.Base.dll - current/Avalonia/lib/net10.0/Avalonia.Base.dll - - - CP0008 - T:Avalonia.Data.Binding - baseline/Avalonia/lib/net10.0/Avalonia.Markup.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.dll - - - CP0008 - T:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension - baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll - - - CP0008 - T:Avalonia.Data.TemplateBinding + CP0006 + P:Avalonia.Media.IGlyphTypeface.FamilyNames baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll - CP0008 - T:Avalonia.Data.Binding - baseline/Avalonia/lib/net8.0/Avalonia.Markup.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.dll - - - CP0008 - T:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension - baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll - - - CP0009 - T:Avalonia.Data.TemplateBinding - baseline/Avalonia/lib/net10.0/Avalonia.Base.dll - current/Avalonia/lib/net10.0/Avalonia.Base.dll - - - CP0009 - T:Avalonia.Data.MultiBinding - baseline/Avalonia/lib/net10.0/Avalonia.Markup.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.dll - - - CP0009 - T:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension - baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll - - - CP0009 - T:Avalonia.Markup.Xaml.MarkupExtensions.DynamicResourceExtension - baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll + CP0006 + P:Avalonia.Media.IGlyphTypeface.GlyphCount + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll - CP0009 - T:Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension - baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll + CP0006 + P:Avalonia.Media.IGlyphTypeface.PlatformTypeface + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll - CP0009 - T:Avalonia.Data.TemplateBinding + CP0006 + P:Avalonia.Media.IGlyphTypeface.SupportedFeatures baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll - CP0009 - T:Avalonia.Data.MultiBinding - baseline/Avalonia/lib/net8.0/Avalonia.Markup.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.dll + CP0006 + P:Avalonia.Media.IGlyphTypeface.TextShaperTypeface + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll - CP0009 - T:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension - baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + CP0006 + P:Avalonia.Media.IGlyphTypeface.TypographicFamilyName + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll - CP0009 - T:Avalonia.Markup.Xaml.MarkupExtensions.DynamicResourceExtension - baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + CP0006 + M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll - CP0009 - T:Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension - baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + CP0006 + M:Avalonia.OpenGL.IGlExternalSemaphore.WaitTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.get_Count - baseline/Avalonia/lib/net10.0/Avalonia.Base.dll - current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 + P:Avalonia.OpenGL.IGlExternalImageTexture.TextureType + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32) - baseline/Avalonia/lib/net10.0/Avalonia.Base.dll - current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 + M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator - baseline/Avalonia/lib/net10.0/Avalonia.Base.dll - current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 + M:Avalonia.Input.Platform.IClipboard.TryGetDataAsync + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - P:Avalonia.Media.Fonts.FontCollectionBase.Count - baseline/Avalonia/lib/net10.0/Avalonia.Base.dll - current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 + M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataAsync + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32) - baseline/Avalonia/lib/net10.0/Avalonia.Base.dll - current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 + M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.get_Count - 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/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32) - baseline/Avalonia/lib/net6.0/Avalonia.Base.dll - current/Avalonia/lib/net6.0/Avalonia.Base.dll + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.IO.Stream,Avalonia.Media.FontSimulations,Avalonia.Media.IPlatformTypeface@) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator - baseline/Avalonia/lib/net6.0/Avalonia.Base.dll - current/Avalonia/lib/net6.0/Avalonia.Base.dll + CP0006 + M:Avalonia.Platform.IFontManagerImpl.TryCreateGlyphTypeface(System.String,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,Avalonia.Media.IPlatformTypeface@) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - P:Avalonia.Media.Fonts.FontCollectionBase.Count - 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/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32) - 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/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.get_Count - baseline/Avalonia/lib/net8.0/Avalonia.Base.dll - current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 + M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32) - baseline/Avalonia/lib/net8.0/Avalonia.Base.dll - current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 + M:Avalonia.Platform.ITextShaperImpl.CreateTypeface(Avalonia.Media.IGlyphTypeface) + baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator - 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/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - P:Avalonia.Media.Fonts.FontCollectionBase.Count - 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/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32) - 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/netstandard2.0/Avalonia.Base.dll + current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.get_Count + CP0006 + P:Avalonia.Media.IGlyphTypeface.GlyphCount baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32) + CP0006 + P:Avalonia.Media.IGlyphTypeface.PlatformTypeface baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator + CP0006 + P:Avalonia.Media.IGlyphTypeface.SupportedFeatures baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - P:Avalonia.Media.Fonts.FontCollectionBase.Count + CP0006 + P:Avalonia.Media.IGlyphTypeface.TextShaperTypeface baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - CP0012 - P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32) + CP0006 + P:Avalonia.Media.IGlyphTypeface.TypographicFamilyName baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - + + CP0006 + M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) + baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll + current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll + + + CP0006 + M:Avalonia.OpenGL.IGlExternalSemaphore.WaitTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) + baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll + current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll + + + CP0006 + P:Avalonia.OpenGL.IGlExternalImageTexture.TextureType + baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll + current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll + + \ No newline at end of file diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 4963d12997..b9f2d16168 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -10,7 +10,7 @@ using static Android.Content.Intent; namespace ControlCatalog.Android { - [Activity(Name = "com.Avalonia.ControlCatalog.MainActivity", Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, Exported = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] + [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, Exported = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] // CategoryLeanbackLauncher is required for Android TV. [IntentFilter(new[] { ActionView }, Categories = new[] { CategoryDefault, CategoryLeanbackLauncher })] public class MainActivity : AvaloniaMainActivity diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index 4f99b55d92..ea82860e0e 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.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Skia; using Avalonia.Threading; -using Avalonia.Utilities; using SkiaSharp; namespace RenderDemo.Pages @@ -21,7 +19,7 @@ namespace RenderDemo.Pages { ClipToBounds = true; var text = "Current rendering API is not Skia"; - var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.GetGlyph(ch)).ToArray(); + var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.CharacterToGlyphMap[ch]).ToArray(); _noSkia = new GlyphRun(Typeface.Default.GlyphTypeface, 12, text.AsMemory(), glyphs); } diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs index 8e8cc1cd72..a5a7911e15 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs @@ -22,7 +22,7 @@ namespace RenderDemo.Pages public class GlyphRunControl : Control { - private IGlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; + private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; private readonly Random _rand = new Random(); private ushort[] _glyphIndices = new ushort[1]; private char[] _characters = new char[1]; @@ -69,7 +69,7 @@ namespace RenderDemo.Pages _fontSize += _direction; - _glyphIndices[0] = _glyphTypeface.GetGlyph(c); + _glyphIndices[0] = _glyphTypeface.CharacterToGlyphMap[c]; _characters[0] = c; @@ -81,7 +81,7 @@ namespace RenderDemo.Pages public class GlyphRunGeometryControl : Control { - private IGlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; + private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; private readonly Random _rand = new Random(); private ushort[] _glyphIndices = new ushort[1]; private char[] _characters = new char[1]; @@ -128,7 +128,7 @@ namespace RenderDemo.Pages _fontSize += _direction; - _glyphIndices[0] = _glyphTypeface.GetGlyph(c); + _glyphIndices[0] = _glyphTypeface.CharacterToGlyphMap[c]; _characters[0] = c; diff --git a/samples/TextTestApp/MainWindow.axaml.cs b/samples/TextTestApp/MainWindow.axaml.cs index 2bc6ac4160..239c6087d8 100644 --- a/samples/TextTestApp/MainWindow.axaml.cs +++ b/samples/TextTestApp/MainWindow.axaml.cs @@ -223,12 +223,12 @@ namespace TextTestApp } } - private IImage CreateGlyphDrawing(IGlyphTypeface glyphTypeface, double emSize, GlyphInfo info) + private IImage CreateGlyphDrawing(GlyphTypeface glyphTypeface, double emSize, GlyphInfo info) { return new DrawingImage { Drawing = new GeometryDrawing { Brush = Brushes.Black, Geometry = GetGlyphOutline(glyphTypeface, emSize, info) } }; } - private Geometry GetGlyphOutline(IGlyphTypeface typeface, double emSize, GlyphInfo info) + private Geometry GetGlyphOutline(GlyphTypeface typeface, double emSize, GlyphInfo info) { // substitute for GlyphTypeface.GetGlyphOutline return new GlyphRun(typeface, emSize, new[] { '\0' }, [info]).BuildGeometry(); diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index d9e3e299bd..5316a84570 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -24,6 +24,7 @@ namespace Avalonia 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 dc1b5cd7dd..14376acfed 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 595a926f0c..9d1d0145d0 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -98,7 +98,7 @@ namespace Avalonia.Media /// /// True, if the could create the glyph typeface, False otherwise. /// - public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { glyphTypeface = null; @@ -109,7 +109,7 @@ namespace Avalonia.Media return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); } - + if (fontFamily.Key != null) { if (fontFamily.Key is CompositeFontFamilyKey compositeKey) @@ -187,7 +187,7 @@ namespace Avalonia.Media } } - private bool TryGetGlyphTypefaceByKeyAndName(Typeface typeface, FontFamilyKey key, string familyName, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + private bool TryGetGlyphTypefaceByKeyAndName(Typeface typeface, FontFamilyKey key, string familyName, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { var source = key.Source.EnsureAbsolute(key.BaseUri); @@ -271,7 +271,7 @@ namespace Avalonia.Media { typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); - if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.CharacterToGlyphMap.TryGetGlyph(codepoint, out _)) { return true; } @@ -300,6 +300,11 @@ namespace Avalonia.Media fontCollection.TryGetGlyphTypeface(familyName, fontStyle, fontWeight, fontStretch, out _) && fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) { + if (typeface.FontFamily.Name == DefaultFontFamily.Name && i + 1 < compositeKey.Keys.Count) + { + continue; + } + return true; } } @@ -328,24 +333,18 @@ namespace Avalonia.Media if (key == null) { - if (SystemFonts is IFontCollection2 fontCollection2) + if (SystemFonts.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces)) { - if (fontCollection2.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces)) - { - return familyTypefaces; - } + return familyTypefaces; } } else { var source = key.Source.EnsureAbsolute(key.BaseUri); - if (TryGetFontCollection(source, out var fontCollection) && fontCollection is IFontCollection2 fontCollection2) + if (TryGetFontCollection(source, out var fontCollection) && fontCollection.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces)) { - if (fontCollection2.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces)) - { - return familyTypefaces; - } + return familyTypefaces; } } @@ -374,7 +373,7 @@ namespace Avalonia.Media fontCollection = new EmbeddedFontCollection(source, source); } } - + if (fontCollection != null) { return _fontCollections.TryAdd(fontCollection.Key, fontCollection); diff --git a/src/Avalonia.Base/Media/FontMetrics.cs b/src/Avalonia.Base/Media/FontMetrics.cs index 6d952a6b93..1728c2ab44 100644 --- a/src/Avalonia.Base/Media/FontMetrics.cs +++ b/src/Avalonia.Base/Media/FontMetrics.cs @@ -8,7 +8,7 @@ /// /// Gets the font design units per em. /// - public short DesignEmHeight { get; init; } + public ushort DesignEmHeight { get; init; } /// /// A value indicating whether all glyphs in the font have the same advancement. diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index 8f2b43ff8d..4c535bdc0e 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -9,13 +9,13 @@ using Avalonia.Platform; namespace Avalonia.Media.Fonts { - public abstract class FontCollectionBase : IFontCollection2 + public abstract class FontCollectionBase : IFontCollection { private static readonly Comparer FontFamilyNameComparer = Comparer.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); // Make this internal for testing purposes - internal readonly ConcurrentDictionary> _glyphTypefaceCache = new(); + internal readonly ConcurrentDictionary> _glyphTypefaceCache = new(); private readonly object _fontFamiliesLock = new(); private volatile FontFamily[] _fontFamilies = Array.Empty(); @@ -39,12 +39,14 @@ namespace Avalonia.Media.Fonts { match = default; + var key = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }; + //If a font family is defined we try to find a match inside that family first if (familyName != null && _glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) { - if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface)) + if (TryGetNearestMatch(glyphTypefaces, key, out var glyphTypeface)) { - if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + if (glyphTypeface.CharacterToGlyphMap.TryGetGlyph(codepoint, out _)) { match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), style, weight, stretch); @@ -64,15 +66,17 @@ namespace Avalonia.Media.Fonts glyphTypefaces = pair.Value; - if (TryGetNearestMatch(glyphTypefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface)) + if (TryGetNearestMatch(glyphTypefaces, key, out var glyphTypeface)) { - if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + if (glyphTypeface.CharacterToGlyphMap.TryGetGlyph(codepoint, out _)) { + var platformTypeface = glyphTypeface.PlatformTypeface; + // Found a match - match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), - glyphTypeface.Style, - glyphTypeface.Weight, - glyphTypeface.Stretch); + match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), + platformTypeface.Style, + platformTypeface.Weight, + platformTypeface.Stretch); return true; } @@ -83,11 +87,11 @@ namespace Avalonia.Media.Fonts } public virtual bool TryCreateSyntheticGlyphTypeface( - IGlyphTypeface glyphTypeface, + GlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, - [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface) + [NotNullWhen(true)] out GlyphTypeface? syntheticGlyphTypeface) { syntheticGlyphTypeface = null; @@ -99,44 +103,40 @@ namespace Avalonia.Media.Fonts var key = new FontCollectionKey(style, weight, stretch); - var currentKey = - new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); - + var currentKey = glyphTypeface.ToFontCollectionKey(); + if (currentKey == key) { 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 (_fontManagerImpl.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface)) + if (_fontManagerImpl.TryCreateGlyphTypeface(stream, fontSimulations, out var platformTypeface)) { + syntheticGlyphTypeface = new GlyphTypeface(platformTypeface, fontSimulations); + //Add the TypographicFamilyName to the cache - if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) + if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName)) { - TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, syntheticGlyphTypeface); + TryAddGlyphTypeface(glyphTypeface.TypographicFamilyName, key, syntheticGlyphTypeface); } - foreach (var kvp in glyphTypeface2.FamilyNames) + foreach (var kvp in glyphTypeface.FamilyNames) { TryAddGlyphTypeface(kvp.Value, key, syntheticGlyphTypeface); } @@ -154,17 +154,11 @@ namespace Avalonia.Media.Fonts public IEnumerator GetEnumerator() => ((IEnumerable)_fontFamilies).GetEnumerator(); public virtual bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName); - style = typeface.Style; - - weight = typeface.Weight; - - stretch = typeface.Stretch; - - var key = new FontCollectionKey(style, weight, stretch); + var key = typeface.ToFontCollectionKey(); return TryGetGlyphTypeface(familyName, key, out glyphTypeface); } @@ -195,7 +189,7 @@ namespace Avalonia.Media.Fonts return false; } - public bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + public bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) { @@ -210,52 +204,59 @@ namespace Avalonia.Media.Fonts } /// - /// Attempts to add the specified to the font collection. + /// Attempts to add the specified to the font collection. /// - /// This method checks the and, if applicable, - /// the typographic family name and other family names provided by the interface. + /// This method checks the and, if applicable, + /// the typographic family name and other family names provided by the interface. /// If any of these names can be associated with the glyph typeface, the typeface is added to the collection. /// The method ensures that duplicate entries are not added. /// The glyph typeface to add. Must not be and must have a non-empty . + /// cref="GlyphTypeface.FamilyName"/>. /// if the glyph typeface was successfully added to the collection; otherwise, . - public bool TryAddGlyphTypeface(IGlyphTypeface glyphTypeface) + public bool TryAddGlyphTypeface(GlyphTypeface glyphTypeface) + { + var key = glyphTypeface.ToFontCollectionKey(); + + return TryAddGlyphTypeface(glyphTypeface, key); + } + + /// + /// Attempts to add the specified glyph typeface to the collection using the provided key. + /// + /// The method adds the glyph typeface using both its typographic family name and all + /// available family names. If the glyph typeface or its family name is invalid, the method returns false and + /// does not add the typeface. + /// The glyph typeface to add. Cannot be null, and its FamilyName property must not be null or empty. + /// The key that identifies the font collection to which the glyph typeface will be added. + /// true if the glyph typeface was successfully added to the collection; otherwise, false. + public bool TryAddGlyphTypeface(GlyphTypeface glyphTypeface, FontCollectionKey key) { if (glyphTypeface == null || string.IsNullOrEmpty(glyphTypeface.FamilyName)) { return false; } - var key = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); + var result = false; - if (glyphTypeface is IGlyphTypeface2 glyphTypeface2) + //Add the TypographicFamilyName to the cache + if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName)) { - var result = false; - - //Add the TypographicFamilyName to the cache - if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) + if (TryAddGlyphTypeface(glyphTypeface.TypographicFamilyName, key, glyphTypeface)) { - if (TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, glyphTypeface)) - { - result = true; - } + result = true; } + } - foreach (var kvp in glyphTypeface2.FamilyNames) + foreach (var kvp in glyphTypeface.FamilyNames) + { + if (TryAddGlyphTypeface(kvp.Value, key, glyphTypeface)) { - if (TryAddGlyphTypeface(kvp.Value, key, glyphTypeface)) - { - result = true; - } + result = true; } - - return result; - } - else - { - return TryAddGlyphTypeface(glyphTypeface.FamilyName, key, glyphTypeface); } + + return result; } /// @@ -265,17 +266,21 @@ namespace Avalonia.Media.Fonts /// If successful, it adds the created glyph typeface to the collection. /// The font stream containing the font data. The stream must be readable and positioned at the beginning of the /// font data. - /// When this method returns, contains the created instance if the operation + /// When this method returns, contains the created instance if the operation /// succeeds; otherwise, . /// if the glyph typeface was successfully created and added; otherwise, . - public bool TryAddGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + public bool TryAddGlyphTypeface(Stream stream, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { - if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out glyphTypeface)) + glyphTypeface = null; + + if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface)) { return false; } + glyphTypeface = new GlyphTypeface(platformTypeface); + return TryAddGlyphTypeface(glyphTypeface); } @@ -310,17 +315,19 @@ namespace Avalonia.Media.Fonts { var stream = _assetLoader.Open(fontAsset); - if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) + if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface)) { continue; } - var key = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); + var glyphTypeface = new GlyphTypeface(platformTypeface); + + var key = glyphTypeface.ToFontCollectionKey(); //Add TypographicFamilyName to the cache - if (glyphTypeface is IGlyphTypeface2 glyphTypeface2 && !string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName)) + if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName)) { - if (TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, glyphTypeface)) + if (TryAddGlyphTypeface(glyphTypeface.TypographicFamilyName, key, glyphTypeface)) { result = true; } @@ -346,8 +353,10 @@ namespace Avalonia.Media.Fonts using var stream = File.OpenRead(source.LocalPath); - if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) + if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface)) { + var glyphTypeface = new GlyphTypeface(platformTypeface); + if (TryAddGlyphTypeface(glyphTypeface)) { result = true; @@ -368,8 +377,10 @@ namespace Avalonia.Media.Fonts { using var stream = File.OpenRead(file); - if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) + if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface)) { + var glyphTypeface = new GlyphTypeface(platformTypeface); + if (TryAddGlyphTypeface(glyphTypeface)) { result = true; @@ -445,10 +456,10 @@ namespace Avalonia.Media.Fonts /// find the best match based on the provided . /// The name of the font family to search for. This parameter is case-insensitive. /// The key representing the desired font collection attributes. - /// When this method returns, contains the matching if a match is found; otherwise, + /// When this method returns, contains the matching if a match is found; otherwise, /// . /// if a matching glyph typeface is found; otherwise, . - protected bool TryGetGlyphTypeface(string familyName, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + protected bool TryGetGlyphTypeface(string familyName, FontCollectionKey key, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { glyphTypeface = null; @@ -461,7 +472,7 @@ namespace Avalonia.Media.Fonts if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) { - var matchedKey = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); + var matchedKey = glyphTypeface.ToFontCollectionKey(); if (matchedKey != key) { @@ -550,19 +561,21 @@ namespace Avalonia.Media.Fonts } /// - /// Attempts to retrieve the nearest matching for the specified font key from the + /// Attempts to retrieve the nearest matching for the specified font key from the /// provided collection of glyph typefaces. /// /// This method attempts to find the best match for the specified font key by considering - /// various fallback strategies, such as normalizing the font style, stretch, and weight. If no suitable match is found, the method will return the first available non-null from the + /// various fallback strategies, such as normalizing the font style, stretch, and weight. + /// If no suitable match is found, the method will return the first available non-null from the /// collection, if any. /// A collection of glyph typefaces, indexed by . /// The representing the desired font attributes. - /// When this method returns, contains the that most closely matches the specified + /// When this method returns, contains the that most closely matches the specified /// key, if a match is found; otherwise, . - /// if a matching is found; otherwise, if a matching is found; otherwise, . - protected bool TryGetNearestMatch(IDictionary glyphTypefaces, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + protected bool TryGetNearestMatch(IDictionary glyphTypefaces, + FontCollectionKey key, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) { @@ -627,7 +640,7 @@ namespace Avalonia.Media.Fonts /// The glyph typeface to add to the cache. Can be null. /// if the glyph typeface was successfully added to the cache; otherwise, . - protected bool TryAddGlyphTypeface(string familyName, FontCollectionKey key, IGlyphTypeface? glyphTypeface) + protected bool TryAddGlyphTypeface(string familyName, FontCollectionKey key, GlyphTypeface? glyphTypeface) { if (string.IsNullOrEmpty(familyName)) { @@ -651,7 +664,7 @@ namespace Avalonia.Media.Fonts } // Family doesn't exist yet. Create a new dictionary instance and try to install it. - var newDict = new ConcurrentDictionary(); + var newDict = new ConcurrentDictionary(); // GetOrAdd will return the instance that ended up in the dictionary. If it's our // newDict instance then we won the race to add the family and should publish it. @@ -693,9 +706,9 @@ namespace Avalonia.Media.Fonts /// null. /// true if a suitable fallback glyph typeface is found; otherwise, false. private static bool TryFindStretchFallback( - IDictionary glyphTypefaces, + IDictionary glyphTypefaces, FontCollectionKey key, - [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { glyphTypeface = null; @@ -740,9 +753,9 @@ namespace Avalonia.Media.Fonts /// null. /// true if a fallback glyph typeface matching the requested weight is found; otherwise, false. private static bool TryFindWeightFallback( - IDictionary glyphTypefaces, + IDictionary glyphTypefaces, FontCollectionKey key, - [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { glyphTypeface = null; var weight = (int)key.Weight; diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs index 0d0dc3016e..c9a1264c24 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs @@ -1,4 +1,12 @@ namespace Avalonia.Media.Fonts { + /// + /// Represents a unique key for identifying a font inside a font collection based on style, weight, and stretch attributes. + /// + /// Use this key to efficiently look up or group fonts in a collection by their style, weight, + /// and stretch characteristics. + /// The font style to use when constructing the key. + /// The font weight to use when constructing the key. + /// The font stretch to use when constructing the key. public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch); } diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionKeyExtensions.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionKeyExtensions.cs new file mode 100644 index 0000000000..5d583e175c --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionKeyExtensions.cs @@ -0,0 +1,50 @@ +using System; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + internal static class FontCollectionKeyExtensions + { + /// + /// Creates a new FontCollectionKey based on the style, weight, and stretch of the specified Typeface. + /// + /// The Typeface from which to extract style, weight, and stretch information. Cannot be null. + /// A FontCollectionKey representing the style, weight, and stretch of the specified Typeface. + public static FontCollectionKey ToFontCollectionKey(this Typeface typeface) + { + return new FontCollectionKey(typeface.Style, typeface.Weight, typeface.Stretch); + } + + /// + /// Creates a new FontCollectionKey based on the style, weight, and stretch of the specified GlyphTypeface. + /// + /// The GlyphTypeface instance from which to extract style, weight, and stretch information. Cannot be null. + /// A FontCollectionKey representing the style, weight, and stretch of the specified glyph typeface. + /// Thrown if glyphTypeface is null. + public static FontCollectionKey ToFontCollectionKey(this GlyphTypeface glyphTypeface) + { + if (glyphTypeface == null) + { + throw new ArgumentNullException(nameof(glyphTypeface)); + } + + return new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); + } + + /// + /// Creates a new FontCollectionKey based on the style, weight, and stretch of the specified platform typeface. + /// + /// The platform typeface from which to extract style, weight, and stretch information. Cannot be null. + /// A FontCollectionKey representing the style, weight, and stretch of the specified platform typeface. + /// Thrown if platformTypeface is null. + public static FontCollectionKey ToFontCollectionKey(this IPlatformTypeface platformTypeface) + { + if (platformTypeface == null) + { + throw new ArgumentNullException(nameof(platformTypeface)); + } + + return new FontCollectionKey(platformTypeface.Style, platformTypeface.Weight, platformTypeface.Stretch); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs index 4579cb5a34..17d1ef7f21 100644 --- a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs @@ -5,6 +5,12 @@ using System.Globalization; namespace Avalonia.Media.Fonts { + /// + /// Represents a collection of font families and provides methods for querying and managing font typefaces + /// within the collection. + /// + /// Implementations of this interface allow applications to retrieve font families, match + /// characters to typefaces, and obtain glyph typefaces based on specific font properties. public interface IFontCollection : IReadOnlyList, IDisposable { /// @@ -22,7 +28,7 @@ namespace Avalonia.Media.Fonts /// The glyph typeface. /// Returns true if a glyph typface can be found; otherwise, false bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface); /// /// Tries to match a specified character to a that supports specified font properties. @@ -39,17 +45,14 @@ namespace Avalonia.Media.Fonts /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface); - } - internal interface IFontCollection2 : IFontCollection - { /// /// Tries to get a list of typefaces for the specified family name. /// /// The family name. /// The list of typefaces. /// - /// True, if the could get the list of typefaces, False otherwise. + /// True, if the could get the list of typefaces, False otherwise. /// bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces); @@ -62,7 +65,8 @@ namespace Avalonia.Media.Fonts /// The font stretch. /// /// Returns true if a synthetic glyph typface can be created; otherwise, false - bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface); + bool TryCreateSyntheticGlyphTypeface(GlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, + [NotNullWhen(true)] out GlyphTypeface? syntheticGlyphTypeface); /// /// Attempts to retrieve the glyph typeface that most closely matches the specified font family name, style, @@ -77,9 +81,10 @@ namespace Avalonia.Media.Fonts /// The desired font style. /// The desired font weight. /// The desired font stretch. - /// When this method returns, contains the that most closely matches the specified + /// When this method returns, contains the that most closely matches the specified /// parameters, if a match is found; otherwise, . This parameter is passed uninitialized. /// if a matching glyph typeface is found; otherwise, . - bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, + [NotNullWhen(true)] out GlyphTypeface? glyphTypeface); } } diff --git a/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs b/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs index b0c725ca92..0a50cc6f13 100644 --- a/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs +++ b/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs @@ -2,11 +2,11 @@ namespace Avalonia.Media.Fonts { - internal readonly record struct OpenTypeTag + public readonly record struct OpenTypeTag { - public static readonly OpenTypeTag None = new OpenTypeTag(0, 0, 0, 0); - public static readonly OpenTypeTag Max = new OpenTypeTag(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); - public static readonly OpenTypeTag MaxSigned = new OpenTypeTag((byte)sbyte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); + internal static readonly OpenTypeTag None = new OpenTypeTag(0, 0, 0, 0); + internal static readonly OpenTypeTag Max = new OpenTypeTag(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); + internal static readonly OpenTypeTag MaxSigned = new OpenTypeTag((byte)sbyte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); private readonly uint _value; diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index 7ff8df9951..cf055e5d99 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -26,7 +26,7 @@ namespace Avalonia.Media.Fonts public override Uri Key => FontManager.SystemFontsKey; public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName); @@ -35,13 +35,7 @@ namespace Avalonia.Media.Fonts return true; } - style = typeface.Style; - - weight = typeface.Weight; - - stretch = typeface.Stretch; - - var key = new FontCollectionKey(style, weight, stretch); + var key = typeface.ToFontCollectionKey(); //Check cache first to avoid unnecessary calls to the font manager if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface)) @@ -50,7 +44,7 @@ namespace Avalonia.Media.Fonts } //Try to create the glyph typeface via system font manager - if (!_platformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + if (!_platformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out var platformTypeface)) { //Add null to cache to avoid future calls TryAddGlyphTypeface(familyName, key, null); @@ -58,9 +52,23 @@ namespace Avalonia.Media.Fonts return false; } + glyphTypeface = new GlyphTypeface(platformTypeface); + + //Add to cache with platform typeface family name first + TryAddGlyphTypeface(platformTypeface.FamilyName, key, glyphTypeface); + //Add to cache if (!TryAddGlyphTypeface(glyphTypeface)) { + // Another thread may have added an entry for this key while we were creating the glyph typeface. + // Re-check the cache and yield the existing glyph typeface if present. + if (_glyphTypefaceCache.TryGetValue(familyName, out var existingMap) && existingMap.TryGetValue(key, out var existingTypeface) && existingTypeface != null) + { + glyphTypeface = existingTypeface; + + return true; + } + return false; } @@ -70,14 +78,7 @@ namespace Avalonia.Media.Fonts public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) { - familyTypefaces = null; - - if (_platformImpl is IFontManagerImpl2 fontManagerImpl2) - { - return fontManagerImpl2.TryGetFamilyTypefaces(familyName, out familyTypefaces); - } - - return false; + return _platformImpl.TryGetFamilyTypefaces(familyName, out familyTypefaces); } public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName, @@ -85,10 +86,9 @@ namespace Avalonia.Media.Fonts { var requestedKey = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }; - //TODO12: Think about removing familyName parameter if (base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match)) { - var matchKey = new FontCollectionKey { Style = match.Style, Weight = match.Weight, Stretch = match.Stretch }; + var matchKey = match.ToFontCollectionKey(); if (requestedKey == matchKey) { @@ -96,25 +96,43 @@ namespace Avalonia.Media.Fonts } } - if (_platformImpl is IFontManagerImpl2 fontManagerImpl2) + if (_platformImpl.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out var platformTypeface)) { - if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out var glyphTypeface)) + // Construct the resulting Typeface + match = new Typeface(platformTypeface.FamilyName, platformTypeface.Style, platformTypeface.Weight, + platformTypeface.Stretch); + + // Compute the key for cache lookup this can be different from the requested key + var key = match.ToFontCollectionKey(); + + // Check cache first: if an entry exists and is non-null, match succeeded and we can return true. + if (_glyphTypefaceCache.TryGetValue(platformTypeface.FamilyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out var existing)) { - match = new Typeface(glyphTypeface.FamilyName, glyphTypeface.Style, glyphTypeface.Weight, - glyphTypeface.Stretch); + return existing != null; + } - // Add to cache if not already present - TryAddGlyphTypeface(glyphTypeface); + // Not in cache yet: create glyph typeface and try to add it. + var glyphTypeface = new GlyphTypeface(platformTypeface); + // Try adding with the platform typeface family name first. + TryAddGlyphTypeface(platformTypeface.FamilyName, key, glyphTypeface); + + // Try adding the glyph typeface with the matched key. + if (TryAddGlyphTypeface(glyphTypeface, key)) + { return true; } + // TryAddGlyphTypeface failed: another thread may have added an entry. Re-check the cache. + if (_glyphTypefaceCache.TryGetValue(platformTypeface.FamilyName, out glyphTypefaces) && glyphTypefaces.TryGetValue(key, out existing)) + { + return existing != null; + } + return false; } - else - { - return _platformImpl.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match); - } + + return false; } } } diff --git a/src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs b/src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs index bca46b7e8c..58d162847f 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); + + short value = BinaryPrimitives.ReadInt16BigEndian(_span.Slice(_position, 2)); + + _position += 2; - return BinaryPrimitives.ReadInt16BigEndian(_buffer); + return value; } public TEnum ReadInt16() where TEnum : struct, Enum { TryConvert(ReadUInt16(), out TEnum value); + return value; } @@ -112,77 +100,75 @@ namespace Avalonia.Media.Fonts.Tables 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; + } + + public FontVersion ReadVersion16Dot16() + { + EnsureAvailable(4); + + uint value = BinaryPrimitives.ReadUInt32BigEndian(_span.Slice(_position, 4)); + + _position += 4; + + return new FontVersion(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); + + long value = BinaryPrimitives.ReadInt64BigEndian(_span.Slice(_position, 8)); + + _position += 8; - return BinaryPrimitives.ReadInt64BigEndian(_buffer); + 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); - return BinaryPrimitives.ReadUInt16BigEndian(_buffer); + ushort value = BinaryPrimitives.ReadUInt16BigEndian(_span.Slice(_position, 2)); + + _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 +177,6 @@ namespace Avalonia.Media.Fonts.Tables 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 +185,10 @@ namespace Avalonia.Media.Fonts.Tables } } - /// - /// 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 +201,15 @@ namespace Avalonia.Media.Fonts.Tables { 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 +218,6 @@ namespace Avalonia.Media.Fonts.Tables 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 +226,66 @@ namespace Avalonia.Media.Fonts.Tables } } - /// - /// 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)); - return BinaryPrimitives.ReadUInt32BigEndian(_buffer); + _position += 4; + + 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); + + string result = encoding.GetString(_span.Slice(_position, bytesToRead)); + + _position += bytesToRead; + + return result; } - /// - /// Reads the uint32 string. - /// - /// a 4 character long UTF8 encoded string. public string ReadTag() { - ReadInternal(_buffer, 4); + EnsureAvailable(4); - return Encoding.UTF8.GetString(_buffer, 0, 4); + string tag = Encoding.UTF8.GetString(_span.Slice(_position, 4)); + + _position += 4; + + return tag; } - /// - /// 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 +296,20 @@ namespace Avalonia.Media.Fonts.Tables _ => 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/CharacterToGlyphMap.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs new file mode 100644 index 0000000000..9a461afb0b --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Avalonia.Media.Fonts.Tables.Cmap +{ + /// + /// Provides a read-only mapping from Unicode code points to glyph identifiers for a font's character map (cmap) + /// table. + /// + /// This struct enables efficient lookup of glyph IDs corresponding to Unicode code points, + /// supporting both Format 4 (BMP) and Format 12 (Unicode full repertoire) cmap subtables. + /// +#pragma warning disable CA1815 // Override equals not needed for readonly struct + public readonly struct CharacterToGlyphMap +#pragma warning restore CA1815 // Override equals not needed for readonly struct + { + private readonly CmapFormat _format; + private readonly CmapFormat4Table? _format4; + private readonly CmapFormat12Table? _format12; + + /// + /// Initializes a new instance of the CharacterToGlyphMap class using the specified Format 4 cmap table. + /// + /// The Format 4 cmap table that provides character-to-glyph mapping data. Cannot be null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal CharacterToGlyphMap(CmapFormat4Table table) + { + _format = CmapFormat.Format4; + _format4 = table; + _format12 = null; + } + + /// + /// Initializes a new instance of the CharacterToGlyphMap class using the specified Format 12 character-to-glyph + /// mapping table. + /// + /// The Format 12 cmap table that defines the mapping from Unicode code points to glyph indices. Cannot be null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal CharacterToGlyphMap(CmapFormat12Table table) + { + _format = CmapFormat.Format12; + _format12 = table; + _format4 = null; + } + + /// + /// Gets the glyph index associated with the specified Unicode code point. + /// + /// The Unicode code point for which to retrieve the glyph index. + /// The glyph index corresponding to the specified code point. + public ushort this[int codePoint] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetGlyph(codePoint); + } + + /// + /// Retrieves the glyph index that corresponds to the specified Unicode code point. + /// + /// The Unicode code point for which to obtain the glyph index. + /// The glyph index associated with the specified code point. Returns 0 if the code point is not mapped to any + /// glyph. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort GetGlyph(int codePoint) + { + return _format switch + { + CmapFormat.Format4 => _format4!.GetGlyph(codePoint), + CmapFormat.Format12 => _format12!.GetGlyph(codePoint), + _ => 0 + }; + } + + /// + /// Determines whether the character map contains a glyph for the specified Unicode code point. + /// + /// The Unicode code point to check for the presence of a corresponding glyph. + /// true if a glyph exists for the specified code point; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ContainsGlyph(int codePoint) + { + return _format switch + { + CmapFormat.Format4 => _format4!.ContainsGlyph(codePoint), + CmapFormat.Format12 => _format12!.ContainsGlyph(codePoint), + _ => false + }; + } + + /// + /// Maps a sequence of Unicode code points to their corresponding glyph IDs using the current character mapping + /// format. + /// + /// If the current character mapping format is not supported, all entries in are set to zero. The mapping is performed in place, and the method does not allocate + /// additional memory. + /// A read-only span of Unicode code points to be mapped to glyph IDs. + /// A span in which the resulting glyph IDs are written. Must be at least as long as . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GetGlyphs(ReadOnlySpan codePoints, Span glyphIds) + { + switch (_format) + { + case CmapFormat.Format4: + _format4!.GetGlyphs(codePoints, glyphIds); + return; + case CmapFormat.Format12: + _format12!.GetGlyphs(codePoints, glyphIds); + return; + default: + glyphIds.Clear(); + return; + } + } + + + /// + /// Attempts to retrieve the glyph identifier corresponding to the specified Unicode code point. + /// + /// The Unicode code point for which to obtain the glyph identifier. + /// When this method returns, contains the glyph identifier associated with the specified code point, if found; + /// otherwise, zero. This parameter is passed uninitialized. + /// true if a glyph identifier was found for the specified code point; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetGlyph(int codePoint, out ushort glyphId) + { + switch (_format) + { + case CmapFormat.Format4: return _format4!.TryGetGlyph(codePoint, out glyphId); + case CmapFormat.Format12: return _format12!.TryGetGlyph(codePoint, out glyphId); + default: glyphId = 0; return false; + } + } + + /// + /// Returns an enumerator that iterates through all code point ranges mapped by this instance. + /// + /// A that can be used to enumerate the mapped code point ranges. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CodepointRangeEnumerator GetMappedRanges() + { + return new CodepointRangeEnumerator(_format, _format4, _format12); + } + } +} 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 0000000000..8979e21d7a --- /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 0000000000..667498bd43 --- /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 0000000000..b4440e7884 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs @@ -0,0 +1,258 @@ +using System; +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Avalonia.Media.Fonts.Tables.Cmap +{ + internal sealed class CmapFormat12Table + { + private readonly ReadOnlyMemory _table; + private readonly int _groupCount; + private readonly ReadOnlyMemory _groups; + + /// + /// Gets the language code for the cmap subtable. + /// For non-language-specific tables, this value is 0. + /// + public uint Language { get; } + + 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); + + 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); + } + + /// + /// Retrieves the glyph index corresponding to the specified Unicode code point. + /// + /// The Unicode code point for which to obtain the glyph index. Must be a valid code point supported by the + /// font. + /// The glyph index as an unsigned 16-bit integer for the specified code point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort GetGlyph(int codePoint) => this[codePoint]; + + /// + /// Determines whether the specified Unicode code point is present in the glyph set. + /// + /// The Unicode code point to check for presence in the glyph set. Must be a valid integer representing a + /// Unicode character. + /// true if the glyph set contains the specified code point; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ContainsGlyph(int codePoint) + { + return FindGroupIndex(codePoint) >= 0; + } + + /// + /// Maps multiple Unicode code points to glyph indices in a single operation. + /// + /// Read-only span of code points to map. + /// Output span to write glyph IDs. Must be at least as long as . + /// + /// This method is significantly more efficient than calling the indexer multiple times as it: + /// - Reuses span references (no repeated memory access) + /// - Caches group data for sequential lookups + /// - Optimizes for locality of code points (common in text runs) + /// Format 12 is commonly used for fonts with large character sets (CJK, emoji, etc.) + /// This is the preferred method for batch character-to-glyph mapping in text shaping. + /// + public void GetGlyphs(ReadOnlySpan codePoints, Span glyphIds) + { + if (glyphIds.Length < codePoints.Length) + { + throw new ArgumentException("Output span must be at least as long as input span", nameof(glyphIds)); + } + + var groups = _groups.Span; + + // Track last group for locality optimization + int lastGroup = -1; + uint lastStart = 0; + uint lastEnd = 0; + uint lastStartGlyph = 0; + + for (int i = 0; i < codePoints.Length; i++) + { + int codePoint = codePoints[i]; + + // Optimization: check if codepoint is in the same group as previous + if (lastGroup >= 0 && codePoint >= lastStart && codePoint <= lastEnd) + { + glyphIds[i] = (ushort)(lastStartGlyph + (codePoint - lastStart)); + continue; + } + + // Binary search for group + int groupIndex = FindGroupIndexOptimized(codePoint, groups); + + if (groupIndex < 0) + { + glyphIds[i] = 0; + lastGroup = -1; + + continue; + } + + // Cache group data + lastGroup = groupIndex; + lastStart = ReadUInt32BE(groups, groupIndex, 0); + lastEnd = ReadUInt32BE(groups, groupIndex, 4); + lastStartGlyph = ReadUInt32BE(groups, groupIndex, 8); + + glyphIds[i] = (ushort)(lastStartGlyph + (codePoint - lastStart)); + } + } + + public bool TryGetGlyph(int codePoint, out ushort glyphId) + { + int groupIndex = FindGroupIndex(codePoint); + + if (groupIndex < 0) + { + glyphId = 0; + return false; + } + + var groups = _groups.Span; + + uint start = ReadUInt32BE(groups, groupIndex, 0); + uint startGlyph = ReadUInt32BE(groups, groupIndex, 8); + + glyphId = (ushort)(startGlyph + (codePoint - start)); + + return glyphId != 0; + } + + internal bool TryGetRange(int index, out CodepointRange range) + { + if ((uint)index >= (uint)_groupCount) + { + range = default; + + return false; + } + + var groups = _groups.Span; + + int start = (int)ReadUInt32BE(groups, index, 0); + int end = (int)ReadUInt32BE(groups, index, 4); + + range = new CodepointRange(start, end); + + return true; + } + + public ushort this[int codePoint] + { + get + { + int groupIndex = FindGroupIndex(codePoint); + + if (groupIndex < 0) + { + return 0; + } + + var groups = _groups.Span; + + uint start = ReadUInt32BE(groups, groupIndex, 0); + uint startGlyph = ReadUInt32BE(groups, groupIndex, 8); + + // Calculate glyph index + return (ushort)(startGlyph + (codePoint - start)); + } + } + + // Optimized binary search that works directly with cached span + private int FindGroupIndexOptimized(int codePoint, ReadOnlySpan groups) + { + int lo = 0; + int hi = _groupCount - 1; + + while (lo <= hi) + { + int mid = (lo + hi) >> 1; + uint start = ReadUInt32BE(groups, mid, 0); + uint end = ReadUInt32BE(groups, mid, 4); + + if (codePoint < start) + { + hi = mid - 1; + } + else if (codePoint > end) + { + lo = mid + 1; + } + else + { + return mid; + } + } + + return -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint ReadUInt32BE(ReadOnlySpan span, int groupIndex, int fieldOffset) + { + int byteIndex = groupIndex * 12 + fieldOffset; + + return BinaryPrimitives.ReadUInt32BigEndian(span.Slice(byteIndex, 4)); + } + + // Binary search to find the group containing the code point + private int FindGroupIndex(int codePoint) + { + int lo = 0; + int hi = _groupCount - 1; + + var groups = _groups.Span; + + while (lo <= hi) + { + int mid = (lo + hi) >> 1; + uint start = ReadUInt32BE(groups, mid, 0); + uint end = ReadUInt32BE(groups, mid, 4); + + if (codePoint < start) + { + hi = mid - 1; + } + else if (codePoint > end) + { + lo = mid + 1; + } + else + { + return mid; + } + } + + // Not found + return -1; + } + } +} 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 0000000000..7d1fded616 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat4Table.cs @@ -0,0 +1,454 @@ +using System; +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Avalonia.Media.Fonts.Tables.Cmap +{ + internal sealed class CmapFormat4Table + { + private readonly ReadOnlyMemory _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; + + /// + /// Gets the language code for the cmap subtable. + /// For non-language-specific tables, this value is 0. + /// + public ushort Language { get; } + + 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); + + Language = reader.ReadUInt16(); // language code, 0 for non-language-specific + + ushort segCountX2 = reader.ReadUInt16(); // 2 * segCount + _segCount = segCountX2 / 2; + + ushort searchRange = reader.ReadUInt16(); // searchRange = 2 * (2^floor(log2(segCount))) + ushort entrySelector = reader.ReadUInt16(); // entrySelector = log2(searchRange/2) + ushort rangeShift = reader.ReadUInt16(); // rangeShift = segCountX2 - searchRange + + // Spec sanity checks (warn in debug builds instead of asserting) +#if DEBUG + var expectedSearchRange = (ushort)(2 * (1 << (int)Math.Floor(Math.Log(_segCount, 2)))); + if (searchRange != expectedSearchRange) + { + Debug.WriteLine($"CMAP format 4: unexpected searchRange {searchRange}, expected {expectedSearchRange} for segCount {_segCount}."); + } + + var expectedEntrySelector = (ushort)Math.Floor(Math.Log(_segCount, 2)); + if (entrySelector != expectedEntrySelector) + { + Debug.WriteLine($"CMAP format 4: unexpected entrySelector {entrySelector}, expected {expectedEntrySelector} for segCount {_segCount}."); + } + + var expectedRangeShift = (ushort)(segCountX2 - searchRange); + if (rangeShift != expectedRangeShift) + { + Debug.WriteLine($"CMAP format 4: unexpected rangeShift {rangeShift}, expected {expectedRangeShift} for segCountX2 {segCountX2} and searchRange {searchRange}."); + } +#endif + + // Compute offsets + int endCodeOffset = reader.Position; + int startCodeOffset = endCodeOffset + _segCount * 2 + 2; // + reservedPad + int idDeltaOffset = startCodeOffset + _segCount * 2; // after startCodes + int idRangeOffsetOffset = idDeltaOffset + _segCount * 2; // after idDeltas + int glyphIdArrayOffset = idRangeOffsetOffset + _segCount * 2; // after idRangeOffsets + + // Ensure declared length is consistent + Debug.Assert(length >= glyphIdArrayOffset, + "Subtable length must be at least large enough to contain glyphIdArray."); + + // Slice directly + _endCodes = _table.Slice(endCodeOffset, _segCount * 2); + + _startCodes = _table.Slice(startCodeOffset, _segCount * 2); + + _idDeltas = _table.Slice(idDeltaOffset, _segCount * 2); + + _idRangeOffsets = _table.Slice(idRangeOffsetOffset, _segCount * 2); + + int glyphCount = (length - glyphIdArrayOffset) / 2; + + Debug.Assert(glyphCount >= 0, "GlyphIdArray length must not be negative."); + + _glyphIdArray = _table.Slice(glyphIdArrayOffset, glyphCount * 2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort GetGlyph(int codePoint) => this[codePoint]; + + public bool ContainsGlyph(int codePoint) + { + int seg = FindSegmentIndex(codePoint); + + if ((uint)seg >= (uint)_segCount) + { + return false; + } + + ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets.Span, seg); + ushort idDelta = ReadUInt16BE(_idDeltas.Span, seg); + + if (idRangeOffset == 0) + { + // Always maps to something (possibly .notdef via delta) + return ((codePoint + idDelta) & 0xFFFF) != 0; + } + + int start = ReadUInt16BE(_startCodes.Span, seg); + int ro = idRangeOffset >> 1; + int idx = (codePoint - start) + ro - (_segCount - seg); + + if ((uint)idx >= (uint)(_glyphIdArray.Length >> 1)) + { + return false; + } + + ushort glyphId = ReadUInt16BE(_glyphIdArray.Span, idx); + + return glyphId != 0; + } + + /// + /// Maps multiple Unicode code points to glyph indices in a single operation. + /// + /// Read-only span of code points to map. + /// Output span to write glyph IDs. Must be at least as long as . + /// + /// This method is significantly more efficient than calling the indexer multiple times as it: + /// - Reuses span references (no repeated .Span property access) + /// - Caches segment data for sequential lookups + /// - Optimizes for locality of code points (common in text runs) + /// This is the preferred method for batch character-to-glyph mapping in text shaping. + /// + public void GetGlyphs(ReadOnlySpan codePoints, Span glyphIds) + { + if (glyphIds.Length < codePoints.Length) + { + throw new ArgumentException("Output span must be at least as long as input span", nameof(glyphIds)); + } + + // Cache all spans once + var startCodes = _startCodes.Span; + var endCodes = _endCodes.Span; + var idDeltas = _idDeltas.Span; + var idRangeOffsets = _idRangeOffsets.Span; + var glyphIdArray = _glyphIdArray.Span; + int glyphArrayWords = glyphIdArray.Length / 2; + + // Track last segment for locality optimization + int lastSegment = -1; + + for (int i = 0; i < codePoints.Length; i++) + { + int codePoint = codePoints[i]; + int segmentIndex; + + // Optimization: check if codepoint is in the same segment as previous + if (lastSegment >= 0 && lastSegment < _segCount) + { + int lastStart = ReadUInt16BE(startCodes, lastSegment); + int lastEnd = ReadUInt16BE(endCodes, lastSegment); + + if (codePoint >= lastStart && codePoint <= lastEnd) + { + segmentIndex = lastSegment; + goto MapGlyph; + } + } + + // Binary search for segment + segmentIndex = FindSegmentIndexOptimized(codePoint, startCodes, endCodes); + + if (segmentIndex < 0) + { + glyphIds[i] = 0; + continue; + } + + lastSegment = segmentIndex; + + MapGlyph: + ushort idRangeOffset = ReadUInt16BE(idRangeOffsets, segmentIndex); + ushort idDelta = ReadUInt16BE(idDeltas, segmentIndex); + + if (idRangeOffset == 0) + { + glyphIds[i] = (ushort)((codePoint + idDelta) & 0xFFFF); + } + else + { + int start = ReadUInt16BE(startCodes, segmentIndex); + int ro = idRangeOffset / 2; + int idx = (codePoint - start) + ro - (_segCount - segmentIndex); + + if ((uint)idx < (uint)glyphArrayWords) + { + ushort glyphId = ReadUInt16BE(glyphIdArray, idx); + + if (glyphId != 0) + { + glyphId = (ushort)((glyphId + idDelta) & 0xFFFF); + } + + glyphIds[i] = glyphId; + } + else + { + glyphIds[i] = 0; + } + } + } + } + + public bool TryGetGlyph(int codePoint, out ushort glyphId) + { + int seg = FindSegmentIndex(codePoint); + + if ((uint)seg >= (uint)_segCount) + { + glyphId = 0; + + return false; + } + + ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets.Span, seg); + ushort idDelta = ReadUInt16BE(_idDeltas.Span, seg); + + if (idRangeOffset == 0) + { + glyphId = (ushort)((codePoint + idDelta) & 0xFFFF); + + return glyphId != 0; + } + + int start = ReadUInt16BE(_startCodes.Span, seg); + int ro = idRangeOffset >> 1; + int idx = (codePoint - start) + ro - (_segCount - seg); + + if ((uint)idx >= (uint)(_glyphIdArray.Length >> 1)) + { + glyphId = 0; + + return false; + } + + glyphId = ReadUInt16BE(_glyphIdArray.Span, idx); + + if (glyphId != 0) + { + glyphId = (ushort)((glyphId + idDelta) & 0xFFFF); + } + + return glyphId != 0; + } + + internal bool TryGetRange(int index, out CodepointRange range) + { + if ((uint)index >= (uint)_segCount) + { + range = default; + return false; + } + + int start = ReadUInt16BE(_startCodes.Span, index); + int end = ReadUInt16BE(_endCodes.Span, index); + + // Skip sentinel segment (0xFFFF) + if (start == 0xFFFF && end == 0xFFFF) + { + range = default; + + return false; + } + + range = new CodepointRange(start, end); + + return true; + } + + public ushort this[int codePoint] + { + get + { + // Find the segment containing the codePoint + int segmentIndex = FindSegmentIndex(codePoint); + + if (segmentIndex < 0) + { + return 0; + } + + ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets.Span, segmentIndex); + ushort idDelta = ReadUInt16BE(_idDeltas.Span, segmentIndex); + + // If idRangeOffset is 0, glyphId = (codePoint + idDelta) % 65536 + if (idRangeOffset == 0) + { + return (ushort)((codePoint + idDelta) & 0xFFFF); + } + else + { + int start = ReadUInt16BE(_startCodes.Span, segmentIndex); + int ro = idRangeOffset / 2; // words + // The index into the glyphIdArray + int idx = (codePoint - start) + ro - (_segCount - segmentIndex); + + // Ensure index is within bounds of glyphIdArray + int glyphArrayWords = _glyphIdArray.Length / 2; + + if ((uint)idx < (uint)glyphArrayWords) + { + ushort glyphId = ReadUInt16BE(_glyphIdArray.Span, idx); + + // If glyphId is not 0, apply idDelta + if (glyphId != 0) + { + glyphId = (ushort)((glyphId + idDelta) & 0xFFFF); + } + + return glyphId; + } + } + + // Not found or maps to missing glyph + return 0; + } + } + + // Resolves the glyph ID for a given code point within a specific segment + private ushort ResolveGlyph(int segmentIndex, int codePoint) + { + ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets.Span, segmentIndex); + ushort idDelta = ReadUInt16BE(_idDeltas.Span, segmentIndex); + + if (idRangeOffset == 0) + { + return (ushort)((codePoint + idDelta) & 0xFFFF); + } + else + { + int start = ReadUInt16BE(_startCodes.Span, segmentIndex); + int ro = idRangeOffset / 2; // words + int idx = (codePoint - start) + ro - (_segCount - segmentIndex); + int glyphArrayWords = _glyphIdArray.Length / 2; + + if ((uint)idx < (uint)glyphArrayWords) + { + ushort glyphId = ReadUInt16BE(_glyphIdArray.Span, idx); + + if (glyphId != 0) + { + glyphId = (ushort)((glyphId + idDelta) & 0xFFFF); + } + + return glyphId; + } + } + + // Not found or maps to missing glyph + return 0; + } + + private int FindSegmentIndex(int codePoint) + { + int lo = 0; + int hi = _segCount - 1; + + var startCodes = _startCodes.Span; + var endCodes = _endCodes.Span; + + // Binary search over endCodes (sorted ascending) + while (lo <= hi) + { + int mid = (lo + hi) >> 1; + int end = ReadUInt16BE(endCodes, mid); + + if (codePoint > end) + { + lo = mid + 1; + } + else + { + hi = mid - 1; + } + } + + // lo is now the first segment whose endCode >= codePoint + if (lo < _segCount) + { + int start = ReadUInt16BE(startCodes, lo); + + if (codePoint >= start) + { + return lo; + } + } + + return -1; // not found + } + + // Optimized binary search that works directly with cached spans + private int FindSegmentIndexOptimized(int codePoint, ReadOnlySpan startCodes, ReadOnlySpan endCodes) + { + int lo = 0; + int hi = _segCount - 1; + + while (lo <= hi) + { + int mid = (lo + hi) >> 1; + int end = ReadUInt16BE(endCodes, mid); + + if (codePoint > end) + { + lo = mid + 1; + } + else + { + hi = mid - 1; + } + } + + if (lo < _segCount) + { + int start = ReadUInt16BE(startCodes, lo); + + if (codePoint >= start) + { + return lo; + } + } + + return -1; + } + + // Reads a big-endian UInt16 from the specified word index in the given memory + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort ReadUInt16BE(ReadOnlySpan span, int wordIndex) + { + int byteIndex = wordIndex * 2; + + // Ensure we don't go out of bounds + return BinaryPrimitives.ReadUInt16BigEndian(span.Slice(byteIndex, 2)); + } + } +} 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 0000000000..a05470a7c9 --- /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) + { + Platform = platform; + Encoding = encoding; + Offset = offset; + Format = format; + } + + /// + /// Gets the platform identifier for the current environment. + /// + public PlatformID Platform { get; } + + /// + /// Gets the character map (CMap) encoding associated with this instance. + /// + /// + public CmapEncoding Encoding { get; } + + /// + /// Gets the offset of the sub table. + /// + public int Offset { get; } + + /// + /// Gets the format of the character-to-glyph mapping (cmap) table. + /// + public CmapFormat Format { get; } + + 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 0000000000..f526658133 --- /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 CharacterToGlyphMap Load(GlyphTypeface glyphTypeface) + { + if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) + { + throw new InvalidOperationException("No cmap table found."); + } + + var reader = new BigEndianBinaryReader(table.Span); + + reader.ReadUInt16(); // version + + var numTables = reader.ReadUInt16(); + + var entries = new CmapSubtableEntry[numTables]; + + for (var i = 0; i < numTables; i++) + { + var platformID = (PlatformID)reader.ReadUInt16(); + var encodingID = (CmapEncoding)reader.ReadUInt16(); + var offset = (int)reader.ReadUInt32(); + + var position = reader.Position; + + reader.Seek(offset); + + var format = (CmapFormat)reader.ReadUInt16(); + + reader.Seek(position); + + var entry = new CmapSubtableEntry(platformID, encodingID, offset, format); + + entries[i] = entry; + } + + // Try to find the best Format 12 subtable entry + if (TryFindFormat12Entry(entries, out var format12Entry)) + { + // Prefer Format 12 if available + return new CharacterToGlyphMap(new CmapFormat12Table(format12Entry.GetSubtableMemory(table))); + } + + // Fallback to Format 4 + if (TryFindFormat4Entry(entries, out var format4Entry)) + { + return new CharacterToGlyphMap(new CmapFormat4Table(format4Entry.GetSubtableMemory(table))); + } + + throw new InvalidOperationException("No suitable cmap subtable found."); + + // Tries to find the best Format 12 subtable entry based on platform and encoding preferences + static bool TryFindFormat12Entry(CmapSubtableEntry[] entries, out CmapSubtableEntry result) + { + result = default; + var foundPlatformScore = int.MaxValue; + var foundEncodingScore = int.MaxValue; + + foreach (var entry in entries) + { + if (entry.Format != CmapFormat.Format12) + { + continue; + } + + var platformScore = entry.Platform switch + { + PlatformID.Unicode => 0, + PlatformID.Windows => 1, + _ => 2 + }; + + var encodingScore = 2; // Default: lowest preference + + switch (entry.Platform) + { + case PlatformID.Unicode when entry.Encoding == CmapEncoding.Unicode_2_0_full: + encodingScore = 0; // non-BMP preferred + break; + case PlatformID.Unicode when entry.Encoding == CmapEncoding.Unicode_2_0_BMP: + encodingScore = 1; // BMP + break; + case PlatformID.Windows when entry.Encoding == CmapEncoding.Microsoft_UCS4 && platformScore != 0: + encodingScore = 0; // non-BMP preferred + break; + case PlatformID.Windows when entry.Encoding == CmapEncoding.Microsoft_UnicodeBMP && platformScore != 0: + encodingScore = 1; // BMP + break; + } + + if (encodingScore < foundEncodingScore || encodingScore == foundEncodingScore && platformScore < foundPlatformScore) + { + result = entry; + foundEncodingScore = encodingScore; + foundPlatformScore = platformScore; + } + else + { + if (platformScore < foundPlatformScore) + { + result = entry; + foundEncodingScore = encodingScore; + foundPlatformScore = platformScore; + } + } + + if (foundPlatformScore == 0 && foundEncodingScore == 0) + { + break; // Best possible match found + } + } + + return result.Format != CmapFormat.Format0; + } + + // Tries to find the best Format 4 subtable entry based on platform preferences + static bool TryFindFormat4Entry(CmapSubtableEntry[] entries, out CmapSubtableEntry result) + { + result = default; + var foundPlatformScore = int.MaxValue; + + foreach (var entry in entries) + { + if (entry.Format != CmapFormat.Format4) + { + continue; + } + + var platformScore = entry.Platform switch + { + PlatformID.Unicode => 0, + PlatformID.Windows => 1, + _ => 2 + }; + + if (platformScore < foundPlatformScore) + { + result = entry; + foundPlatformScore = platformScore; + } + + if (foundPlatformScore == 0) + { + break; // Best possible match found + } + } + + return result.Format != CmapFormat.Format0; + } + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRange.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRange.cs new file mode 100644 index 0000000000..06381f1cfc --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRange.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Avalonia.Media.Fonts.Tables.Cmap +{ + /// + /// Represents a range of Unicode code points, defined by inclusive start and end values. + /// + public readonly struct CodepointRange + { + /// + /// Gets the start of the range. + /// + public readonly int Start; + + /// + /// Gets the end of the range. + /// + public readonly int End; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CodepointRange(int start, int end) + { + Start = start; + End = end; + } + + public override bool Equals(object? obj) + { + return obj is CodepointRange range && + Start == range.Start && + End == range.End; + } + + public override int GetHashCode() + { + return HashCode.Combine(Start, End); + } + + public static bool operator ==(CodepointRange left, CodepointRange right) + { + return left.Equals(right); + } + + public static bool operator !=(CodepointRange left, CodepointRange right) + { + return !(left == right); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs new file mode 100644 index 0000000000..b631c264d1 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs @@ -0,0 +1,71 @@ +using System.Runtime.CompilerServices; + +namespace Avalonia.Media.Fonts.Tables.Cmap +{ + /// + /// Enumerates contiguous ranges of Unicode code points present in a character map (cmap) table. + /// + /// This enumerator is typically used to iterate over all code point ranges defined by a cmap + /// table in an OpenType or TrueType font. It supports both Format 4 and Format 12 cmap subtables. The enumerator is + /// a ref struct and must be used within the stack context; it cannot be stored on the heap or used across await or + /// yield boundaries. + public ref struct CodepointRangeEnumerator + { + private readonly CmapFormat _format; + private readonly CmapFormat4Table? _f4; + private readonly CmapFormat12Table? _f12; + private int _index; + + internal CodepointRangeEnumerator(CmapFormat format, CmapFormat4Table? f4, CmapFormat12Table? f12) + { + _format = format; + _f4 = f4; + _f12 = f12; + _index = -1; + } + + /// + /// Gets the current code point range in the enumeration sequence. + /// + public CodepointRange Current { get; private set; } + + /// + /// Advances the enumerator to the next character mapping range in the collection. + /// + /// After calling MoveNext, check the Current property to access the current character + /// mapping range. If the end of the collection is reached, MoveNext returns false and Current is set to its + /// default value. + /// true if the enumerator was successfully advanced to the next range; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + _index++; + + switch (_format) + { + case CmapFormat.Format4: + { + var result = _f4!.TryGetRange(_index, out var range); + + Current = range; + + return result; + } + case CmapFormat.Format12: + { + var result = _f12!.TryGetRange(_index, out var range); + + Current = range; + + return result; + } + default: + { + Current = default; + + return false; + } + } + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs index 0a916c7ed0..23cf0144b3 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs @@ -2,8 +2,8 @@ // Licensed under the Apache License, Version 2.0. // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts +using System; using System.Collections.Generic; -using System.IO; namespace Avalonia.Media.Fonts.Tables { @@ -17,8 +17,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) { @@ -27,34 +27,31 @@ namespace Avalonia.Media.Fonts.Tables public IReadOnlyList Features { get; } - public static FeatureListTable? LoadGSub(IGlyphTypeface glyphTypeface) + public static FeatureListTable? LoadGSub(GlyphTypeface glyphTypeface) { - if (!glyphTypeface.TryGetTable(GSubTag, out var gPosTable)) + if (!glyphTypeface.PlatformTypeface.TryGetTable(GSubTag, out var gPosTable)) { return null; } - using var stream = new MemoryStream(gPosTable); - using var reader = new BigEndianBinaryReader(stream, false); - - return Load(reader); + var reader = new BigEndianBinaryReader(gPosTable.Span); + return Load(ref reader); } - public static FeatureListTable? LoadGPos(IGlyphTypeface glyphTypeface) + + public static FeatureListTable? LoadGPos(GlyphTypeface glyphTypeface) { - if (!glyphTypeface.TryGetTable(GPosTag, out var gSubTable)) + if (!glyphTypeface.PlatformTypeface.TryGetTable(GPosTag, out var gSubTable)) { return null; } - using var stream = new MemoryStream(gSubTable); - using var reader = new BigEndianBinaryReader(stream, false); - - return Load(reader); + var reader = new BigEndianBinaryReader(gSubTable.Span); + return Load(ref reader); } - private static FeatureListTable Load(BigEndianBinaryReader reader) + private static FeatureListTable Load(ref BigEndianBinaryReader reader) { // GPOS/GSUB Header, Version 1.0 // +----------+-------------------+-----------------------------------------------------------+ @@ -73,14 +70,14 @@ namespace Avalonia.Media.Fonts.Tables reader.ReadUInt16(); reader.ReadUInt16(); - reader.ReadOffset16(); + var featureListOffset = reader.ReadOffset16(); - return Load(reader, featureListOffset); + return Load(ref reader, featureListOffset); } - private static FeatureListTable Load(BigEndianBinaryReader reader, long offset) + private static FeatureListTable Load(ref BigEndianBinaryReader reader, int offset) { // FeatureList // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+ @@ -90,11 +87,21 @@ namespace Avalonia.Media.Fonts.Tables // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+ // | FeatureRecord | featureRecords[featureCount] | Array of FeatureRecords — zero-based (first feature has FeatureIndex = 0), listed alphabetically by feature tag | // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+ - reader.Seek(offset, SeekOrigin.Begin); + reader.Seek(offset); var featureCount = reader.ReadUInt16(); - var features = new List(featureCount); + if (featureCount == 0) + { + return new FeatureListTable(Array.Empty()); + } + + // Use stackalloc for small counts, array for larger + Span tempFeatures = featureCount <= 64 + ? stackalloc OpenTypeTag[featureCount] + : new OpenTypeTag[featureCount]; + + int uniqueCount = 0; for (var i = 0; i < featureCount; i++) { @@ -107,19 +114,24 @@ namespace Avalonia.Media.Fonts.Tables // | Offset16 | featureOffset | Offset to Feature table, from beginning of FeatureList | // +----------+---------------+--------------------------------------------------------+ var featureTag = reader.ReadUInt32(); - reader.ReadOffset16(); var tag = new OpenTypeTag(featureTag); - if (!features.Contains(tag)) + // Check for duplicates in already added features + bool isDuplicate = tempFeatures.Contains(tag); + + if (!isDuplicate) { - features.Add(tag); + tempFeatures[uniqueCount++] = tag; } } - return new FeatureListTable(features /*featureTables*/); - } + // Create array with only unique features + var features = new OpenTypeTag[uniqueCount]; + tempFeatures.Slice(0, uniqueCount).CopyTo(features); + return new FeatureListTable(features); + } } } diff --git a/src/Avalonia.Base/Media/Fonts/Tables/FontVersion.cs b/src/Avalonia.Base/Media/Fonts/Tables/FontVersion.cs new file mode 100644 index 0000000000..23cc3674ae --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/FontVersion.cs @@ -0,0 +1,75 @@ +using System.Diagnostics; + +namespace Avalonia.Media.Fonts.Tables +{ + /// + /// Represents a Version16Dot16 value from OpenType font tables. + /// The high 16 bits represent the major version, and the low 16 bits represent the minor version as a fraction. + /// + [DebuggerDisplay("{ToString(),nq}")] + internal readonly struct FontVersion + { + /// + /// Gets the major version number. + /// + public ushort Major { get; } + + /// + /// Gets the minor version number (as a fraction of 65536). + /// + public ushort Minor { get; } + + /// + /// Initializes a new instance of the struct from raw Version16Dot16 value. + /// + /// The 32-bit Version16Dot16 value. + public FontVersion(uint value) + { + Major = (ushort)(value >> 16); + Minor = (ushort)(value & 0xFFFF); + } + + /// + /// Initializes a new instance of the struct from major and minor components. + /// + /// The major version number. + /// The minor version number (as a fraction of 65536). + public FontVersion(ushort major, ushort minor) + { + Major = major; + Minor = minor; + } + + /// + /// Converts the version to a floating-point representation. + /// + public float ToFloat() => Major + (Minor / 65536f); + + /// + /// Returns the raw 32-bit Version16Dot16 value. + /// + public uint ToUInt32() => ((uint)Major << 16) | Minor; + + public override string ToString() + { + // For common fractional values, show them nicely (e.g., 2.5 instead of 2.5000076) + if (Minor == 0) + return Major.ToString(); + if (Minor == 0x8000) // 0.5 + return $"{Major}.5"; + + return ToFloat().ToString("F6").TrimEnd('0').TrimEnd('.'); + } + + public static implicit operator float(FontVersion version) => version.ToFloat(); + + public static bool operator ==(FontVersion left, FontVersion right) => + left.Major == right.Major && left.Minor == right.Minor; + + public static bool operator !=(FontVersion left, FontVersion right) => !(left == right); + + public override bool Equals(object? obj) => obj is FontVersion other && this == other; + + public override int GetHashCode() => ((int)Major << 16) | Minor; + } +} 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 0000000000..01f6831205 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/HeadTable.cs @@ -0,0 +1,322 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Avalonia.Media.Fonts.Tables +{ + internal sealed class HeadTable + { + internal const string TableName = "head"; + internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); + + private static readonly DateTime s_fontEpoch = new DateTime(1904, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + public FontVersion Version { get; } + public FontVersion FontRevision { get; } + public uint CheckSumAdjustment { get; } + public uint MagicNumber { get; } + public HeadFlags Flags { get; } + public ushort UnitsPerEm { get; } + public DateTime Created { get; } + public DateTime Modified { get; } + public short XMin { get; } + public short YMin { get; } + public short XMax { get; } + public short YMax { get; } + public MacStyleFlags MacStyle { get; } + public ushort LowestRecPPEM { get; } + public FontDirectionHint FontDirectionHint { get; } + public IndexToLocFormat IndexToLocFormat { get; } + public GlyphDataFormat GlyphDataFormat { get; } + + private HeadTable( + FontVersion version, + FontVersion fontRevision, + uint checkSumAdjustment, + uint magicNumber, + HeadFlags flags, + ushort unitsPerEm, + DateTime created, + DateTime modified, + short xMin, + short yMin, + short xMax, + short yMax, + MacStyleFlags macStyle, + ushort lowestRecPPEM, + FontDirectionHint fontDirectionHint, + IndexToLocFormat indexToLocFormat, + GlyphDataFormat glyphDataFormat) + { + Version = version; + FontRevision = fontRevision; + CheckSumAdjustment = checkSumAdjustment; + MagicNumber = magicNumber; + Flags = flags; + UnitsPerEm = unitsPerEm; + Created = created; + Modified = modified; + XMin = xMin; + YMin = yMin; + XMax = xMax; + YMax = yMax; + MacStyle = macStyle; + LowestRecPPEM = lowestRecPPEM; + FontDirectionHint = fontDirectionHint; + IndexToLocFormat = indexToLocFormat; + GlyphDataFormat = glyphDataFormat; + } + + public static bool TryLoad(GlyphTypeface glyphTypeface, [NotNullWhen(true)] out HeadTable? headTable) + { + headTable = null; + + if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) + { + return false; + } + + var reader = new BigEndianBinaryReader(table.Span); + headTable = Load(ref reader); + + return true; + } + + private static HeadTable Load(ref BigEndianBinaryReader reader) + { + FontVersion version = reader.ReadVersion16Dot16(); + FontVersion fontRevision = reader.ReadVersion16Dot16(); + uint checkSumAdjustment = reader.ReadUInt32(); + uint magicNumber = reader.ReadUInt32(); + HeadFlags flags = (HeadFlags)reader.ReadUInt16(); + ushort unitsPerEm = reader.ReadUInt16(); + long createdRaw = reader.ReadInt64(); + long modifiedRaw = reader.ReadInt64(); + short xMin = reader.ReadInt16(); + short yMin = reader.ReadInt16(); + short xMax = reader.ReadInt16(); + short yMax = reader.ReadInt16(); + MacStyleFlags macStyle = (MacStyleFlags)reader.ReadUInt16(); + ushort lowestRecPPEM = reader.ReadUInt16(); + FontDirectionHint fontDirectionHint = (FontDirectionHint)reader.ReadInt16(); + IndexToLocFormat indexToLocFormat = (IndexToLocFormat)reader.ReadInt16(); + GlyphDataFormat glyphDataFormat = (GlyphDataFormat)reader.ReadInt16(); + + DateTime created = SafeAddSeconds(s_fontEpoch, createdRaw); + DateTime modified = SafeAddSeconds(s_fontEpoch, modifiedRaw); + + return new HeadTable( + version, + fontRevision, + checkSumAdjustment, + magicNumber, + flags, + unitsPerEm, + created, + modified, + xMin, + yMin, + xMax, + yMax, + macStyle, + lowestRecPPEM, + fontDirectionHint, + indexToLocFormat, + glyphDataFormat); + } + + private static DateTime SafeAddSeconds(DateTime epoch, long seconds) + { + // Handle invalid/corrupted timestamps gracefully + // Valid range for font timestamps is roughly 1904-01-01 to ~2040 + // Negative values or extremely large values indicate corrupted data + + try + { + // Check if the resulting date would be valid before attempting addition + // DateTime.MinValue is 0001-01-01, DateTime.MaxValue is 9999-12-31 + if (seconds < 0) + { + // Calculate minimum allowed seconds from epoch to DateTime.MinValue + var minSeconds = (long)(DateTime.MinValue - epoch).TotalSeconds; + if (seconds < minSeconds) + { + return DateTime.MinValue; + } + } + else + { + // Calculate maximum allowed seconds from epoch to DateTime.MaxValue + var maxSeconds = (long)(DateTime.MaxValue - epoch).TotalSeconds; + if (seconds > maxSeconds) + { + return DateTime.MaxValue; + } + } + + return epoch.AddSeconds(seconds); + } + catch (ArgumentOutOfRangeException) + { + // Fallback for any edge cases that slip through + return seconds < 0 ? DateTime.MinValue : DateTime.MaxValue; + } + } + } + + /// + /// Flags for the 'head' table. + /// + [Flags] + internal enum HeadFlags : ushort + { + /// + /// Bit 0: Baseline for font at y=0. + /// + BaselineAtY0 = 1 << 0, + + /// + /// Bit 1: Left sidebearing point at x=0 (relevant only for TrueType rasterizers). + /// + LeftSidebearingAtX0 = 1 << 1, + + /// + /// Bit 2: Instructions may depend on point size. + /// + InstructionsDependOnPointSize = 1 << 2, + + /// + /// Bit 3: Force ppem to integer values for all internal scaler math; may use fractional ppem sizes if this bit is clear. + /// + ForcePpemToInteger = 1 << 3, + + /// + /// Bit 4: Instructions may alter advance width (the advance widths might not scale linearly). + /// + InstructionsMayAlterAdvanceWidth = 1 << 4, + + /// + /// Bit 5: This bit should be set in fonts that are intended to be laid out vertically, and in which the glyphs have been drawn such that an x-coordinate of 0 corresponds to the desired vertical baseline. + /// + VerticalBaseline = 1 << 5, + + /// + /// Bit 7: Font data is 'lossless' as a result of having been subjected to optimizing transformation and/or compression. + /// + Lossless = 1 << 7, + + /// + /// Bit 8: Font converted (produce compatible metrics). + /// + FontConverted = 1 << 8, + + /// + /// Bit 9: Font optimized for ClearType. Note that this implies that instructions may alter advance widths (bit 4 should also be set). + /// + ClearTypeOptimized = 1 << 9, + + /// + /// Bit 10: Last Resort font. If set, indicates that the glyphs encoded in the 'cmap' subtables are simply generic symbolic representations of code point ranges and don't truly represent support for those code points. + /// + LastResortFont = 1 << 10, + } + + /// + /// Mac style flags for font styling (used by macOS). + /// + [Flags] + internal enum MacStyleFlags : ushort + { + /// + /// Bit 0: Bold (if set to 1). + /// + Bold = 1 << 0, + + /// + /// Bit 1: Italic (if set to 1). + /// + Italic = 1 << 1, + + /// + /// Bit 2: Underline (if set to 1). + /// + Underline = 1 << 2, + + /// + /// Bit 3: Outline (if set to 1). + /// + Outline = 1 << 3, + + /// + /// Bit 4: Shadow (if set to 1). + /// + Shadow = 1 << 4, + + /// + /// Bit 5: Condensed (if set to 1). + /// + Condensed = 1 << 5, + + /// + /// Bit 6: Extended (if set to 1). + /// + Extended = 1 << 6, + } + + /// + /// Specifies the format used for the 'loca' table. + /// + internal enum IndexToLocFormat : short + { + /// + /// Short offsets (Offset16). The actual local offset divided by 2 is stored. + /// + Short = 0, + + /// + /// Long offsets (Offset32). The actual local offset is stored. + /// + Long = 1 + } + + /// + /// Specifies the format of glyph data. + /// + internal enum GlyphDataFormat : short + { + /// + /// Current format (TrueType outlines). + /// + Current = 0 + } + + /// + /// Font direction hint for mixed directional text. + /// + internal enum FontDirectionHint : short + { + /// + /// Fully mixed directional glyphs. + /// + FullyMixed = 0, + + /// + /// Only strongly left to right glyphs. + /// + OnlyLeftToRight = 1, + + /// + /// Like 1 but also contains neutrals. + /// + LeftToRightWithNeutrals = 2, + + /// + /// Only strongly right to left glyphs. + /// + OnlyRightToLeft = -1, + + /// + /// Like -1 but also contains neutrals. + /// + RightToLeftWithNeutrals = -2 + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeaderTable.cs similarity index 74% rename from src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs rename to src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeaderTable.cs index 0942296536..9f6caf4955 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeaderTable.cs @@ -2,16 +2,79 @@ // 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 readonly struct 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); + + /// + /// Gets the version of the horizontal header table. + /// + public FontVersion Version { get; } + + /// + /// 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 HorizontalHeaderTable( + FontVersion version, short ascender, short descender, short lineGap, @@ -24,6 +87,7 @@ namespace Avalonia.Media.Fonts.Tables short caretOffset, ushort numberOfHMetrics) { + Version = version; Ascender = ascender; Descender = descender; LineGap = lineGap; @@ -37,48 +101,28 @@ namespace Avalonia.Media.Fonts.Tables NumberOfHMetrics = numberOfHMetrics; } - public ushort AdvanceWidthMax { get; } - - public short Ascender { get; } - - public short CaretOffset { get; } - - public short CaretSlopeRise { get; } - - public short CaretSlopeRun { get; } - - public short Descender { get; } - - public short LineGap { get; } - - public short MinLeftSideBearing { get; } - - public short MinRightSideBearing { get; } - - public ushort NumberOfHMetrics { get; } - - public short XMaxExtent { get; } - - public static HorizontalHeadTable? Load(IGlyphTypeface glyphTypeface) + public static bool TryLoad(GlyphTypeface fontFace, out HorizontalHeaderTable horizontalHeaderTable) { - if (!glyphTypeface.TryGetTable(Tag, out var table)) + horizontalHeaderTable = default; + + if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table)) { - return null; + return false; } - using var stream = new MemoryStream(table); - using var binaryReader = new BigEndianBinaryReader(stream, false); + var binaryReader = new BigEndianBinaryReader(table.Span); - // Move to start of table. - return Load(binaryReader); + return TryLoad(ref binaryReader, out horizontalHeaderTable); } - public static HorizontalHeadTable Load(BigEndianBinaryReader reader) + private static bool TryLoad(ref BigEndianBinaryReader reader, out HorizontalHeaderTable horizontalHeaderTable) { + horizontalHeaderTable = default; + // +--------+---------------------+---------------------------------------------------------------------------------+ // | Type | Name | Description | // +========+=====================+=================================================================================+ - // | Fixed | version | 0x00010000 (1.0) | + // | Version16Dot16 | version | 0x00010000 (1.0) | // +--------+---------------------+---------------------------------------------------------------------------------+ // | FWord | ascent | Distance from baseline of highest ascender | // +--------+---------------------+---------------------------------------------------------------------------------+ @@ -112,8 +156,7 @@ namespace Avalonia.Media.Fonts.Tables // +--------+---------------------+---------------------------------------------------------------------------------+ // | uint16 | numOfLongHorMetrics | number of advance widths in metrics table | // +--------+---------------------+---------------------------------------------------------------------------------+ - ushort majorVersion = reader.ReadUInt16(); - ushort minorVersion = reader.ReadUInt16(); + FontVersion version = reader.ReadVersion16Dot16(); short ascender = reader.ReadFWORD(); short descender = reader.ReadFWORD(); short lineGap = reader.ReadFWORD(); @@ -129,14 +172,16 @@ namespace Avalonia.Media.Fonts.Tables reader.ReadInt16(); // reserved reader.ReadInt16(); // reserved short metricDataFormat = reader.ReadInt16(); // 0 + if (metricDataFormat != 0) { - throw new InvalidFontTableException($"Expected metricDataFormat = 0 found {metricDataFormat}", TableName); + return false; } ushort numberOfHMetrics = reader.ReadUInt16(); - return new HorizontalHeadTable( + horizontalHeaderTable = new HorizontalHeaderTable( + version, ascender, descender, lineGap, @@ -148,6 +193,8 @@ namespace Avalonia.Media.Fonts.Tables caretSlopeRun, caretOffset, numberOfHMetrics); + + return true; } } } 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 0000000000..6d6cbb288e --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/MaxpTable.cs @@ -0,0 +1,138 @@ +using System; + +namespace Avalonia.Media.Fonts.Tables +{ + internal readonly struct MaxpTable + { + internal const string TableName = "maxp"; + internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); + + public FontVersion Version { get; } + public ushort NumGlyphs { get; } + public ushort MaxPoints { get; } + public ushort MaxContours { get; } + public ushort MaxCompositePoints { get; } + public ushort MaxCompositeContours { get; } + public ushort MaxZones { get; } + public ushort MaxTwilightPoints { get; } + public ushort MaxStorage { get; } + public ushort MaxFunctionDefs { get; } + public ushort MaxInstructionDefs { get; } + public ushort MaxStackElements { get; } + public ushort MaxSizeOfInstructions { get; } + public ushort MaxComponentElements { get; } + public ushort MaxComponentDepth { get; } + + private MaxpTable( + FontVersion version, + ushort numGlyphs, + ushort maxPoints, + ushort maxContours, + ushort maxCompositePoints, + ushort maxCompositeContours, + ushort maxZones, + ushort maxTwilightPoints, + ushort maxStorage, + ushort maxFunctionDefs, + ushort maxInstructionDefs, + ushort maxStackElements, + ushort maxSizeOfInstructions, + ushort maxComponentElements, + ushort maxComponentDepth) + { + Version = version; + NumGlyphs = numGlyphs; + MaxPoints = maxPoints; + MaxContours = maxContours; + MaxCompositePoints = maxCompositePoints; + MaxCompositeContours = maxCompositeContours; + MaxZones = maxZones; + MaxTwilightPoints = maxTwilightPoints; + MaxStorage = maxStorage; + MaxFunctionDefs = maxFunctionDefs; + MaxInstructionDefs = maxInstructionDefs; + MaxStackElements = maxStackElements; + MaxSizeOfInstructions = maxSizeOfInstructions; + MaxComponentElements = maxComponentElements; + MaxComponentDepth = maxComponentDepth; + } + + public static MaxpTable Load(GlyphTypeface fontFace) + { + if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table)) + { + throw new InvalidOperationException($"Could not load the '{TableName}' table."); + } + + var binaryReader = new BigEndianBinaryReader(table.Span); + + return Load(ref binaryReader); + } + + private static MaxpTable Load(ref BigEndianBinaryReader reader) + { + // Version 0.5 (CFF/CFF2 fonts): + // | Version16Dot16 | version | 0x00005000 for version 0.5 | + // | uint16 | numGlyphs | The number of glyphs in the font| + + // Version 1.0 (TrueType fonts): + // | Version16Dot16 | version | 0x00010000 for version 1.0 | + // | uint16 | numGlyphs | The number of glyphs in the font | + // | uint16 | maxPoints | Maximum points in a non-composite glyph | + // | uint16 | maxContours | Maximum contours in a non-composite glyph | + // | uint16 | maxCompositePoints | Maximum points in a composite glyph | + // | uint16 | maxCompositeContours | Maximum contours in a composite glyph | + // | uint16 | maxZones | 1 or 2; should be set to 2 in most cases | + // | uint16 | maxTwilightPoints | Maximum points used in Z0 | + // | uint16 | maxStorage | Number of Storage Area locations | + // | uint16 | maxFunctionDefs | Number of FDEFs | + // | uint16 | maxInstructionDefs | Number of IDEFs | + // | uint16 | maxStackElements | Maximum stack depth | + // | uint16 | maxSizeOfInstructions | Maximum byte count for glyph instructions | + // | uint16 | maxComponentElements | Maximum number of components at top level | + // | uint16 | maxComponentDepth | Maximum levels of recursion | + + FontVersion version = reader.ReadVersion16Dot16(); + ushort numGlyphs = reader.ReadUInt16(); + + if (version.Major < 1) + { + return new MaxpTable( + version, + numGlyphs, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } + + ushort maxPoints = reader.ReadUInt16(); + ushort maxContours = reader.ReadUInt16(); + ushort maxCompositePoints = reader.ReadUInt16(); + ushort maxCompositeContours = reader.ReadUInt16(); + ushort maxZones = reader.ReadUInt16(); + ushort maxTwilightPoints = reader.ReadUInt16(); + ushort maxStorage = reader.ReadUInt16(); + ushort maxFunctionDefs = reader.ReadUInt16(); + ushort maxInstructionDefs = reader.ReadUInt16(); + ushort maxStackElements = reader.ReadUInt16(); + ushort maxSizeOfInstructions = reader.ReadUInt16(); + ushort maxComponentElements = reader.ReadUInt16(); + ushort maxComponentDepth = reader.ReadUInt16(); + + return new MaxpTable( + version, + numGlyphs, + maxPoints, + maxContours, + maxCompositePoints, + maxCompositeContours, + maxZones, + maxTwilightPoints, + maxStorage, + maxFunctionDefs, + maxInstructionDefs, + maxStackElements, + maxSizeOfInstructions, + maxComponentElements, + maxComponentDepth); + } + } +} 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 0000000000..7de0a0a278 --- /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 0000000000..9154284a14 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Metrics/HorizontalMetricsTable.cs @@ -0,0 +1,227 @@ +using System; + +namespace Avalonia.Media.Fonts.Tables.Metrics +{ + internal class HorizontalMetricsTable + { + public const string TagName = "hmtx"; + public static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TagName); + + private readonly ReadOnlyMemory _data; + private readonly ushort _numOfHMetrics; + private readonly int _numGlyphs; + + private HorizontalMetricsTable(ReadOnlyMemory data, ushort numOfHMetrics, int numGlyphs) + { + _data = data; + _numOfHMetrics = numOfHMetrics; + _numGlyphs = numGlyphs; + } + + internal static HorizontalMetricsTable? Load(GlyphTypeface glyphTypeface, ushort numberOfHMetrics, int glyphCount) + { + if (glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) + { + return new HorizontalMetricsTable(table, numberOfHMetrics, glyphCount); + } + + return null; + } + + /// + /// Attempts to retrieve the horizontal glyph metrics for the specified glyph index. + /// + /// The index of the glyph for which to retrieve metrics. + /// When this method returns, contains the horizontal glyph metric if the glyph index is valid; otherwise, the default value. + /// true if the glyph index is valid and metrics were retrieved; otherwise, false. + public bool TryGetMetrics(ushort glyphIndex, out HorizontalGlyphMetric metric) + { + metric = default; + + if (glyphIndex >= _numGlyphs) + { + return false; + } + + var reader = new BigEndianBinaryReader(_data.Span); + + if (glyphIndex < _numOfHMetrics) + { + reader.Seek(glyphIndex * 4); + + ushort advanceWidth = reader.ReadUInt16(); + short leftSideBearing = reader.ReadInt16(); + + metric = new HorizontalGlyphMetric(advanceWidth, leftSideBearing); + } + else + { + reader.Seek((_numOfHMetrics - 1) * 4); + + ushort lastAdvanceWidth = reader.ReadUInt16(); + + int lsbIndex = glyphIndex - _numOfHMetrics; + int lsbOffset = _numOfHMetrics * 4 + lsbIndex * 2; + + reader.Seek(lsbOffset); + + short leftSideBearing = reader.ReadInt16(); + + metric = new HorizontalGlyphMetric(lastAdvanceWidth, leftSideBearing); + } + + return true; + } + + /// + /// Attempts to retrieve the advance width for a single glyph. + /// + /// Glyph index to query. + /// When this method returns, contains the advance width if the glyph index is valid; otherwise, zero. + /// true if the glyph index is valid and the advance was retrieved; otherwise, false. + public bool TryGetAdvance(ushort glyphIndex, out ushort advance) + { + advance = 0; + + if (glyphIndex >= _numGlyphs) + { + return false; + } + + var reader = new BigEndianBinaryReader(_data.Span); + + if (glyphIndex < _numOfHMetrics) + { + reader.Seek(glyphIndex * 4); + + advance = reader.ReadUInt16(); + } + else + { + reader.Seek((_numOfHMetrics - 1) * 4); + + advance = reader.ReadUInt16(); + } + + return true; + } + + /// + /// Attempts to retrieve advance widths for multiple glyphs in a single operation. + /// + /// Read-only span of glyph indices to query. + /// Output span to write the advance widths. Must be at least as long as . + /// true if all glyph indices are valid and advances were retrieved; otherwise, false. + /// + /// This method is more efficient than calling multiple times as it reuses + /// the same reader and span reference. If any glyph index is invalid, the method returns false + /// and the contents of are undefined. + /// + public bool TryGetAdvances(ReadOnlySpan glyphIndices, Span advances) + { + if (advances.Length < glyphIndices.Length) + { + return false; + } + + var data = _data.Span; + var reader = new BigEndianBinaryReader(data); + + // Cache the last advance width for glyphs beyond numOfHMetrics + ushort? lastAdvanceWidth = null; + + for (int i = 0; i < glyphIndices.Length; i++) + { + ushort glyphIndex = glyphIndices[i]; + + if (glyphIndex >= _numGlyphs) + { + return false; + } + + if (glyphIndex < _numOfHMetrics) + { + reader.Seek(glyphIndex * 4); + advances[i] = reader.ReadUInt16(); + } + else + { + // All glyphs beyond numOfHMetrics share the same advance width + if (!lastAdvanceWidth.HasValue) + { + reader.Seek((_numOfHMetrics - 1) * 4); + lastAdvanceWidth = reader.ReadUInt16(); + } + + advances[i] = lastAdvanceWidth.Value; + } + } + + return true; + } + + /// + /// Attempts to retrieve horizontal glyph metrics for multiple glyphs in a single operation. + /// + /// Read-only span of glyph indices to query. + /// Output span to write the metrics. Must be at least as long as . + /// true if all glyph indices are valid and metrics were retrieved; otherwise, false. + /// + /// This method is more efficient than calling multiple times as it reuses + /// the same reader and span reference. If any glyph index is invalid, the method returns false + /// and the contents of are undefined. + /// + public bool TryGetMetrics(ReadOnlySpan glyphIndices, Span metrics) + { + if (metrics.Length < glyphIndices.Length) + { + return false; + } + + var data = _data.Span; + var reader = new BigEndianBinaryReader(data); + + // Cache the last advance width for glyphs beyond numOfHMetrics + ushort? lastAdvanceWidth = null; + + for (int i = 0; i < glyphIndices.Length; i++) + { + ushort glyphIndex = glyphIndices[i]; + + if (glyphIndex >= _numGlyphs) + { + return false; + } + + if (glyphIndex < _numOfHMetrics) + { + reader.Seek(glyphIndex * 4); + + ushort advanceWidth = reader.ReadUInt16(); + short leftSideBearing = reader.ReadInt16(); + + metrics[i] = new HorizontalGlyphMetric(advanceWidth, leftSideBearing); + } + else + { + // All glyphs beyond numOfHMetrics share the same advance width + if (!lastAdvanceWidth.HasValue) + { + reader.Seek((_numOfHMetrics - 1) * 4); + lastAdvanceWidth = reader.ReadUInt16(); + } + + int lsbIndex = glyphIndex - _numOfHMetrics; + int lsbOffset = _numOfHMetrics * 4 + lsbIndex * 2; + + reader.Seek(lsbOffset); + short leftSideBearing = reader.ReadInt16(); + + metrics[i] = new HorizontalGlyphMetric(lastAdvanceWidth.Value, leftSideBearing); + } + } + + return true; + } + } +} 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 0000000000..ef252ad6eb --- /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 0000000000..c5fdda1a74 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Metrics/VerticalMetricsTable.cs @@ -0,0 +1,227 @@ +using System; + +namespace Avalonia.Media.Fonts.Tables.Metrics +{ + internal class VerticalMetricsTable + { + public const string TagName = "vmtx"; + public static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TagName); + + private readonly ReadOnlyMemory _data; + private readonly ushort _numOfVMetrics; + private readonly int _numGlyphs; + + private VerticalMetricsTable(ReadOnlyMemory data, ushort numOfVMetrics, int numGlyphs) + { + _data = data; + _numOfVMetrics = numOfVMetrics; + _numGlyphs = numGlyphs; + } + + public static VerticalMetricsTable? Load(GlyphTypeface glyphTypeface, ushort numberOfVMetrics, int glyphCount) + { + if (glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) + { + return new VerticalMetricsTable(table, numberOfVMetrics, glyphCount); + } + + return null; + } + + /// + /// Attempts to retrieve the vertical glyph metrics for the specified glyph index. + /// + /// The index of the glyph for which to retrieve metrics. + /// When this method returns, contains the vertical glyph metric if the glyph index is valid; otherwise, the default value. + /// true if the glyph index is valid and metrics were retrieved; otherwise, false. + public bool TryGetMetrics(ushort glyphIndex, out VerticalGlyphMetric metric) + { + metric = default; + + if (glyphIndex >= _numGlyphs) + { + return false; + } + + var reader = new BigEndianBinaryReader(_data.Span); + + if (glyphIndex < _numOfVMetrics) + { + reader.Seek(glyphIndex * 4); + + ushort advanceHeight = reader.ReadUInt16(); + short topSideBearing = reader.ReadInt16(); + + metric = new VerticalGlyphMetric(advanceHeight, topSideBearing); + } + else + { + reader.Seek((_numOfVMetrics - 1) * 4); + + ushort lastAdvanceHeight = reader.ReadUInt16(); + + int tsbIndex = glyphIndex - _numOfVMetrics; + int tsbOffset = _numOfVMetrics * 4 + tsbIndex * 2; + + reader.Seek(tsbOffset); + + short tsb = reader.ReadInt16(); + + metric = new VerticalGlyphMetric(lastAdvanceHeight, tsb); + } + + return true; + } + + /// + /// Attempts to retrieve the advance height for a single glyph. + /// + /// Glyph index to query. + /// When this method returns, contains the advance height if the glyph index is valid; otherwise, zero. + /// true if the glyph index is valid and the advance was retrieved; otherwise, false. + public bool TryGetAdvance(ushort glyphIndex, out ushort advance) + { + advance = 0; + + if (glyphIndex >= _numGlyphs) + { + return false; + } + + var reader = new BigEndianBinaryReader(_data.Span); + + if (glyphIndex < _numOfVMetrics) + { + reader.Seek(glyphIndex * 4); + + advance = reader.ReadUInt16(); + } + else + { + reader.Seek((_numOfVMetrics - 1) * 4); + + advance = reader.ReadUInt16(); + } + + return true; + } + + /// + /// Attempts to retrieve advance heights for multiple glyphs in a single operation. + /// + /// Read-only span of glyph indices to query. + /// Output span to write the advance heights. Must be at least as long as . + /// true if all glyph indices are valid and advances were retrieved; otherwise, false. + /// + /// This method is more efficient than calling multiple times as it reuses + /// the same reader and span reference. If any glyph index is invalid, the method returns false + /// and the contents of are undefined. + /// + public bool TryGetAdvances(ReadOnlySpan glyphIndices, Span advances) + { + if (advances.Length < glyphIndices.Length) + { + return false; + } + + var data = _data.Span; + var reader = new BigEndianBinaryReader(data); + + // Cache the last advance height for glyphs beyond numOfVMetrics + ushort? lastAdvanceHeight = null; + + for (int i = 0; i < glyphIndices.Length; i++) + { + ushort glyphIndex = glyphIndices[i]; + + if (glyphIndex >= _numGlyphs) + { + return false; + } + + if (glyphIndex < _numOfVMetrics) + { + reader.Seek(glyphIndex * 4); + advances[i] = reader.ReadUInt16(); + } + else + { + // All glyphs beyond numOfVMetrics share the same advance height + if (!lastAdvanceHeight.HasValue) + { + reader.Seek((_numOfVMetrics - 1) * 4); + lastAdvanceHeight = reader.ReadUInt16(); + } + + advances[i] = lastAdvanceHeight.Value; + } + } + + return true; + } + + /// + /// Attempts to retrieve vertical glyph metrics for multiple glyphs in a single operation. + /// + /// Read-only span of glyph indices to query. + /// Output span to write the metrics. Must be at least as long as . + /// true if all glyph indices are valid and metrics were retrieved; otherwise, false. + /// + /// This method is more efficient than calling multiple times as it reuses + /// the same reader and span reference. If any glyph index is invalid, the method returns false + /// and the contents of are undefined. + /// + public bool TryGetMetrics(ReadOnlySpan glyphIndices, Span metrics) + { + if (metrics.Length < glyphIndices.Length) + { + return false; + } + + var data = _data.Span; + var reader = new BigEndianBinaryReader(data); + + // Cache the last advance height for glyphs beyond numOfVMetrics + ushort? lastAdvanceHeight = null; + + for (int i = 0; i < glyphIndices.Length; i++) + { + ushort glyphIndex = glyphIndices[i]; + + if (glyphIndex >= _numGlyphs) + { + return false; + } + + if (glyphIndex < _numOfVMetrics) + { + reader.Seek(glyphIndex * 4); + + ushort advanceHeight = reader.ReadUInt16(); + short topSideBearing = reader.ReadInt16(); + + metrics[i] = new VerticalGlyphMetric(advanceHeight, topSideBearing); + } + else + { + // All glyphs beyond numOfVMetrics share the same advance height + if (!lastAdvanceHeight.HasValue) + { + reader.Seek((_numOfVMetrics - 1) * 4); + lastAdvanceHeight = reader.ReadUInt16(); + } + + int tsbIndex = glyphIndex - _numOfVMetrics; + int tsbOffset = _numOfVMetrics * 4 + tsbIndex * 2; + + reader.Seek(tsbOffset); + short topSideBearing = reader.ReadInt16(); + + metrics[i] = new VerticalGlyphMetric(lastAdvanceHeight.Value, topSideBearing); + } + } + + return true; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs index 7a7ad71995..794113674d 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs @@ -2,44 +2,55 @@ // 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 + internal readonly struct NameRecord { - private readonly string value; - - public NameRecord(PlatformIDs platform, ushort languageId, KnownNameIds nameId, string value) + private readonly ReadOnlyMemory _stringStorage; + + 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 GetValue() { - var platform = reader.ReadUInt16(); - var encodingId = reader.ReadUInt16(); - var encoding = encodingId.AsEncoding(); - var languageID = reader.ReadUInt16(); - var nameID = reader.ReadUInt16(); + if (Length == 0) + { + return string.Empty; + } - var stringReader = StringLoader.Create(reader, encoding); + var span = _stringStorage.Span.Slice(Offset, Length); - return new NameRecord(platform, languageID, nameID, string.Empty) - { - StringReader = stringReader - }; + return Encoding.GetString(span); } } } diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs index c0c1048e51..f58d6a2551 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 @@ -14,7 +13,11 @@ namespace Avalonia.Media.Fonts.Tables.Name internal const string TableName = "name"; internal static readonly OpenTypeTag Tag = OpenTypeTag.Parse(TableName); + private const ushort USEnglishLanguageId = 0x0409; + private readonly NameRecord[] _names; + private string? _cachedFamilyName; + private string? _cachedTypographicFamilyName; internal NameTable(NameRecord[] names) { @@ -46,7 +49,21 @@ namespace Avalonia.Media.Fonts.Tables.Name /// The name of the font. /// public string FontFamilyName(ushort culture) - => GetNameById(culture, KnownNameIds.FontFamilyName); + { + if (culture == USEnglishLanguageId && _cachedFamilyName is not null) + { + return _cachedFamilyName; + } + + var value = GetNameById(culture, KnownNameIds.FontFamilyName); + + if (culture == USEnglishLanguageId) + { + _cachedFamilyName = value; + } + + return value; + } /// /// Gets the name of the font. @@ -59,86 +76,79 @@ namespace Avalonia.Media.Fonts.Tables.Name public string GetNameById(ushort culture, KnownNameIds nameId) { + if (nameId == KnownNameIds.TypographicFamilyName && culture == USEnglishLanguageId && _cachedTypographicFamilyName is not null) + { + return _cachedTypographicFamilyName; + } + var languageId = culture; NameRecord? usaVersion = null; NameRecord? firstWindows = null; NameRecord? first = null; + foreach (var name in _names) { if (name.NameID == 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; - if (name.LanguageID == 0x0409) + if (name.LanguageID == USEnglishLanguageId) { - // Grab the us version as its on next best match. usaVersion ??= name; } if (name.LanguageID == languageId) { - // Return the most exact first. - return name.Value; + return name.GetValue(); } } } } - return usaVersion?.Value ?? - firstWindows?.Value ?? - first?.Value ?? - string.Empty; + var value = usaVersion?.GetValue() ?? + firstWindows?.GetValue() ?? + first?.GetValue() ?? + string.Empty; + + if (nameId == KnownNameIds.TypographicFamilyName && culture == USEnglishLanguageId) + { + _cachedTypographicFamilyName = value; + } + + return value; } public string GetNameById(ushort culture, ushort nameId) => GetNameById(culture, (KnownNameIds)nameId); - public static NameTable? Load(IGlyphTypeface glyphTypeface) + public static NameTable? Load(GlyphTypeface glyphTypeface) { - if (!glyphTypeface.TryGetTable(Tag, out var table)) + if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) { return null; } - using var stream = new MemoryStream(table); - using var binaryReader = new BigEndianBinaryReader(stream, false); - - // Move to start of table. - return Load(binaryReader); - } + var reader = new BigEndianBinaryReader(table.Span); - public static NameTable Load(BigEndianBinaryReader reader) - { - var strings = new List(); - var format = reader.ReadUInt16(); - var nameCount = reader.ReadUInt16(); - var stringOffset = reader.ReadUInt16(); + reader.ReadUInt16(); + var count = reader.ReadUInt16(); + var storageOffset = reader.ReadUInt16(); - var names = new NameRecord[nameCount]; + var names = new NameRecord[count]; - for (var i = 0; i < nameCount; i++) + for (var i = 0; i < count; i++) { - names[i] = NameRecord.Read(reader); - - var sr = names[i].StringReader; - - if (sr is not null) - { - strings.Add(sr); - } - } - - foreach (var readable in strings) - { - var readableStartOffset = stringOffset + readable.Offset; - - reader.Seek(readableStartOffset, SeekOrigin.Begin); - - readable.LoadValue(reader); + var platform = reader.ReadUInt16(); + 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 9dc41ef083..5f5dc6c025 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs @@ -3,338 +3,199 @@ // 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 readonly struct OS2Table { internal const string TableName = "OS/2"; - internal static OpenTypeTag Tag = OpenTypeTag.Parse(TableName); - - private readonly byte[] panose; - private readonly short capHeight; - private readonly short familyClass; - private readonly short heightX; - private readonly string tag; - private readonly ushort codePageRange1; - private readonly ushort codePageRange2; - private readonly uint unicodeRange1; - private readonly uint unicodeRange2; - private readonly uint unicodeRange3; - private readonly uint unicodeRange4; - private readonly ushort breakChar; - private readonly ushort defaultChar; - private readonly ushort firstCharIndex; - private readonly ushort lastCharIndex; - private readonly ushort lowerOpticalPointSize; - private readonly ushort maxContext; - private readonly ushort upperOpticalPointSize; - private readonly short averageCharWidth; + internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); - public OS2Table( - short averageCharWidth, + [Flags] + internal enum FontSelectionFlags : ushort + { + ITALIC = 1, + UNDERSCORE = 1 << 1, + NEGATIVE = 1 << 2, + OUTLINED = 1 << 3, + STRIKEOUT = 1 << 4, + BOLD = 1 << 5, + REGULAR = 1 << 6, + USE_TYPO_METRICS = 1 << 7, + WWS = 1 << 8, + OBLIQUE = 1 << 9, + } + + public ushort Version { get; } + public short XAvgCharWidth { get; } + public ushort WeightClass { get; } + public ushort WidthClass { get; } + public ushort FsType { get; } + public short YSubscriptXSize { get; } + public short YSubscriptYSize { get; } + public short YSubscriptXOffset { get; } + public short YSubscriptYOffset { get; } + public short YSuperscriptXSize { get; } + public short YSuperscriptYSize { get; } + public short YSuperscriptXOffset { get; } + public short YSuperscriptYOffset { get; } + public short StrikeoutSize { get; } + public short StrikeoutPosition { get; } + public short FamilyClass { get; } + public Panose Panose { get; } + public uint UnicodeRange1 { get; } + public uint UnicodeRange2 { get; } + public uint UnicodeRange3 { get; } + public uint UnicodeRange4 { get; } + public uint VendorId { get; } + public FontSelectionFlags Selection { get; } + public ushort FirstCharIndex { get; } + public ushort LastCharIndex { get; } + public short TypoAscender { get; } + public short TypoDescender { get; } + public short TypoLineGap { get; } + public ushort WinAscent { get; } + public ushort WinDescent { get; } + + public uint CodePageRange1 { get; } + public uint CodePageRange2 { get; } + + public short XHeight { get; } + public short CapHeight { get; } + public ushort DefaultChar { get; } + public ushort BreakChar { get; } + public ushort MaxContext { get; } + + public ushort LowerOpticalPointSize { get; } + public ushort UpperOpticalPointSize { get; } + + private OS2Table( + ushort version, + short xAvgCharWidth, ushort weightClass, ushort widthClass, - ushort styleType, - short subscriptXSize, - short subscriptYSize, - short subscriptXOffset, - short subscriptYOffset, - short superscriptXSize, - short superscriptYSize, - short superscriptXOffset, - short superscriptYOffset, + ushort fsType, + short ySubscriptXSize, + short ySubscriptYSize, + short ySubscriptXOffset, + short ySubscriptYOffset, + short ySuperscriptXSize, + short ySuperscriptYSize, + short ySuperscriptXOffset, + short ySuperscriptYOffset, short strikeoutSize, short strikeoutPosition, short familyClass, - byte[] panose, + Panose panose, uint unicodeRange1, uint unicodeRange2, uint unicodeRange3, uint unicodeRange4, - string tag, - FontStyleSelection fontStyle, + uint vendorId, + FontSelectionFlags selection, ushort firstCharIndex, ushort lastCharIndex, short typoAscender, short typoDescender, short typoLineGap, ushort winAscent, - ushort winDescent) + ushort winDescent, + uint codePageRange1, + uint codePageRange2, + short xHeight, + short capHeight, + ushort defaultChar, + ushort breakChar, + ushort maxContext, + ushort lowerOpticalPointSize, + ushort upperOpticalPointSize) { - this.averageCharWidth = averageCharWidth; + Version = version; + XAvgCharWidth = xAvgCharWidth; WeightClass = weightClass; WidthClass = widthClass; - StyleType = styleType; - SubscriptXSize = subscriptXSize; - SubscriptYSize = subscriptYSize; - SubscriptXOffset = subscriptXOffset; - SubscriptYOffset = subscriptYOffset; - SuperscriptXSize = superscriptXSize; - SuperscriptYSize = superscriptYSize; - SuperscriptXOffset = superscriptXOffset; - SuperscriptYOffset = superscriptYOffset; + FsType = fsType; + YSubscriptXSize = ySubscriptXSize; + YSubscriptYSize = ySubscriptYSize; + YSubscriptXOffset = ySubscriptXOffset; + YSubscriptYOffset = ySubscriptYOffset; + YSuperscriptXSize = ySuperscriptXSize; + YSuperscriptYSize = ySuperscriptYSize; + YSuperscriptXOffset = ySuperscriptXOffset; + YSuperscriptYOffset = ySuperscriptYOffset; StrikeoutSize = strikeoutSize; StrikeoutPosition = strikeoutPosition; - this.familyClass = familyClass; - this.panose = panose; - this.unicodeRange1 = unicodeRange1; - this.unicodeRange2 = unicodeRange2; - this.unicodeRange3 = unicodeRange3; - this.unicodeRange4 = unicodeRange4; - this.tag = tag; - FontStyle = fontStyle; - this.firstCharIndex = firstCharIndex; - this.lastCharIndex = lastCharIndex; + FamilyClass = familyClass; + Panose = panose; + UnicodeRange1 = unicodeRange1; + UnicodeRange2 = unicodeRange2; + UnicodeRange3 = unicodeRange3; + UnicodeRange4 = unicodeRange4; + VendorId = vendorId; + Selection = selection; + FirstCharIndex = firstCharIndex; + LastCharIndex = lastCharIndex; TypoAscender = typoAscender; TypoDescender = typoDescender; TypoLineGap = typoLineGap; WinAscent = winAscent; WinDescent = winDescent; + CodePageRange1 = codePageRange1; + CodePageRange2 = codePageRange2; + XHeight = xHeight; + CapHeight = capHeight; + DefaultChar = defaultChar; + BreakChar = breakChar; + MaxContext = maxContext; + LowerOpticalPointSize = lowerOpticalPointSize; + UpperOpticalPointSize = upperOpticalPointSize; } - public OS2Table( - OS2Table version0Table, - ushort codePageRange1, - ushort codePageRange2, - short heightX, - short capHeight, - ushort defaultChar, - ushort breakChar, - ushort maxContext) - : this( - version0Table.averageCharWidth, - version0Table.WeightClass, - version0Table.WidthClass, - version0Table.StyleType, - version0Table.SubscriptXSize, - version0Table.SubscriptYSize, - version0Table.SubscriptXOffset, - version0Table.SubscriptYOffset, - version0Table.SuperscriptXSize, - version0Table.SuperscriptYSize, - version0Table.SuperscriptXOffset, - version0Table.SuperscriptYOffset, - version0Table.StrikeoutSize, - version0Table.StrikeoutPosition, - version0Table.familyClass, - version0Table.panose, - version0Table.unicodeRange1, - version0Table.unicodeRange2, - version0Table.unicodeRange3, - version0Table.unicodeRange4, - version0Table.tag, - version0Table.FontStyle, - version0Table.firstCharIndex, - version0Table.lastCharIndex, - version0Table.TypoAscender, - version0Table.TypoDescender, - version0Table.TypoLineGap, - version0Table.WinAscent, - version0Table.WinDescent) + public static bool TryLoad(GlyphTypeface fontFace, out OS2Table os2Table) { - this.codePageRange1 = codePageRange1; - this.codePageRange2 = codePageRange2; - this.heightX = heightX; - this.capHeight = capHeight; - this.defaultChar = defaultChar; - this.breakChar = breakChar; - this.maxContext = maxContext; - } - - public OS2Table(OS2Table versionLessThan5Table, ushort lowerOpticalPointSize, ushort upperOpticalPointSize) - : this( - versionLessThan5Table, - versionLessThan5Table.codePageRange1, - versionLessThan5Table.codePageRange2, - versionLessThan5Table.heightX, - versionLessThan5Table.capHeight, - versionLessThan5Table.defaultChar, - versionLessThan5Table.breakChar, - versionLessThan5Table.maxContext) - { - this.lowerOpticalPointSize = lowerOpticalPointSize; - this.upperOpticalPointSize = upperOpticalPointSize; - } - - [Flags] - internal enum FontStyleSelection : ushort - { - /// - /// Font contains italic or oblique characters, otherwise they are upright. - /// - ITALIC = 1, - - /// - /// Characters are underscored. - /// - UNDERSCORE = 1 << 1, - - /// - /// Characters have their foreground and background reversed. - /// - NEGATIVE = 1 << 2, - - /// - /// characters, otherwise they are solid. - /// - OUTLINED = 1 << 3, - - /// - /// Characters are overstruck. - /// - STRIKEOUT = 1 << 4, - - /// - /// Characters are emboldened. - /// - BOLD = 1 << 5, - - /// - /// Characters are in the standard weight/style for the font. - /// - REGULAR = 1 << 6, - - /// - /// If set, it is strongly recommended to use OS/2.typoAscender - OS/2.typoDescender+ OS/2.typoLineGap as a value for default line spacing for this font. - /// - USE_TYPO_METRICS = 1 << 7, - - /// - /// The font has ‘name’ table strings consistent with a weight/width/slope family without requiring use of ‘name’ IDs 21 and 22. (Please see more detailed description below.) - /// - WWS = 1 << 8, - - /// - /// Font contains oblique characters. - /// - OBLIQUE = 1 << 9, - - // 10–15 Reserved; set to 0. - } - - public FontStyleSelection FontStyle { get; } - - public short TypoAscender { get; } - - public short TypoDescender { get; } - - public short TypoLineGap { get; } - - public ushort WinAscent { get; } - - public ushort WinDescent { get; } - - public short StrikeoutPosition { get; } - - public short StrikeoutSize { get; } - - public short SubscriptXOffset { get; } - - public short SubscriptXSize { get; } - - public short SubscriptYOffset { get; } - - public short SubscriptYSize { get; } - - public short SuperscriptXOffset { get; } - - public short SuperscriptXSize { get; } - - public short SuperscriptYOffset { get; } - - public short SuperscriptYSize { get; } - - public ushort StyleType { get; } - - public ushort WeightClass { get; } - - public ushort WidthClass { get; } - - public static OS2Table? Load(IGlyphTypeface glyphTypeface) - { - if (!glyphTypeface.TryGetTable(Tag, out var table)) + os2Table = default; + + if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table)) { - return null; + return false; } - using var stream = new MemoryStream(table); - using var binaryReader = new BigEndianBinaryReader(stream, false); + var binaryReader = new BigEndianBinaryReader(table.Span); + + os2Table = Load(ref binaryReader); - // Move to start of table. - return Load(binaryReader); + return true; } - public static OS2Table Load(BigEndianBinaryReader reader) + private static OS2Table Load(ref BigEndianBinaryReader reader) { - // Version 1.0 - // Type | Name | Comments - // -------|------------------------|----------------------- - // uint16 |version | 0x0005 - // int16 |xAvgCharWidth | - // uint16 |usWeightClass | - // uint16 |usWidthClass | - // uint16 |fsType | - // int16 |ySubscriptXSize | - // int16 |ySubscriptYSize | - // int16 |ySubscriptXOffset | - // int16 |ySubscriptYOffset | - // int16 |ySuperscriptXSize | - // int16 |ySuperscriptYSize | - // int16 |ySuperscriptXOffset | - // int16 |ySuperscriptYOffset | - // int16 |yStrikeoutSize | - // int16 |yStrikeoutPosition | - // int16 |sFamilyClass | - // uint8 |panose[10] | - // uint32 |ulUnicodeRange1 | Bits 0–31 - // uint32 |ulUnicodeRange2 | Bits 32–63 - // uint32 |ulUnicodeRange3 | Bits 64–95 - // uint32 |ulUnicodeRange4 | Bits 96–127 - // Tag |achVendID | - // uint16 |fsSelection | - // uint16 |usFirstCharIndex | - // uint16 |usLastCharIndex | - // int16 |sTypoAscender | - // int16 |sTypoDescender | - // int16 |sTypoLineGap | - // uint16 |usWinAscent | - // uint16 |usWinDescent | - // uint32 |ulCodePageRange1 | Bits 0–31 - // uint32 |ulCodePageRange2 | Bits 32–63 - // int16 |sxHeight | - // int16 |sCapHeight | - // uint16 |usDefaultChar | - // uint16 |usBreakChar | - // uint16 |usMaxContext | - // uint16 |usLowerOpticalPointSize | - // uint16 |usUpperOpticalPointSize | - ushort version = reader.ReadUInt16(); // assert 0x0005 - short averageCharWidth = reader.ReadInt16(); + ushort version = reader.ReadUInt16(); + short xAvgCharWidth = reader.ReadInt16(); ushort weightClass = reader.ReadUInt16(); ushort widthClass = reader.ReadUInt16(); - ushort styleType = reader.ReadUInt16(); - short subscriptXSize = reader.ReadInt16(); - short subscriptYSize = reader.ReadInt16(); - short subscriptXOffset = reader.ReadInt16(); - short subscriptYOffset = reader.ReadInt16(); - - short superscriptXSize = reader.ReadInt16(); - short superscriptYSize = reader.ReadInt16(); - short superscriptXOffset = reader.ReadInt16(); - short superscriptYOffset = reader.ReadInt16(); - + ushort fsType = reader.ReadUInt16(); + short ySubscriptXSize = reader.ReadInt16(); + short ySubscriptYSize = reader.ReadInt16(); + short ySubscriptXOffset = reader.ReadInt16(); + short ySubscriptYOffset = reader.ReadInt16(); + short ySuperscriptXSize = reader.ReadInt16(); + short ySuperscriptYSize = reader.ReadInt16(); + short ySuperscriptXOffset = reader.ReadInt16(); + short ySuperscriptYOffset = reader.ReadInt16(); short strikeoutSize = reader.ReadInt16(); short strikeoutPosition = reader.ReadInt16(); short familyClass = reader.ReadInt16(); - byte[] panose = reader.ReadUInt8Array(10); - uint unicodeRange1 = reader.ReadUInt32(); // Bits 0–31 - uint unicodeRange2 = reader.ReadUInt32(); // Bits 32–63 - uint unicodeRange3 = reader.ReadUInt32(); // Bits 64–95 - uint unicodeRange4 = reader.ReadUInt32(); // Bits 96–127 - string tag = reader.ReadTag(); - FontStyleSelection fontStyle = reader.ReadUInt16(); + + Panose panose = Panose.Load(ref reader); + + uint unicodeRange1 = reader.ReadUInt32(); + uint unicodeRange2 = reader.ReadUInt32(); + uint unicodeRange3 = reader.ReadUInt32(); + uint unicodeRange4 = reader.ReadUInt32(); + + uint vendorId = reader.ReadUInt32(); + + FontSelectionFlags selection = reader.ReadUInt16(); ushort firstCharIndex = reader.ReadUInt16(); ushort lastCharIndex = reader.ReadUInt16(); short typoAscender = reader.ReadInt16(); @@ -343,82 +204,75 @@ namespace Avalonia.Media.Fonts.Tables ushort winAscent = reader.ReadUInt16(); ushort winDescent = reader.ReadUInt16(); - var version0Table = new OS2Table( - averageCharWidth, - weightClass, - widthClass, - styleType, - subscriptXSize, - subscriptYSize, - subscriptXOffset, - subscriptYOffset, - superscriptXSize, - superscriptYSize, - superscriptXOffset, - superscriptYOffset, - strikeoutSize, - strikeoutPosition, - familyClass, - panose, - unicodeRange1, - unicodeRange2, - unicodeRange3, - unicodeRange4, - tag, - fontStyle, - firstCharIndex, - lastCharIndex, - typoAscender, - typoDescender, - typoLineGap, - winAscent, - winDescent); - - if (version == 0) - { - return version0Table; - } - - short heightX = 0; + uint codePageRange1 = 0; + uint codePageRange2 = 0; + short xHeight = 0; short capHeight = 0; - ushort defaultChar = 0; ushort breakChar = 0; ushort maxContext = 0; + ushort lowerOpticalPointSize = 0; + ushort upperOpticalPointSize = 0xFFFF; - ushort codePageRange1 = reader.ReadUInt16(); // Bits 0–31 - ushort codePageRange2 = reader.ReadUInt16(); // Bits 32–63 + if (version >= 1) + { + codePageRange1 = reader.ReadUInt32(); + codePageRange2 = reader.ReadUInt32(); + } - // fields exist only in > v1 https://docs.microsoft.com/en-us/typography/opentype/spec/os2 - if (version > 1) + if (version >= 2) { - heightX = reader.ReadInt16(); + xHeight = reader.ReadInt16(); capHeight = reader.ReadInt16(); defaultChar = reader.ReadUInt16(); breakChar = reader.ReadUInt16(); maxContext = reader.ReadUInt16(); } - var versionLessThan5Table = new OS2Table( - version0Table, - codePageRange1, - codePageRange2, - heightX, - capHeight, - defaultChar, - breakChar, - maxContext); - - if (version < 5) + if (version >= 5) { - return versionLessThan5Table; + lowerOpticalPointSize = reader.ReadUInt16(); + upperOpticalPointSize = reader.ReadUInt16(); } - ushort lowerOpticalPointSize = reader.ReadUInt16(); - ushort upperOpticalPointSize = reader.ReadUInt16(); - return new OS2Table( - versionLessThan5Table, + version, + xAvgCharWidth, + weightClass, + widthClass, + fsType, + ySubscriptXSize, + ySubscriptYSize, + ySubscriptXOffset, + ySubscriptYOffset, + ySuperscriptXSize, + ySuperscriptYSize, + ySuperscriptXOffset, + ySuperscriptYOffset, + strikeoutSize, + strikeoutPosition, + familyClass, + panose, + unicodeRange1, + unicodeRange2, + unicodeRange3, + unicodeRange4, + vendorId, + selection, + firstCharIndex, + lastCharIndex, + typoAscender, + typoDescender, + typoLineGap, + winAscent, + winDescent, + codePageRange1, + codePageRange2, + xHeight, + capHeight, + defaultChar, + breakChar, + maxContext, lowerOpticalPointSize, upperOpticalPointSize); } diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Panose.cs b/src/Avalonia.Base/Media/Fonts/Tables/Panose.cs new file mode 100644 index 0000000000..67d3c7a47d --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Panose.cs @@ -0,0 +1,248 @@ +namespace Avalonia.Media.Fonts.Tables +{ + /// + /// Represents the PANOSE classification for a font. + /// PANOSE is a font classification system that describes the visual characteristics of a typeface. + /// + /// + /// The interpretation of bytes 1-9 depends on the FamilyKind (byte 0). + /// This struct represents the Latin Text interpretation (FamilyKind = 2), which is the most common. + /// For other family kinds, access the raw bytes via the indexer. + /// + internal readonly struct Panose + { + private readonly byte[] _data; + + public Panose(byte b0, byte b1, byte b2, byte b3, byte b4, byte b5, byte b6, byte b7, byte b8, byte b9) + { + _data = new byte[10] { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9 }; + } + + public static Panose Load(ref BigEndianBinaryReader reader) + { + return new Panose( + reader.ReadByte(), + reader.ReadByte(), + reader.ReadByte(), + reader.ReadByte(), + reader.ReadByte(), + reader.ReadByte(), + reader.ReadByte(), + reader.ReadByte(), + reader.ReadByte(), + reader.ReadByte() + ); + } + + /// + /// Gets the family kind classification (byte 0). + /// + public PanoseFamilyKind FamilyKind => (PanoseFamilyKind)_data[0]; + + // Latin Text properties (when FamilyKind == LatinText) + + /// + /// Gets the serif style (byte 1) for Latin Text fonts. + /// + public PanoseSerifStyle SerifStyle => (PanoseSerifStyle)_data[1]; + + /// + /// Gets the weight (byte 2) for Latin Text fonts. + /// + public PanoseWeight Weight => (PanoseWeight)_data[2]; + + /// + /// Gets the proportion (byte 3) for Latin Text fonts. + /// + public PanoseProportion Proportion => (PanoseProportion)_data[3]; + + /// + /// Gets the contrast (byte 4) for Latin Text fonts. + /// + public PanoseContrast Contrast => (PanoseContrast)_data[4]; + + /// + /// Gets the stroke variation (byte 5) for Latin Text fonts. + /// + public PanoseStrokeVariation StrokeVariation => (PanoseStrokeVariation)_data[5]; + + /// + /// Gets the arm style (byte 6) for Latin Text fonts. + /// + public PanoseArmStyle ArmStyle => (PanoseArmStyle)_data[6]; + + /// + /// Gets the letterform (byte 7) for Latin Text fonts. + /// + public PanoseLetterform Letterform => (PanoseLetterform)_data[7]; + + /// + /// Gets the midline (byte 8) for Latin Text fonts. + /// + public PanoseMidline Midline => (PanoseMidline)_data[8]; + + /// + /// Gets the x-height (byte 9) for Latin Text fonts. + /// + public PanoseXHeight XHeight => (PanoseXHeight)_data[9]; + } + + internal enum PanoseFamilyKind : byte + { + Any = 0, + NoFit = 1, + LatinText = 2, + LatinHandWritten = 3, + LatinDecorative = 4, + LatinSymbol = 5 + } + + internal enum PanoseSerifStyle : byte + { + Any = 0, + NoFit = 1, + Cove = 2, + ObtuseCove = 3, + SquareCove = 4, + ObtuseSquareCove = 5, + Square = 6, + Thin = 7, + Oval = 8, + Exaggerated = 9, + Triangle = 10, + NormalSans = 11, + ObtuseSans = 12, + PerpendicularSans = 13, + Flared = 14, + Rounded = 15 + } + + internal enum PanoseWeight : byte + { + Any = 0, + NoFit = 1, + VeryLight = 2, + Light = 3, + Thin = 4, + Book = 5, + Medium = 6, + Demi = 7, + Bold = 8, + Heavy = 9, + Black = 10, + ExtraBlack = 11 + } + + internal enum PanoseProportion : byte + { + Any = 0, + NoFit = 1, + OldStyle = 2, + Modern = 3, + EvenWidth = 4, + Extended = 5, + Condensed = 6, + VeryExtended = 7, + VeryCondensed = 8, + Monospaced = 9 + } + + internal enum PanoseContrast : byte + { + Any = 0, + NoFit = 1, + None = 2, + VeryLow = 3, + Low = 4, + MediumLow = 5, + Medium = 6, + MediumHigh = 7, + High = 8, + VeryHigh = 9, + HorizontalLow = 10, + HorizontalMedium = 11, + HorizontalHigh = 12, + Broken = 13 + } + + internal enum PanoseStrokeVariation : byte + { + Any = 0, + NoFit = 1, + NoVariation = 2, + GradualDiagonal = 3, + GradualTransitional = 4, + GradualVertical = 5, + GradualHorizontal = 6, + RapidVertical = 7, + RapidHorizontal = 8, + InstantVertical = 9, + InstantHorizontal = 10 + } + + internal enum PanoseArmStyle : byte + { + Any = 0, + NoFit = 1, + StraightArmsHorizontal = 2, + StraightArmsWedge = 3, + StraightArmsVertical = 4, + StraightArmsSingleSerif = 5, + StraightArmsDoubleSerif = 6, + NonStraightArmsHorizontal = 7, + NonStraightArmsWedge = 8, + NonStraightArmsVertical = 9, + NonStraightArmsSingleSerif = 10, + NonStraightArmsDoubleSerif = 11 + } + + internal enum PanoseLetterform : byte + { + Any = 0, + NoFit = 1, + NormalContact = 2, + NormalWeighted = 3, + NormalBoxed = 4, + NormalFlattened = 5, + NormalRounded = 6, + NormalOffCenter = 7, + NormalSquare = 8, + ObliqueContact = 9, + ObliqueWeighted = 10, + ObliqueBoxed = 11, + ObliqueFlattened = 12, + ObliqueRounded = 13, + ObliqueOffCenter = 14, + ObliqueSquare = 15 + } + + internal enum PanoseMidline : byte + { + Any = 0, + NoFit = 1, + StandardTrimmed = 2, + StandardPointed = 3, + StandardSerifed = 4, + HighTrimmed = 5, + HighPointed = 6, + HighSerifed = 7, + ConstantTrimmed = 8, + ConstantPointed = 9, + ConstantSerifed = 10, + LowTrimmed = 11, + LowPointed = 12, + LowSerifed = 13 + } + + internal enum PanoseXHeight : byte + { + Any = 0, + NoFit = 1, + ConstantSmall = 2, + ConstantStandard = 3, + ConstantLarge = 4, + DuckingSmall = 5, + DuckingStandard = 6, + DuckingLarge = 7 + } +} 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 c57c4e2726..05c864b535 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 0000000000..a017faa6fc --- /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 FontVersion Version { get; } + public float ItalicAngle { get; } + public short UnderlinePosition { get; } + public short UnderlineThickness { get; } + public bool IsFixedPitch { get; } + + private PostTable(FontVersion version, float italicAngle, short underlinePosition, short underlineThickness, bool isFixedPitch) + { + Version = version; + ItalicAngle = italicAngle; + UnderlinePosition = underlinePosition; + UnderlineThickness = underlineThickness; + IsFixedPitch = isFixedPitch; + } + + public static PostTable Load(GlyphTypeface glyphTypeface) + { + if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) + { + return default; + } + + var binaryReader = new BigEndianBinaryReader(table.Span); + + return Load(ref binaryReader); + } + + private static PostTable Load(ref BigEndianBinaryReader reader) + { + FontVersion version = reader.ReadVersion16Dot16(); + float italicAngle = reader.ReadFixed(); + short underlinePosition = reader.ReadFWORD(); + short underlineThickness = reader.ReadFWORD(); + uint isFixedPitch = reader.ReadUInt32(); + + return new PostTable(version, italicAngle, underlinePosition, underlineThickness, isFixedPitch != 0); + } + } +} 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 a42c87b5bd..0000000000 --- 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 0000000000..81ef57d901 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/VerticalHeaderTable.cs @@ -0,0 +1,127 @@ +namespace Avalonia.Media.Fonts.Tables +{ + internal readonly struct VerticalHeaderTable + { + internal const string TableName = "vhea"; + internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); + + public FontVersion Version { get; } + public short Ascender { get; } + public short Descender { get; } + public short LineGap { get; } + public ushort AdvanceHeightMax { get; } + public short MinTopSideBearing { get; } + public short MinBottomSideBearing { get; } + public short YMaxExtent { get; } + public short CaretSlopeRise { get; } + public short CaretSlopeRun { get; } + public short CaretOffset { get; } + public ushort NumberOfVMetrics { get; } + + public VerticalHeaderTable( + FontVersion version, + short ascender, + short descender, + short lineGap, + ushort advanceHeightMax, + short minTopSideBearing, + short minBottomSideBearing, + short yMaxExtent, + short caretSlopeRise, + short caretSlopeRun, + short caretOffset, + ushort numberOfVMetrics) + { + Version = version; + Ascender = ascender; + Descender = descender; + LineGap = lineGap; + AdvanceHeightMax = advanceHeightMax; + MinTopSideBearing = minTopSideBearing; + MinBottomSideBearing = minBottomSideBearing; + YMaxExtent = yMaxExtent; + CaretSlopeRise = caretSlopeRise; + CaretSlopeRun = caretSlopeRun; + CaretOffset = caretOffset; + NumberOfVMetrics = numberOfVMetrics; + } + + public static bool TryLoad(GlyphTypeface fontFace, out VerticalHeaderTable verticalHeaderTable) + { + verticalHeaderTable = default; + + if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table)) + { + return false; + } + + var binaryReader = new BigEndianBinaryReader(table.Span); + + return TryLoad(ref binaryReader, out verticalHeaderTable); + } + + private static bool TryLoad(ref BigEndianBinaryReader reader, out VerticalHeaderTable verticalHeaderTable) + { + verticalHeaderTable = default; + + // See OpenType spec for vhea: + // | Version16Dot16 | version | 0x00010000 (1.0) or 0x00011000 (1.1) | + // | FWord | ascender | Distance from baseline of highest ascender (vertical) | + // | FWord | descender | Distance from baseline of lowest descender (vertical) | + // | FWord | lineGap | typographic line gap (vertical) | + // | uFWord | advanceHeightMax | must be consistent with vertical metrics | + // | FWord | minTopSideBearing | must be consistent with vertical metrics | + // | FWord | minBottomSideBearing| must be consistent with vertical metrics | + // | FWord | yMaxExtent | max(tsb + (yMax-yMin)) | + // | int16 | caretSlopeRise | used to calculate the slope of the caret (rise/run) set to 1 for vertical caret | + // | int16 | caretSlopeRun | 0 for vertical | + // | FWord | caretOffset | set value to 0 for non-slanted fonts | + // | int16 | reserved | set value to 0 | + // | int16 | reserved | set value to 0 | + // | int16 | reserved | set value to 0 | + // | int16 | reserved | set value to 0 | + // | int16 | metricDataFormat | 0 for current format | + // | uint16 | numOfLongVerMetrics | number of advance heights in vertical metrics table | + + FontVersion version = reader.ReadVersion16Dot16(); + short ascender = reader.ReadFWORD(); + short descender = reader.ReadFWORD(); + short lineGap = reader.ReadFWORD(); + ushort advanceHeightMax = reader.ReadUFWORD(); + short minTopSideBearing = reader.ReadFWORD(); + short minBottomSideBearing = reader.ReadFWORD(); + short yMaxExtent = reader.ReadFWORD(); + short caretSlopeRise = reader.ReadInt16(); + short caretSlopeRun = reader.ReadInt16(); + short caretOffset = reader.ReadInt16(); + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + short metricDataFormat = reader.ReadInt16(); // 0 + + if (metricDataFormat != 0) + { + return false; + } + + ushort numberOfVMetrics = reader.ReadUInt16(); + + verticalHeaderTable = new VerticalHeaderTable( + version, + ascender, + descender, + lineGap, + advanceHeightMax, + minTopSideBearing, + minBottomSideBearing, + yMaxExtent, + caretSlopeRise, + caretSlopeRun, + caretOffset, + numberOfVMetrics); + + return true; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/UnmanagedFontMemory.cs b/src/Avalonia.Base/Media/Fonts/UnmanagedFontMemory.cs new file mode 100644 index 0000000000..155f5e5572 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/UnmanagedFontMemory.cs @@ -0,0 +1,356 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Avalonia.Media.Fonts +{ + /// + /// 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 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 (_ptr == IntPtr.Zero || _length < 12) + { + return false; + } + + // Create a span over the unmanaged memory (read-only view) + var fontData = Memory.Span; + + // Minimal SFNT header: 4 (sfnt) + 2 (numTables) + 6 (rest) = 12 + if (fontData.Length < 12) + { + return false; + } + + // Check cache first + if (_tableCache.TryGetValue(tag, out var cached)) + { + table = cached; + + return true; + } + + // Parse table directory + var numTables = BinaryPrimitives.ReadUInt16BigEndian(fontData.Slice(4, 2)); + var recordsStart = 12; + var requiredDirectoryBytes = checked(recordsStart + numTables * 16); + + if (fontData.Length < requiredDirectoryBytes) + { + return false; + } + + for (int i = 0; i < numTables; i++) + { + var entryOffset = recordsStart + i * 16; + var entrySlice = fontData.Slice(entryOffset, 16); + var entryTag = (OpenTypeTag)BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(0, 4)); + + if (entryTag != tag) + { + continue; + } + + var offset = BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(8, 4)); + var length = BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(12, 4)); + + // Bounds checks - ensure values fit within the span + if (offset > (uint)fontData.Length || length > (uint)fontData.Length) + { + return false; + } + + if (offset + length > (uint)fontData.Length) + { + return false; + } + + // Safe to cast to int for Slice since we validated bounds + table = Memory.Slice((int)offset, (int)length); + + // Acquire write lock to update cache + _lock.EnterWriteLock(); + + try + { + // Cache the result for faster subsequent lookups + _tableCache[tag] = table; + + return true; + } + finally + { + // Release write lock + _lock.ExitWriteLock(); + } + } + + return false; + } + finally + { + // Release upgradeable read lock + _lock.ExitUpgradeableReadLock(); + } + } + + /// + /// 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 + { + data.CopyTo(new Span((void*)ptr, 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 (_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 (_ptr == IntPtr.Zero || _length == 0) + { + return new MemoryHandle(); + } + + if (elementIndex > _length) + { + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + } + + unsafe + { + var p = (byte*)_ptr.ToPointer() + elementIndex; + return new MemoryHandle(p); + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public override void Unpin() + { + // Decrement pin count + Interlocked.Decrement(ref _pinCount); + } + + public void Dispose() + { + Dispose(true); + } + + protected override void Dispose(bool disposing) + { + // Always use lock for disposal since we don't have a finalizer + _lock.EnterWriteLock(); + + try + { + if (Volatile.Read(ref _pinCount) > 0) + { + throw new InvalidOperationException("Cannot dispose while memory is pinned."); + } + + if (_ptr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_ptr); + _ptr = IntPtr.Zero; + } + + _length = 0; + } + finally + { + _lock.ExitWriteLock(); + _lock.Dispose(); + } + } + } +} diff --git a/src/Avalonia.Base/Media/GlyphMetrics.cs b/src/Avalonia.Base/Media/GlyphMetrics.cs index e9b5a112ac..11a4a3fa23 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 489dcb7a40..41eed5b747 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -39,7 +39,7 @@ namespace Avalonia.Media /// The baseline origin of the run. /// The bidi level. public GlyphRun( - IGlyphTypeface glyphTypeface, + GlyphTypeface glyphTypeface, double fontRenderingEmSize, ReadOnlyMemory characters, IReadOnlyList glyphIndices, @@ -61,7 +61,7 @@ namespace Avalonia.Media /// The baseline origin of the run. /// The bidi level. public GlyphRun( - IGlyphTypeface glyphTypeface, + GlyphTypeface glyphTypeface, double fontRenderingEmSize, ReadOnlyMemory characters, IReadOnlyList glyphInfos, @@ -90,19 +90,25 @@ namespace Avalonia.Media } private static IReadOnlyList CreateGlyphInfos(IReadOnlyList glyphIndices, - double fontRenderingEmSize, IGlyphTypeface glyphTypeface) + double fontRenderingEmSize, GlyphTypeface 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) + var advances = glyphIndexSpan.Length <= 256 + ? stackalloc ushort[glyphIndexSpan.Length] + : new ushort[glyphIndexSpan.Length]; + + if (glyphTypeface.TryGetHorizontalGlyphAdvances(glyphIndexSpan, advances)) { - glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale); + for (var i = 0; i < glyphIndexSpan.Length; ++i) + { + glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, advances[i] * scale); + } } - + return glyphInfos; } @@ -137,9 +143,9 @@ namespace Avalonia.Media } /// - /// Gets the for the . + /// Gets the for the . /// - public IGlyphTypeface GlyphTypeface { get; } + public GlyphTypeface GlyphTypeface { get; } /// /// Gets or sets the em size used for rendering the . @@ -205,7 +211,7 @@ namespace Avalonia.Media } /// - /// Gets the scale of the current + /// Gets the scale of the current /// internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight; @@ -270,7 +276,7 @@ namespace Avalonia.Media //For in cluster hits we need to move to the start of the next cluster. if (inClusterHit) { - for(; glyphIndex < _glyphInfos.Count; glyphIndex++) + for (; glyphIndex < _glyphInfos.Count; glyphIndex++) { if (_glyphInfos[glyphIndex].GlyphCluster > characterIndex) { @@ -367,7 +373,7 @@ namespace Avalonia.Media 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 0000000000..ca8e3fec16 --- /dev/null +++ b/src/Avalonia.Base/Media/GlyphTypeface.cs @@ -0,0 +1,655 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.CompilerServices; +using Avalonia.Media.Fonts; +using Avalonia.Media.Fonts.Tables; +using Avalonia.Media.Fonts.Tables.Cmap; +using Avalonia.Media.Fonts.Tables.Metrics; +using Avalonia.Media.Fonts.Tables.Name; +using Avalonia.Platform; + +namespace Avalonia.Media +{ + /// + /// 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. + public sealed class GlyphTypeface + { + private static readonly IReadOnlyDictionary s_emptyStringDictionary = + new Dictionary(0); + + private bool _isDisposed; + + private readonly NameTable? _nameTable; + private readonly OS2Table _os2Table; + private readonly CharacterToGlyphMap _cmapTable; + private readonly HorizontalHeaderTable _hhTable; + private readonly VerticalHeaderTable _vhTable; + private readonly HorizontalMetricsTable? _hmTable; + private readonly VerticalMetricsTable? _vmTable; + private readonly bool _hasOs2Table; + private readonly bool _hasHorizontalMetrics; + private readonly bool _hasVerticalMetrics; + + private IReadOnlyList? _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; + + _hasOs2Table = OS2Table.TryLoad(this, out _os2Table); + _cmapTable = CmapTable.Load(this); + + var maxpTable = MaxpTable.Load(this); + + GlyphCount = maxpTable.NumGlyphs; + + _hasHorizontalMetrics = HorizontalHeaderTable.TryLoad(this, out _hhTable); + + if (_hasHorizontalMetrics) + { + _hmTable = HorizontalMetricsTable.Load(this, _hhTable.NumberOfHMetrics, GlyphCount); + } + + _hasVerticalMetrics = VerticalHeaderTable.TryLoad(this, out _vhTable); + + if (_hasVerticalMetrics) + { + _vmTable = VerticalMetricsTable.Load(this, _vhTable.NumberOfVMetrics, GlyphCount); + } + + var ascent = 0; + var descent = 0; + var lineGap = 0; + + if (_hasOs2Table && (_os2Table.Selection & OS2Table.FontSelectionFlags.USE_TYPO_METRICS) != 0) + { + ascent = -_os2Table.TypoAscender; + descent = -_os2Table.TypoDescender; + lineGap = _os2Table.TypoLineGap; + } + else + { + if (_hasHorizontalMetrics) + { + ascent = -_hhTable.Ascender; + descent = -_hhTable.Descender; + lineGap = _hhTable.LineGap; + } + } + + if (_hasOs2Table && (ascent == 0 || descent == 0)) + { + if (_os2Table.TypoAscender != 0 || _os2Table.TypoDescender != 0) + { + ascent = -_os2Table.TypoAscender; + descent = -_os2Table.TypoDescender; + lineGap = _os2Table.TypoLineGap; + } + else + { + ascent = -_os2Table.WinAscent; + descent = _os2Table.WinDescent; + } + } + + HeadTable.TryLoad(this, out var headTable); + + var postTable = PostTable.Load(this); + + var isFixedPitch = postTable.IsFixedPitch; + var underlineOffset = postTable.UnderlinePosition; + var underlineSize = postTable.UnderlineThickness; + + Metrics = new FontMetrics + { + DesignEmHeight = headTable?.UnitsPerEm ?? 0, + Ascent = ascent, + Descent = descent, + LineGap = lineGap, + UnderlinePosition = -underlineOffset, + UnderlineThickness = underlineSize, + StrikethroughPosition = _hasOs2Table ? -_os2Table.StrikeoutPosition : 0, + StrikethroughThickness = _hasOs2Table ? _os2Table.StrikeoutSize : 0, + IsFixedPitch = isFixedPitch + }; + + FontSimulations = fontSimulations; + + var fontWeight = GetFontWeight(_hasOs2Table ? _os2Table : null, headTable); + + Weight = (fontSimulations & FontSimulations.Bold) != 0 ? FontWeight.Bold : fontWeight; + + var style = GetFontStyle(_hasOs2Table ? _os2Table : null, headTable, postTable); + + Style = (fontSimulations & FontSimulations.Oblique) != 0 ? FontStyle.Italic : style; + + var stretch = GetFontStretch(_hasOs2Table ? _os2Table : null); + + Stretch = stretch; + + _nameTable = NameTable.Load(this); + + FamilyName = _nameTable?.FontFamilyName((ushort)CultureInfo.InvariantCulture.LCID) ?? "unknown"; + + TypographicFamilyName = _nameTable?.GetNameById((ushort)CultureInfo.InvariantCulture.LCID, KnownNameIds.TypographicFamilyName) ?? FamilyName; + + if (_nameTable != null) + { + Dictionary? familyNames = null; + Dictionary? faceNames = null; + + foreach (var nameRecord in _nameTable) + { + if (nameRecord.NameID == KnownNameIds.FontFamilyName) + { + if (nameRecord.Platform != Fonts.Tables.PlatformID.Windows || nameRecord.LanguageID == 0) + { + continue; + } + + var culture = GetCulture(nameRecord.LanguageID); + + familyNames ??= new Dictionary(1); + + if (!familyNames.ContainsKey(culture)) + { + familyNames[culture] = nameRecord.GetValue(); + } + } + + if (nameRecord.NameID == KnownNameIds.FontSubfamilyName) + { + if (nameRecord.Platform != Fonts.Tables.PlatformID.Windows || nameRecord.LanguageID == 0) + { + continue; + } + + var culture = GetCulture(nameRecord.LanguageID); + + faceNames ??= new Dictionary(1); + + if (!faceNames.ContainsKey(culture)) + { + faceNames[culture] = nameRecord.GetValue(); + } + } + } + + FamilyNames = familyNames ?? s_emptyStringDictionary; + FaceNames = faceNames ?? s_emptyStringDictionary; + } + 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); + } + catch (CultureNotFoundException) + { + return CultureInfo.InvariantCulture; + } + } + } + + /// + /// Gets the family name of the font. + /// + public string FamilyName { get; } + + /// + /// Gets the typographic family name of the font. + /// + public string TypographicFamilyName { get; } + + /// + /// Gets a read-only mapping of localized culture-specific family names. + /// + /// The dictionary contains entries for each supported culture, where the key is a representing the culture, and the value is the corresponding localized family name. The + /// dictionary may be empty if no family names are available. + public IReadOnlyDictionary FamilyNames { get; } + + /// + /// Gets a read-only mapping of culture-specific face names. + /// + /// Each entry in the dictionary maps a to + /// the corresponding localized face name. The dictionary is empty if no face names are defined. + public IReadOnlyDictionary FaceNames { get; } + + /// + /// Gets a read-only mapping of Unicode character codes to glyph indices for the font. + /// + /// This dictionary provides the correspondence between Unicode code points and the + /// glyphs defined in the font. The mapping can be used to look up the glyph index for a given character when + /// rendering or processing text. The set of mapped characters depends on the font's supported character + /// set. + public CharacterToGlyphMap CharacterToGlyphMap => _cmapTable; + + /// + /// Gets the font metrics associated with this font. + /// + public FontMetrics Metrics { get; } + + /// + /// Gets the font weight. + /// + public FontWeight Weight { get; } + + /// + /// Gets the font style. + /// + public FontStyle Style { get; } + + /// + /// Gets the font stretch. + /// + public FontStretch Stretch { get; } + + /// + /// Gets the font simulation settings applied to the . + /// + public FontSimulations FontSimulations { get; } + + /// + /// Gets the number of glyphs held by this font. + /// + public int GlyphCount { get; } + + /// + /// Gets the list of OpenType feature tags supported by the font. + /// + /// The returned list reflects the features available in the underlying font and is + /// read-only. The order of features in the list is not guaranteed. This property does not return null; if the + /// font does not support any features, the list will be empty. + public IReadOnlyList SupportedFeatures + { + get + { + if (_supportedFeatures != null) + { + return _supportedFeatures; + } + + _supportedFeatures = LoadSupportedFeatures(); + + return _supportedFeatures; + } + } + + /// + /// Gets the platform-specific typeface associated with this font. + /// + public IPlatformTypeface PlatformTypeface { get; } + + /// + /// Gets the typeface information used by the text shaper for this font. + /// + /// The returned typeface is created on demand and cached for subsequent accesses. This + /// property is typically used by text rendering components that require low-level font shaping + /// details. + public ITextShaperTypeface TextShaperTypeface + { + get + { + if (_textShaperTypeface != null) + { + return _textShaperTypeface; + } + + var textShaper = AvaloniaLocator.Current.GetRequiredService(); + + _textShaperTypeface = textShaper.CreateTypeface(this); + + return _textShaperTypeface; + } + } + + /// + /// Attempts to retrieve the horizontal advance width for the specified glyph. + /// + /// Returns false if horizontal metrics are not available or if the specified glyph is + /// not present in the metrics table. + /// The identifier of the glyph for which to obtain the horizontal advance width. + /// When this method returns, contains the horizontal advance width of the glyph if found; otherwise, zero. This + /// parameter is passed uninitialized. + /// true if the horizontal advance width was successfully retrieved; otherwise, false. + public bool TryGetHorizontalGlyphAdvance(ushort glyphId, out ushort advance) + { + advance = default; + + if (!_hasHorizontalMetrics || _hmTable is null) + { + return false; + } + + if (!_hmTable.TryGetAdvance(glyphId, out advance)) + { + return false; + } + + return true; + } + + /// + /// Attempts to retrieve horizontal advance widths for multiple glyphs in a single operation. + /// + /// This method is significantly more efficient than calling + /// multiple times as it minimizes memory access overhead and exploits data locality. This is the preferred method + /// for batch glyph metrics retrieval in text layout and rendering scenarios. Returns false if horizontal metrics + /// are not available. + /// Read-only span of glyph identifiers for which to retrieve advance widths. + /// Output span to write the advance widths. Must be at least as long as . + /// true if horizontal metrics are available and all advances were successfully retrieved; otherwise, false. + public bool TryGetHorizontalGlyphAdvances(ReadOnlySpan glyphIds, Span advances) + { + if (!_hasHorizontalMetrics || _hmTable is null) + { + return false; + } + + return _hmTable.TryGetAdvances(glyphIds, advances); + } + + /// + /// Attempts to retrieve the metrics for the specified glyph. + /// + /// This method returns metrics only if horizontal or vertical metrics are available for + /// the specified glyph. If neither is available, the method returns false and the output parameter is set to + /// its default value. + /// The identifier of the glyph for which to obtain metrics. + /// When this method returns, contains the metrics for the specified glyph if found; otherwise, contains the + /// default value. + /// true if metrics for the specified glyph are available; otherwise, false. + public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) + { + metrics = default; + + HorizontalGlyphMetric hMetric = default; + VerticalGlyphMetric vMetric = default; + + var hasHorizontal = false; + var hasVertical = false; + + if (_hasHorizontalMetrics && _hmTable != null) + { + hasHorizontal = _hmTable.TryGetMetrics(glyph, out hMetric); + } + + if (_hasVerticalMetrics && _vmTable != null) + { + hasVertical = _vmTable.TryGetMetrics(glyph, out vMetric); + } + + if (!hasHorizontal && !hasVertical) + { + return false; + } + + metrics = new GlyphMetrics + { + XBearing = hMetric.LeftSideBearing, + YBearing = vMetric.TopSideBearing, + Width = hMetric.AdvanceWidth, + Height = vMetric.AdvanceHeight + }; + + return true; + } + + /// + /// Attempts to retrieve glyph metrics for multiple glyphs in a single operation. + /// + /// This method is significantly more efficient than calling + /// multiple times as it minimizes memory access overhead and exploits data locality. This is the preferred + /// method for batch glyph metrics retrieval in text layout and rendering scenarios. Returns false if neither + /// horizontal nor vertical metrics are available. + /// Read-only span of glyph identifiers for which to retrieve metrics. + /// Output span to write the glyph metrics. Must be at least as long as . + /// true if metrics are available and all were successfully retrieved; otherwise, false. + public bool TryGetGlyphMetrics(ReadOnlySpan glyphIds, Span metrics) + { + if (metrics.Length < glyphIds.Length) + { + throw new ArgumentException("Output span must be at least as long as input span", nameof(metrics)); + } + + if (!_hasHorizontalMetrics && !_hasVerticalMetrics) + { + return false; + } + + // Use stackalloc for temporary buffers to avoid heap allocations + Span hMetrics = glyphIds.Length <= 256 + ? stackalloc HorizontalGlyphMetric[glyphIds.Length] + : new HorizontalGlyphMetric[glyphIds.Length]; + + Span vMetrics = glyphIds.Length <= 256 + ? stackalloc VerticalGlyphMetric[glyphIds.Length] + : new VerticalGlyphMetric[glyphIds.Length]; + + bool hasHorizontal = false; + bool hasVertical = false; + + // Batch retrieve horizontal metrics + if (_hasHorizontalMetrics && _hmTable != null) + { + hasHorizontal = _hmTable.TryGetMetrics(glyphIds, hMetrics); + } + + // Batch retrieve vertical metrics + if (_hasVerticalMetrics && _vmTable != null) + { + hasVertical = _vmTable.TryGetMetrics(glyphIds, vMetrics); + } + + if (!hasHorizontal && !hasVertical) + { + return false; + } + + // Combine horizontal and vertical metrics + for (int i = 0; i < glyphIds.Length; i++) + { + metrics[i] = new GlyphMetrics + { + XBearing = hasHorizontal ? hMetrics[i].LeftSideBearing : (short)0, + YBearing = hasVertical ? vMetrics[i].TopSideBearing : (short)0, + Width = hasHorizontal ? hMetrics[i].AdvanceWidth : (ushort)0, + Height = hasVertical ? vMetrics[i].AdvanceHeight : (ushort)0 + }; + } + + return true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private IReadOnlyList LoadSupportedFeatures() + { + var gPosFeatures = FeatureListTable.LoadGPos(this); + var gSubFeatures = FeatureListTable.LoadGSub(this); + + var count = (gPosFeatures?.Features.Count ?? 0) + (gSubFeatures?.Features.Count ?? 0); + + if (count == 0) + { + return []; + } + + var supportedFeatures = new List(count); + + if (gPosFeatures != null) + { + foreach (var gPosFeature in gPosFeatures.Features) + { + if (supportedFeatures.Contains(gPosFeature)) + { + continue; + } + + supportedFeatures.Add(gPosFeature); + } + } + + if (gSubFeatures != null) + { + foreach (var gSubFeature in gSubFeatures.Features) + { + if (supportedFeatures.Contains(gSubFeature)) + { + continue; + } + + supportedFeatures.Add(gSubFeature); + } + } + + return supportedFeatures; + } + + private static FontStyle GetFontStyle(OS2Table? oS2Table, HeadTable? headTable, PostTable postTable) + { + bool isItalic = false; + bool isOblique = false; + + if (oS2Table.HasValue) + { + isItalic = (oS2Table.Value.Selection & OS2Table.FontSelectionFlags.ITALIC) != 0; + isOblique = (oS2Table.Value.Selection & OS2Table.FontSelectionFlags.OBLIQUE) != 0; + } + + if (!isItalic && headTable != null) + { + isItalic = headTable.MacStyle.HasFlag(MacStyleFlags.Italic); + } + + var italicAngle = postTable.ItalicAngle; + + if (isOblique) + { + return FontStyle.Oblique; + } + + if (Math.Abs(italicAngle) > 0.01f && !isItalic) + { + return FontStyle.Oblique; + } + + if (isItalic) + { + return FontStyle.Italic; + } + + return FontStyle.Normal; + } + + private static FontWeight GetFontWeight(OS2Table? os2Table, HeadTable? headTable) + { + if (os2Table.HasValue && os2Table.Value.WeightClass >= 1 && os2Table.Value.WeightClass <= 1000) + { + return (FontWeight)os2Table.Value.WeightClass; + } + + if (headTable != null && headTable.MacStyle.HasFlag(MacStyleFlags.Bold)) + { + return FontWeight.Bold; + } + + if (os2Table.HasValue && os2Table.Value.Panose.FamilyKind == PanoseFamilyKind.LatinText) + { + return os2Table.Value.Panose.Weight switch + { + PanoseWeight.VeryLight => FontWeight.Thin, + PanoseWeight.Light => FontWeight.Light, + PanoseWeight.Thin => FontWeight.ExtraLight, + PanoseWeight.Book => FontWeight.Normal, + PanoseWeight.Medium => FontWeight.Medium, + PanoseWeight.Demi => FontWeight.SemiBold, + PanoseWeight.Bold => FontWeight.Bold, + PanoseWeight.Heavy => FontWeight.ExtraBold, + PanoseWeight.Black => FontWeight.Black, + PanoseWeight.ExtraBlack => FontWeight.ExtraBlack, + _ => FontWeight.Normal + }; + } + + return FontWeight.Normal; + } + + private static FontStretch GetFontStretch(OS2Table? os2Table) + { + if (os2Table.HasValue && os2Table.Value.WidthClass >= 1 && os2Table.Value.WidthClass <= 9) + { + return (FontStretch)os2Table.Value.WidthClass; + } + + if (os2Table.HasValue && os2Table.Value.Panose.FamilyKind == PanoseFamilyKind.LatinText) + { + return os2Table.Value.Panose.Proportion switch + { + PanoseProportion.VeryCondensed => FontStretch.UltraCondensed, + PanoseProportion.Condensed => FontStretch.Condensed, + PanoseProportion.Modern or PanoseProportion.EvenWidth or PanoseProportion.OldStyle => FontStretch.Normal, + PanoseProportion.Extended => FontStretch.Expanded, + PanoseProportion.VeryExtended => FontStretch.UltraExpanded, + PanoseProportion.Monospaced => FontStretch.Normal, + _ => FontStretch.Normal + }; + } + + return FontStretch.Normal; + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!disposing) + { + return; + } + + PlatformTypeface.Dispose(); + } + } +} diff --git a/src/Avalonia.Base/Media/IFontMemory.cs b/src/Avalonia.Base/Media/IFontMemory.cs new file mode 100644 index 0000000000..edf9a88242 --- /dev/null +++ b/src/Avalonia.Base/Media/IFontMemory.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Media.Fonts; +using Avalonia.Metadata; + +namespace Avalonia.Media +{ + [NotClientImplementable] + public interface IFontMemory : IDisposable + { + /// + /// Attempts to retrieve the memory block associated with the specified OpenType table tag. + /// + /// 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/IGlyphTypeface.cs b/src/Avalonia.Base/Media/IGlyphTypeface.cs deleted file mode 100644 index ab3080e7c8..0000000000 --- a/src/Avalonia.Base/Media/IGlyphTypeface.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Metadata; - -namespace Avalonia.Media -{ - [Unstable] - public interface IGlyphTypeface : IDisposable - { - /// - /// Gets the family name for the object. - /// - string FamilyName { get; } - - /// - /// Gets the designed weight of the font represented by the object. - /// - FontWeight Weight { get; } - - /// - /// Gets the style for the object. - /// - FontStyle Style { get; } - - /// - /// Gets the value for the object. - /// - FontStretch Stretch { get; } - - /// - /// Gets the number of glyphs held by this glyph typeface. - /// - int GlyphCount { get; } - - /// - /// Gets the font metrics. - /// - /// - /// The font metrics. - /// - FontMetrics Metrics { get; } - - /// - /// Gets the algorithmic style simulations applied to this glyph typeface. - /// - FontSimulations FontSimulations { get; } - - /// - /// Tries to get a glyph's metrics in em units. - /// - /// The glyph id. - /// The glyph metrics. - /// - /// 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); - - /// - /// Tries to get an glyph index for specified codepoint. - /// - /// The codepoint. - /// A glyph index. - /// - /// true if an glyph index was found, false otherwise. - /// - bool TryGetGlyph(uint codepoint, out ushort glyph); - - /// - /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0. - /// - /// The codepoints to map. - /// - /// An array of glyph indices. - /// - ushort[] GetGlyphs(ReadOnlySpan codepoints); - - /// - /// Returns the glyph advance for the specified glyph. - /// - /// The glyph. - /// - /// The advance. - /// - int GetGlyphAdvance(ushort glyph); - - /// - /// Returns an array of glyph advances in design em size. - /// - /// The glyph indices. - /// - /// An array of glyph advances. - /// - int[] GetGlyphAdvances(ReadOnlySpan glyphs); - - /// - /// Returns the contents of the table data for the specified 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, [NotNullWhen(true)] out byte[]? table); - } -} diff --git a/src/Avalonia.Base/Media/IGlyphTypeface2.cs b/src/Avalonia.Base/Media/IGlyphTypeface2.cs deleted file mode 100644 index 3bd2b1e767..0000000000 --- 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/IPlatformTypeface.cs b/src/Avalonia.Base/Media/IPlatformTypeface.cs new file mode 100644 index 0000000000..efbd8d1b3c --- /dev/null +++ b/src/Avalonia.Base/Media/IPlatformTypeface.cs @@ -0,0 +1,46 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Avalonia.Metadata; + +namespace Avalonia.Media +{ + [NotClientImplementable] + public interface IPlatformTypeface : IFontMemory + { + /// + /// Gets the font family name. + /// + /// + /// The family name should be the same as the one used to create the typeface via the platform font manager. + /// It can be different from the actaual family name because an alias or a fallback name could have been used. + /// + string FamilyName { get; } + + /// + /// Gets the designed weight of the font represented by the object. + /// + FontWeight Weight { get; } + + /// + /// Gets the style for the object. + /// + FontStyle Style { get; } + + /// + /// Gets the value for the object. + /// + FontStretch Stretch { get; } + + /// + /// Gets the algorithmic style simulations applied to object. + /// + FontSimulations FontSimulations { get; } + + /// + /// Returns the font file stream represented by the . + /// + /// The stream. + /// Returns true if the stream can be obtained, otherwise false. + bool TryGetStream([NotNullWhen(true)] out Stream? stream); + } +} diff --git a/src/Avalonia.Base/Media/ITextShaperTypeface.cs b/src/Avalonia.Base/Media/ITextShaperTypeface.cs new file mode 100644 index 0000000000..d3b4948397 --- /dev/null +++ b/src/Avalonia.Base/Media/ITextShaperTypeface.cs @@ -0,0 +1,11 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Media +{ + [NotClientImplementable] + public interface ITextShaperTypeface : IDisposable + { + + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 5ab7770192..71640fac08 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -13,7 +13,7 @@ namespace Avalonia.Media.TextFormatting private GlyphInfo[]? _rentedBuffer; private ArraySlice _glyphInfos; - public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) + public ShapedBuffer(ReadOnlyMemory text, int bufferLength, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { Text = text; _rentedBuffer = ArrayPool.Shared.Rent(bufferLength); @@ -23,7 +23,7 @@ namespace Avalonia.Media.TextFormatting BidiLevel = bidiLevel; } - internal ShapedBuffer(ReadOnlyMemory text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) + internal ShapedBuffer(ReadOnlyMemory text, ArraySlice glyphInfos, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { Text = text; _glyphInfos = glyphInfos; @@ -40,7 +40,7 @@ namespace Avalonia.Media.TextFormatting /// /// The buffer's glyph typeface. /// - public IGlyphTypeface GlyphTypeface { get; } + public GlyphTypeface GlyphTypeface { get; } /// /// The buffers font rendering em size. diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index acfffb68fa..cc48ed47c6 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -146,7 +146,7 @@ namespace Avalonia.Media.TextFormatting //Move forward until we reach the next base character while (enumerator.MoveNext(out grapheme)) { - if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) + if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.CharacterToGlyphMap.TryGetGlyph(grapheme.FirstCodepoint, out _)) { break; } @@ -167,8 +167,8 @@ namespace Avalonia.Media.TextFormatting /// internal static bool TryGetShapeableLength( ReadOnlySpan text, - IGlyphTypeface glyphTypeface, - IGlyphTypeface? defaultGlyphTypeface, + GlyphTypeface glyphTypeface, + GlyphTypeface? defaultGlyphTypeface, out int length) { length = 0; @@ -194,15 +194,15 @@ namespace Avalonia.Media.TextFormatting if (!currentCodepoint.IsWhiteSpace && defaultGlyphTypeface != null - && defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _)) + && defaultGlyphTypeface.CharacterToGlyphMap.TryGetGlyph(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.TryGetGlyph(currentCodepoint, out _)) { break; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 0e9308b6ed..605804a82d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -719,7 +719,7 @@ namespace Avalonia.Media.TextFormatting var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; var glyphTypeface = properties.CachedGlyphTypeface; - var glyph = glyphTypeface.GetGlyph(s_empty[0]); + var glyph = glyphTypeface.CharacterToGlyphMap[s_empty[0]]; var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, diff --git a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs index db59f92661..c0412d1efc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs @@ -5,7 +5,7 @@ /// public readonly record struct TextMetrics { - public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize) + public TextMetrics(GlyphTypeface glyphTypeface, double fontRenderingEmSize) { var fontMetrics = glyphTypeface.Metrics; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs index 11db49aaf1..cf2f74ee4d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs @@ -12,7 +12,7 @@ namespace Avalonia.Media.TextFormatting /// public abstract class TextRunProperties : IEquatable { - private IGlyphTypeface? _cachedGlyphTypeFace; + private GlyphTypeface? _cachedGlyphTypeFace; /// /// Run typeface @@ -54,7 +54,7 @@ namespace Avalonia.Media.TextFormatting /// public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline; - internal IGlyphTypeface CachedGlyphTypeface + internal GlyphTypeface CachedGlyphTypeface => _cachedGlyphTypeFace ??= Typeface.GlyphTypeface; public bool Equals(TextRunProperties? other) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs index 7fc9398732..9ae20a3eaa 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media.TextFormatting { // TODO12: Remove in 12.0.0 and make fontFeatures parameter in main ctor optional public TextShaperOptions( - IGlyphTypeface typeface, + GlyphTypeface typeface, double fontRenderingEmSize = 12, sbyte bidiLevel = 0, CultureInfo? culture = null, @@ -22,7 +22,7 @@ namespace Avalonia.Media.TextFormatting // TODO12:Change signature in 12.0.0 public TextShaperOptions( - IGlyphTypeface typeface, + GlyphTypeface typeface, IReadOnlyList? fontFeatures, double fontRenderingEmSize = 12, sbyte bidiLevel = 0, @@ -30,7 +30,7 @@ namespace Avalonia.Media.TextFormatting double incrementalTabWidth = 0, double letterSpacing = 0) { - Typeface = typeface; + GlyphTypeface = typeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; Culture = culture; @@ -42,7 +42,7 @@ namespace Avalonia.Media.TextFormatting /// /// Get the typeface. /// - public IGlyphTypeface Typeface { get; } + public GlyphTypeface GlyphTypeface { get; } /// /// Get the font rendering em size. /// diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs index c9ad271568..1adcac5b75 100644 --- a/src/Avalonia.Base/Media/Typeface.cs +++ b/src/Avalonia.Base/Media/Typeface.cs @@ -83,7 +83,7 @@ namespace Avalonia.Media /// /// The glyph typeface. /// - public IGlyphTypeface GlyphTypeface + public GlyphTypeface GlyphTypeface { get { diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index e978ba03a8..dc5e7f3c15 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -30,12 +30,12 @@ namespace Avalonia.Platform /// The font stretch. /// The family name. This is optional and can be used as an initial hint for matching. /// The culture. - /// The matching typeface. + /// The matching platform typeface. /// /// True, if the could match the character to specified parameters, False otherwise. /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface); + FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, [NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface); /// /// Tries to get a glyph typeface for specified parameters. @@ -44,42 +44,23 @@ namespace Avalonia.Platform /// The font style. /// The font weiht. /// The font stretch. - /// The created glyphTypeface + /// The created platform typeface /// /// True, if the could create the glyph typeface, False otherwise. /// bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); + FontStretch stretch, [NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface); /// /// Tries to create a glyph typeface from specified stream. /// /// A stream that holds the font's data. /// Specifies algorithmic style simulations. - /// The created glyphTypeface + /// The created platform typeface /// /// 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 match a specified character to a typeface that supports specified font properties. - /// - /// The codepoint to match against. - /// The font style. - /// The font weight. - /// The font stretch. - /// The family name. This is optional and can be used as an initial hint for matching. - /// The culture. - /// 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, string? familyName, CultureInfo? culture, [NotNullWhen(true)] out IGlyphTypeface? typeface); + bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface); /// /// 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 2342f32307..620f22b77c 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/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 30b426489a..2deb289ddf 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -170,7 +170,7 @@ namespace Avalonia.Platform /// The list of glyphs. /// The baseline origin of the run. Can be null. /// An . - IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin); + IGlyphRunImpl CreateGlyphRun(GlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin); /// /// Creates a backend-specific object using a low-level API graphics context diff --git a/src/Avalonia.Base/Platform/ITextShaperImpl.cs b/src/Avalonia.Base/Platform/ITextShaperImpl.cs index a651b49e64..ec02fb9990 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; @@ -7,7 +8,7 @@ namespace Avalonia.Platform /// /// An abstraction that is used produce shaped text. /// - [Unstable] + [NotClientImplementable] public interface ITextShaperImpl { /// @@ -17,5 +18,13 @@ namespace Avalonia.Platform /// 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(GlyphTypeface glyphTypeface); + } } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs index a71dd6f3e6..78392ec31d 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs @@ -41,10 +41,16 @@ internal static class StorageBookmarkHelper var arrayLength = HeaderLength + nativeBookmarkBytes.Length; var arrayPool = ArrayPool.Shared.Rent(arrayLength); + try { // Write platform into first 16 bytes. var arraySpan = arrayPool.AsSpan(0, arrayLength); + + // Ensure any leftover data from the pooled array is cleared before we use it so + // that bytes we don't overwrite (e.g. header padding) won't leak into the encoded bookmark. + arraySpan.Clear(); + AvaHeaderPrefix.CopyTo(arraySpan); platform.CopyTo(arraySpan.Slice(AvaHeaderPrefix.Length)); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs index 986692176b..2c2e3e886b 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 @@ namespace Avalonia.Rendering.Composition.Server return maxHeight; } - public DiagnosticTextRenderer(IGlyphTypeface typeface, double fontRenderingEmSize) + public DiagnosticTextRenderer(GlyphTypeface 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 7574c55042..6c85ca40fc 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 @@ namespace Avalonia { public static AppBuilder UsePlatformDetect(this AppBuilder builder) { + // Always load HarfBuzz on desktop platforms + LoadHarfBuzz(builder); + // We don't have the ability to load every assembly right now, so we are // stuck with manual configuration here // Helpers are extracted to separate methods to take the advantage of the fact @@ -20,7 +22,7 @@ namespace Avalonia LoadWin32(builder); LoadSkia(builder); } - else if(OperatingSystemEx.IsMacOS()) + else if (OperatingSystemEx.IsMacOS()) { LoadAvaloniaNative(builder); LoadSkia(builder); @@ -49,5 +51,8 @@ namespace Avalonia static void LoadSkia(AppBuilder builder) => builder.UseSkia(); + + static void LoadHarfBuzz(AppBuilder builder) + => builder.UseHarfBuzz(); } } diff --git a/src/Avalonia.Desktop/Avalonia.Desktop.csproj b/src/Avalonia.Desktop/Avalonia.Desktop.csproj index 3ced51775d..357016f5aa 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 0000000000..a528326db9 --- /dev/null +++ b/src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.HarfBuzz.csproj @@ -0,0 +1,22 @@ + + + $(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks) + 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 0000000000..508c9ef525 --- /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 84% rename from src/Skia/Avalonia.Skia/TextShaperImpl.cs rename to src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTextShaper.cs index 0cc1a5d2aa..cd763cb1ef 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTextShaper.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections.Concurrent; using System.Globalization; using System.Runtime.InteropServices; +using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -10,9 +11,9 @@ using HarfBuzzSharp; using Buffer = HarfBuzzSharp.Buffer; using GlyphInfo = HarfBuzzSharp.GlyphInfo; -namespace Avalonia.Skia +namespace Avalonia.Harfbuzz { - internal class TextShaperImpl : ITextShaperImpl + public class HarfBuzzTextShaper : ITextShaperImpl { [ThreadStatic] private static Buffer? s_buffer; @@ -22,7 +23,14 @@ namespace Avalonia.Skia public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var textSpan = text.Span; - var typeface = options.Typeface; + + var glyphTypeface = options.GlyphTypeface; + + if (glyphTypeface.TextShaperTypeface is not HarfBuzzTypeface harfBuzzTypeface) + { + throw new NotSupportedException("The provided GlyphTypeface is not supported by this text shaper."); + } + var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; var culture = options.Culture; @@ -48,7 +56,7 @@ namespace Avalonia.Skia static (_, culture) => new Language(culture), usedCulture); - var font = ((GlyphTypefaceImpl)typeface).Font; + var font = harfBuzzTypeface.HBFont; font.Shape(buffer, GetFeatures(options)); @@ -63,7 +71,7 @@ namespace Avalonia.Skia var bufferLength = buffer.Length; - var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var shapedBuffer = new ShapedBuffer(text, bufferLength, glyphTypeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); @@ -83,11 +91,18 @@ namespace Avalonia.Skia if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') { - glyphIndex = typeface.GetGlyph(' '); + glyphIndex = glyphTypeface.CharacterToGlyphMap[' ']; - glyphAdvance = options.IncrementalTabWidth > 0 ? - options.IncrementalTabWidth : - 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; + if(options.IncrementalTabWidth > 0) + { + glyphAdvance = options.IncrementalTabWidth; + } + else + { + glyphTypeface.TryGetHorizontalGlyphAdvance(glyphIndex, out var advance); + + glyphAdvance = 4 * advance * textScale; + } } shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); @@ -96,6 +111,12 @@ namespace Avalonia.Skia return shapedBuffer; } + + public ITextShaperTypeface CreateTypeface(GlyphTypeface glyphTypeface) + { + return new HarfBuzzTypeface(glyphTypeface); + } + private static void MergeBreakPair(Buffer buffer) { var length = buffer.Length; @@ -193,18 +214,18 @@ namespace Avalonia.Skia } var features = new Feature[options.FontFeatures.Count]; - + for (var i = 0; i < options.FontFeatures.Count; i++) { var fontFeature = options.FontFeatures[i]; features[i] = new Feature( - Tag.Parse(fontFeature.Tag), + Tag.Parse(fontFeature.Tag), (uint)fontFeature.Value, (uint)fontFeature.Start, (uint)fontFeature.End); } - + return features; } } diff --git a/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTypeface.cs b/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTypeface.cs new file mode 100644 index 0000000000..284ac4fa74 --- /dev/null +++ b/src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTypeface.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Media; +using HarfBuzzSharp; + +namespace Avalonia.Harfbuzz +{ + internal class HarfBuzzTypeface : ITextShaperTypeface + { + public HarfBuzzTypeface(GlyphTypeface glyphTypeface) + { + GlyphTypeface = glyphTypeface; + + HBFace = new Face(GetTable) { UnitsPerEm = glyphTypeface.Metrics.DesignEmHeight }; + + HBFont = new Font(HBFace); + + HBFont.SetFunctionsOpenType(); + } + + public GlyphTypeface GlyphTypeface { get; } + public Face HBFace { get; } + public Font HBFont { get; } + + private Blob? GetTable(Face face, Tag tag) + { + if (!GlyphTypeface.PlatformTypeface.TryGetTable((uint)tag, out var table)) + { + return null; + } + + // If table is backed by managed array, pin it and avoid copy. + if (MemoryMarshal.TryGetArray(table, out var seg)) + { + var handle = GCHandle.Alloc(seg.Array!, GCHandleType.Pinned); + var basePtr = handle.AddrOfPinnedObject(); + var ptr = IntPtr.Add(basePtr, seg.Offset); + + var release = new ReleaseDelegate(() => handle.Free()); + + return new Blob(ptr, seg.Count, MemoryMode.ReadOnly, release); + } + + // Fallback: allocate native memory and copy + var nativePtr = Marshal.AllocHGlobal(table.Length); + + unsafe + { + table.Span.CopyTo(new Span((void*)nativePtr, table.Length)); + } + + var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeHGlobal(nativePtr)); + + return new Blob(nativePtr, table.Length, MemoryMode.ReadOnly, releaseDelegate); + } + + public void Dispose() + { + HBFont.Dispose(); + HBFace.Dispose(); + } + + } +} diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index bd21048687..bbd10677e9 100644 --- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -5,6 +5,7 @@ + @@ -24,4 +25,9 @@ + + + + + diff --git a/src/Headless/Avalonia.Headless/BareMinimum.ttf b/src/Headless/Avalonia.Headless/BareMinimum.ttf new file mode 100644 index 0000000000..ad93f5bd13 Binary files /dev/null and b/src/Headless/Avalonia.Headless/BareMinimum.ttf differ diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 2774bf63fe..e2c406998a 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -5,11 +5,9 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Utilities; using Avalonia.Media.Imaging; using Avalonia.Media.TextFormatting; +using Avalonia.Platform; namespace Avalonia.Headless { @@ -19,8 +17,7 @@ namespace Avalonia.Headless { AvaloniaLocator.CurrentMutable .Bind().ToConstant(new HeadlessPlatformRenderInterface()) - .Bind().ToConstant(new HeadlessFontManagerStub()) - .Bind().ToConstant(new HeadlessTextShaperStub()); + .Bind().ToConstant(new HeadlessFontManagerStub()); } public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) => this; @@ -134,7 +131,7 @@ namespace Avalonia.Headless } public IGlyphRunImpl CreateGlyphRun( - IGlyphTypeface glyphTypeface, + GlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin) @@ -145,7 +142,7 @@ namespace Avalonia.Headless internal class HeadlessGlyphRunStub : IGlyphRunImpl { public HeadlessGlyphRunStub( - IGlyphTypeface glyphTypeface, + GlyphTypeface glyphTypeface, double fontRenderingEmSize, Point baselineOrigin) { @@ -158,7 +155,7 @@ namespace Avalonia.Headless public Point BaselineOrigin { get; } - public IGlyphTypeface GlyphTypeface { get; } + public GlyphTypeface GlyphTypeface { get; } public double FontRenderingEmSize { get; } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 167d3fb8c7..dedaa2ff58 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -6,11 +6,13 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls.Utils; +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,32 +68,21 @@ namespace Avalonia.Headless } } - internal class HeadlessGlyphTypefaceImpl : IGlyphTypeface + internal class HeadlessPlatformTypeface : IPlatformTypeface { - public HeadlessGlyphTypefaceImpl(string familyName, FontStyle style, FontWeight weight, FontStretch stretch) - { - FamilyName = familyName; - Style = style; - Weight = weight; - Stretch = stretch; - } + private readonly UnmanagedFontMemory? _fontMemory; - public FontMetrics Metrics => new FontMetrics + public HeadlessPlatformTypeface(Stream stream, string? familyName = null) { - DesignEmHeight = 10, - Ascent = 2, - Descent = 10, - IsFixedPitch = true, - LineGap = 0, - UnderlinePosition = 2, - UnderlineThickness = 1, - StrikethroughPosition = 2, - StrikethroughThickness = 1 - }; - - public int GlyphCount => 1337; + _fontMemory = UnmanagedFontMemory.LoadFromStream(stream); - public FontSimulations FontSimulations => FontSimulations.None; + var dummy = new GlyphTypeface(this); + + FamilyName = familyName ?? dummy.FamilyName; + Weight = dummy.Weight; + Style = dummy.Style; + Stretch = dummy.Stretch; + } public string FamilyName { get; } @@ -101,266 +92,200 @@ namespace Avalonia.Headless public FontStretch Stretch { get; } - public void Dispose() - { - } - - public ushort GetGlyph(uint codepoint) - { - return (ushort)codepoint; - } - - public bool TryGetGlyph(uint codepoint, out ushort glyph) - { - glyph = 8; - - return true; - } + public FontSimulations FontSimulations => FontSimulations.None; - public int GetGlyphAdvance(ushort glyph) + public void Dispose() { - return 8; + _fontMemory?.Dispose(); } - public int[] GetGlyphAdvances(ReadOnlySpan glyphs) + public bool TryGetStream([NotNullWhen(true)] out Stream? stream) { - var advances = new int[glyphs.Length]; - - for (var i = 0; i < advances.Length; i++) + stream = null; + + if (_fontMemory is null) { - advances[i] = 8; + return false; } + + var data = _fontMemory.Memory.Span; - return advances; - } + stream = new MemoryStream(data.ToArray()); - public ushort[] GetGlyphs(ReadOnlySpan codepoints) - { - return codepoints.ToArray().Select(x => (ushort)x).ToArray(); + return true; } - public bool TryGetTable(uint tag, out byte[] table) + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory table) { - table = null!; - return false; - } - - public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) - { - metrics = new GlyphMetrics - { - Width = 10, - Height = 10 - }; - - return true; + table = default; + + return _fontMemory is not null && _fontMemory.TryGetTable(tag, out table); } } +} - internal class HeadlessTextShaperStub : ITextShaperImpl +internal class HeadlessFontManagerStub : IFontManagerImpl +{ + private readonly string _defaultFamilyName; + + public HeadlessFontManagerStub(string defaultFamilyName = "Default") { - public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) - { - var typeface = options.Typeface; - var fontRenderingEmSize = options.FontRenderingEmSize; - var bidiLevel = options.BidiLevel; - var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); - var textSpan = text.Span; - var textStartIndex = TextTestHelper.GetStartCharIndex(text); - - for (var i = 0; i < shapedBuffer.Length;) - { - var glyphCluster = i + textStartIndex; - - var codepoint = Codepoint.ReadAt(textSpan, i, out var count); - - // Handle CRLF as a single cluster - if (codepoint.Value == 0x0D && Codepoint.ReadAt(textSpan, i + count, out var lfCount).Value == 0x0A) - { - count += lfCount; - } - - var glyphIndex = typeface.GetGlyph(codepoint); - - for (var j = 0; j < count; ++j) - { - shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); - } + _defaultFamilyName = defaultFamilyName; + } - i += count; - } + public string GetDefaultFontFamilyName() => _defaultFamilyName; - return shapedBuffer; - } + string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) + { + return new[] { _defaultFamilyName }; } - internal class HeadlessFontManagerStub : IFontManagerImpl + public bool TryMatchCharacter( + int codepoint, + FontStyle fontStyle, + FontWeight fontWeight, + FontStretch fontStretch, + string? familyName, + CultureInfo? culture, + out IPlatformTypeface platformTypeface) { - private readonly string _defaultFamilyName; - - public HeadlessFontManagerStub(string defaultFamilyName = "Default") - { - _defaultFamilyName = defaultFamilyName; - } - - public int TryCreateGlyphTypefaceCount { get; private set; } - - public string GetDefaultFontFamilyName() - { - return _defaultFamilyName; - } - - string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) - { - return new[] { _defaultFamilyName }; - } - - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey) - { - fontKey = new Typeface(_defaultFamilyName); - - return false; - } - - public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) - { - glyphTypeface = null; - - TryCreateGlyphTypefaceCount++; - - if (familyName == "Unknown") - { - return false; - } - - glyphTypeface = new HeadlessGlyphTypefaceImpl(familyName, style, weight, stretch); - - return true; - } - - public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) - { - glyphTypeface = new HeadlessGlyphTypefaceImpl( - FontFamily.DefaultFontFamilyName, - fontSimulations.HasFlag(FontSimulations.Oblique) ? FontStyle.Italic : FontStyle.Normal, - fontSimulations.HasFlag(FontSimulations.Bold) ? FontWeight.Bold : FontWeight.Normal, - FontStretch.Normal); + platformTypeface = null!; - TryCreateGlyphTypefaceCount++; - - return true; - } + return false; } - internal class HeadlessFontManagerWithMultipleSystemFontsStub : IFontManagerImpl + public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) { - private readonly string[] _installedFontFamilyNames; - private readonly string _defaultFamilyName; - - public HeadlessFontManagerWithMultipleSystemFontsStub( - string[] installedFontFamilyNames, - string defaultFamilyName = "Default") - { - _installedFontFamilyNames = installedFontFamilyNames; - _defaultFamilyName = defaultFamilyName; - } - - public int TryCreateGlyphTypefaceCount { get; private set; } - - public string GetDefaultFontFamilyName() - { - return _defaultFamilyName; - } + var defaultFontUri = new Uri("resm:Avalonia.Headless.BareMinimum.ttf?assembly=Avalonia.Headless"); + + var assetLoader = new StandardAssetLoader(typeof(HeadlessFontManagerStub).Assembly); + + var stream = assetLoader.Open(defaultFontUri); + + platformTypeface = new HeadlessPlatformTypeface(stream, familyName); + + return true; + } - string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) - { - return _installedFontFamilyNames; - } + public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + platformTypeface = new HeadlessPlatformTypeface(stream); + + return true; + } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey) - { - fontKey = new Typeface(_defaultFamilyName); + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + { + familyTypefaces = null; + + return false; + } +} - return false; - } +internal class HeadlessFontManagerWithMultipleSystemFontsStub : IFontManagerImpl +{ + private readonly string[] _installedFontFamilyNames; + private readonly string _defaultFamilyName; - public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) - { - glyphTypeface = null; + public HeadlessFontManagerWithMultipleSystemFontsStub( + string[] installedFontFamilyNames, + string defaultFamilyName = "Default") + { + _installedFontFamilyNames = installedFontFamilyNames; + _defaultFamilyName = defaultFamilyName; + } - TryCreateGlyphTypefaceCount++; + public int TryCreateGlyphTypefaceCount { get; private set; } - if (familyName == "Unknown") - { - return false; - } + string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) + { + return _installedFontFamilyNames; + } - glyphTypeface = new HeadlessGlyphTypefaceImpl(familyName, style, weight, stretch); + public string GetDefaultFontFamilyName() + { + return _defaultFamilyName; + } - return true; - } + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + platformTypeface = null; + + return false; + } - public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) - { - glyphTypeface = new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal); + public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + platformTypeface = null; + + return false; + } - return true; - } + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + { + familyTypefaces = null; + + return false; } - internal class HeadlessIconLoaderStub : IPlatformIconLoader + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, + string? familyName, CultureInfo? culture, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) { - private class IconStub : IWindowIconImpl - { - public void Save(Stream outputStream) - { + platformTypeface = null; + + return false; + } +} - } - } - public IWindowIconImpl LoadIcon(string fileName) +internal class HeadlessIconLoaderStub : IPlatformIconLoader +{ + private class IconStub : IWindowIconImpl + { + public void Save(Stream outputStream) { - return new IconStub(); - } - public IWindowIconImpl LoadIcon(Stream stream) - { - return new IconStub(); } + } + public IWindowIconImpl LoadIcon(string fileName) + { + return new IconStub(); + } - public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) - { - return new IconStub(); - } + public IWindowIconImpl LoadIcon(Stream stream) + { + return new IconStub(); } - internal class HeadlessScreensStub : ScreensBase + public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) { - protected override IReadOnlyList GetAllScreenKeys() => new[] { 1 }; + return new IconStub(); + } +} - protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key); +internal class HeadlessScreensStub : ScreensBase +{ + protected override IReadOnlyList GetAllScreenKeys() => new[] { 1 }; - private class PlatformScreenStub : PlatformScreen + protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key); + + private class PlatformScreenStub : PlatformScreen + { + public PlatformScreenStub(int key) : base(new PlatformHandle((nint)key, nameof(HeadlessScreensStub))) { - public PlatformScreenStub(int key) : base(new PlatformHandle((nint)key, nameof(HeadlessScreensStub))) - { - Scaling = 1; - Bounds = WorkingArea = new PixelRect(0, 0, 1920, 1280); - IsPrimary = true; - } + Scaling = 1; + Bounds = WorkingArea = new PixelRect(0, 0, 1920, 1280); + IsPrimary = true; } } +} - internal static class TextTestHelper +internal static class TextTestHelper +{ + public static int GetStartCharIndex(ReadOnlyMemory 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 f8cc9f5f78..bb5e7cca6f 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -11,7 +11,7 @@ using SkiaSharp; namespace Avalonia.Skia { - internal class FontManagerImpl : IFontManagerImpl, IFontManagerImpl2 + internal class FontManagerImpl : IFontManagerImpl { private SKFontManager _skFontManager = SKFontManager.Default; @@ -33,28 +33,6 @@ namespace Avalonia.Skia [ThreadStatic] private static string[]? t_languageTagBuffer; - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey) - { - if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out SKTypeface? skTypeface)) - { - fontKey = default; - - return false; - } - - fontKey = new Typeface( - skTypeface.FamilyName, - skTypeface.FontStyle.Slant.ToAvalonia(), - (FontWeight)skTypeface.FontStyle.Weight, - (FontStretch)skTypeface.FontStyle.Width); - - skTypeface.Dispose(); - - return true; - - } - public bool TryMatchCharacter( int codepoint, FontStyle fontStyle, @@ -62,18 +40,19 @@ namespace Avalonia.Skia FontStretch fontStretch, string? familyName, CultureInfo? culture, - [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + [NotNullWhen(returnValue: true)] out IPlatformTypeface? platformTypeface) { if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out SKTypeface? skTypeface)) { - glyphTypeface = null; + platformTypeface = null; return false; } - glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + platformTypeface = new SkiaTypeface(skTypeface, FontSimulations.None); return true; + } private bool TryMatchCharacter( @@ -117,9 +96,9 @@ namespace Avalonia.Skia } public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + FontStretch stretch, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) { - glyphTypeface = null; + platformTypeface = null; var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch, style.ToSkia()); @@ -142,23 +121,23 @@ namespace Avalonia.Skia fontSimulations |= FontSimulations.Oblique; } - glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations); + platformTypeface = new SkiaTypeface(skTypeface, fontSimulations); return true; } - public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) { var skTypeface = SKTypeface.FromStream(stream); if (skTypeface != null) { - glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations); + platformTypeface = new SkiaTypeface(skTypeface, fontSimulations); return true; } - glyphTypeface = null; + platformTypeface = null; return false; } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 0cc069308f..30f6da1dc2 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; @@ -23,7 +23,7 @@ namespace Avalonia.Skia private const int FontEdgingsCount = (int)SKFontEdging.SubpixelAntialias + 1; private readonly SKTextBlob?[] _textBlobCache = new SKTextBlob?[FontEdgingsCount]; - public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, + public GlyphRunImpl(GlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin) { if (glyphTypeface == null) @@ -36,7 +36,7 @@ namespace Avalonia.Skia throw new ArgumentNullException(nameof(glyphInfos)); } - _glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; + _glyphTypefaceImpl = (SkiaTypeface)glyphTypeface.PlatformTypeface; FontRenderingEmSize = fontRenderingEmSize; var count = glyphInfos.Count; @@ -86,8 +86,6 @@ namespace Avalonia.Skia Bounds = runBounds.Translate(new Vector(baselineOrigin.X, baselineOrigin.Y)); } - public IGlyphTypeface GlyphTypeface => _glyphTypefaceImpl; - public double FontRenderingEmSize { get; } public Point BaselineOrigin { get; } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs deleted file mode 100644 index 9629f5e462..0000000000 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ /dev/null @@ -1,385 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Runtime.InteropServices; -using Avalonia.Media; -using Avalonia.Media.Fonts; -using Avalonia.Media.Fonts.Tables; -using Avalonia.Media.Fonts.Tables.Name; -using HarfBuzzSharp; -using SkiaSharp; - -namespace Avalonia.Skia -{ - internal class GlyphTypefaceImpl : IGlyphTypeface2 - { - private bool _isDisposed; - private readonly NameTable? _nameTable; - private readonly OS2Table? _os2Table; - private readonly HorizontalHeadTable? _hhTable; - private IReadOnlyList? _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) - { - var languageId = nameRecord.LanguageID == 0 ? - (ushort)CultureInfo.InvariantCulture.LCID : - nameRecord.LanguageID; - - switch (nameRecord.NameID) - { - case KnownNameIds.FontFamilyName: - { - familyNames.TryAdd(languageId, nameRecord.Value); - break; - } - case KnownNameIds.FontSubfamilyName: - { - faceNames.TryAdd(languageId, nameRecord.Value); - break; - } - } - } - - FamilyNames = familyNames; - FaceNames = faceNames; - } - else - { - FamilyNames = new Dictionary { { (ushort)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, [NotNullWhen(true)] out byte[]? table) - { - return SKTypeface.TryGetTableData(tag, out table); - } - - public bool TryGetStream([NotNullWhen(true)] out Stream? stream) - { - try - { - var asset = SKTypeface.OpenStream(); - var size = asset.Length; - var buffer = new byte[size]; - - asset.Read(buffer, size); - - stream = new MemoryStream(buffer); - - return true; - } - catch - { - stream = null; - - return false; - } - } - } -} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 05dbd684ef..df7f6cd48f 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -2,15 +2,15 @@ using System; using System.Collections.Generic; using System.IO; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; +using Avalonia.Metal; using Avalonia.OpenGL; using Avalonia.Platform; -using Avalonia.Media.Imaging; +using Avalonia.Skia.Metal; using Avalonia.Skia.Vulkan; using Avalonia.Vulkan; using SkiaSharp; -using Avalonia.Media.TextFormatting; -using Avalonia.Metal; -using Avalonia.Skia.Metal; namespace Avalonia.Skia { @@ -81,7 +81,7 @@ namespace Avalonia.Skia public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { - if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface) + if (glyphRun.GlyphTypeface.PlatformTypeface is not SkiaTypeface glyphTypeface) { throw new InvalidOperationException("PlatformImpl can't be null."); } @@ -205,7 +205,7 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format, alphaFormat); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, + public IGlyphRunImpl CreateGlyphRun(GlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin) { return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin); diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index 27f2631db8..a81b6d5413 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -21,8 +21,7 @@ namespace Avalonia.Skia 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 0000000000..c0b397f975 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SkiaTypeface.cs @@ -0,0 +1,83 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using SkiaSharp; + +namespace Avalonia.Skia +{ + internal class SkiaTypeface : IPlatformTypeface + { + public SkiaTypeface(SKTypeface typeface, FontSimulations fontSimulations) + { + SKTypeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); + FontSimulations = fontSimulations; + Weight = (FontWeight)typeface.FontWeight; + Style = typeface.FontStyle.Slant.ToAvalonia(); + Stretch = (FontStretch)typeface.FontWidth; + } + + public SKTypeface SKTypeface { get; } + + public FontSimulations FontSimulations { get; } + + public string FamilyName => SKTypeface.FamilyName; + + public FontWeight Weight { get; } + + public FontStyle Style { get; } + + public FontStretch Stretch { get; } + + public SKFont CreateSKFont(float size) + { + return new(SKTypeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f) + { + LinearMetrics = true, + Embolden = (FontSimulations & FontSimulations.Bold) != 0 + }; + } + + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory 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/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj index 1e0cee351f..336db8c44d 100644 --- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj +++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj @@ -12,6 +12,7 @@ + diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 821a322077..67152029af 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -52,6 +52,7 @@ namespace Avalonia return builder .UseStandardRuntimePlatformSubsystem() .UseWindowingSubsystem(() => iOS.Platform.Register(appDelegate), "iOS") + .UseHarfBuzz() .UseSkia(); } diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs index 9bca3b5de4..ed95773630 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; @@ -65,12 +64,11 @@ namespace Avalonia.Base.UnitTests.Media } }; - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(fontManagerImpl: new HeadlessFontManagerStub()))) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { AvaloniaLocator.CurrentMutable.Bind().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/Tables/HeadTableTests.cs b/tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/HeadTableTests.cs new file mode 100644 index 0000000000..5e5daa2988 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/HeadTableTests.cs @@ -0,0 +1,256 @@ +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Media.Fonts.Tables; +using Avalonia.Platform; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media.Fonts.Tables +{ + public class HeadTableTests + { + private static string s_InterFontUri = "resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests"; + + [Fact] + public void Should_Load_HeadTable_From_Inter_Font() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var success = HeadTable.TryLoad(typeface, out var headTable); + + Assert.True(success); + Assert.NotNull(headTable); + } + + [Fact] + public void HeadTable_Should_Have_Valid_Version() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + Assert.Equal((ushort)1, headTable.Version.Major); + Assert.Equal((ushort)0, headTable.Version.Minor); + } + + [Fact] + public void HeadTable_Should_Have_Valid_MagicNumber() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + Assert.Equal(0x5F0F3CF5u, headTable.MagicNumber); + } + + [Fact] + public void HeadTable_Should_Have_Valid_UnitsPerEm() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + Assert.Equal(2816, headTable.UnitsPerEm); + } + + [Fact] + public void HeadTable_Should_Have_Valid_BoundingBox() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + Assert.Equal(-2080, headTable.XMin); + Assert.Equal(7274, headTable.XMax); + Assert.Equal(-900, headTable.YMin); + Assert.Equal(3072, headTable.YMax); + } + + [Fact] + public void HeadTable_Should_Have_Valid_IndexToLocFormat() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + Assert.Equal(IndexToLocFormat.Long, headTable.IndexToLocFormat); + } + + [Fact] + public void HeadTable_Should_Have_Valid_GlyphDataFormat() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + Assert.Equal(GlyphDataFormat.Current, headTable.GlyphDataFormat); + } + + [Fact] + public void HeadTable_Should_Have_Valid_LowestRecPPEM() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + Assert.Equal(6, headTable.LowestRecPPEM); + } + + [Fact] + public void HeadTable_Should_Have_Valid_FontRevision() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + Assert.True(headTable.FontRevision.ToFloat() > 0); + } + + [Fact] + public void HeadTable_Should_Have_Valid_Created_Timestamp() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + Assert.True(headTable.Created > new DateTime(1904, 1, 1)); + Assert.True(headTable.Created < DateTime.UtcNow); + } + + [Fact] + public void HeadTable_Should_Have_Valid_Modified_Timestamp() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + Assert.True(headTable.Modified > new DateTime(1904, 1, 1)); + Assert.True(headTable.Modified < DateTime.UtcNow); + } + + [Fact] + public void HeadTable_Should_Have_Valid_Flags() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + + Assert.True(headTable.Flags.HasFlag(HeadFlags.BaselineAtY0)); + } + + [Fact] + public void HeadTable_Should_Have_Valid_FontDirectionHint() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(HeadTable.TryLoad(typeface, out var headTable)); + Assert.Equal(FontDirectionHint.LeftToRightWithNeutrals, headTable.FontDirectionHint); + } + + private class CustomPlatformTypeface : IPlatformTypeface + { + private readonly UnmanagedFontMemory _fontMemory; + + public CustomPlatformTypeface(Stream stream, string fontFamily = "Custom") + { + _fontMemory = UnmanagedFontMemory.LoadFromStream(stream); + FamilyName = fontFamily; + } + + public FontWeight Weight => FontWeight.Normal; + + public FontStyle Style => FontStyle.Normal; + + public FontStretch Stretch => FontStretch.Normal; + + public string FamilyName { get; } + + public FontSimulations FontSimulations => FontSimulations.None; + + public void Dispose() + { + ((IDisposable)_fontMemory).Dispose(); + } + + public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream) + { + var memory = _fontMemory.Memory; + + var handle = memory.Pin(); + stream = new PinnedUnmanagedMemoryStream(handle, memory.Length); + + return true; + } + + private sealed class PinnedUnmanagedMemoryStream : UnmanagedMemoryStream + { + private MemoryHandle _handle; + + public unsafe PinnedUnmanagedMemoryStream(MemoryHandle handle, long length) + : base((byte*)handle.Pointer, length) + { + _handle = handle; + } + + protected override void Dispose(bool disposing) + { + try + { + base.Dispose(disposing); + } + finally + { + _handle.Dispose(); + } + } + } + + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory table) => _fontMemory.TryGetTable(tag, out table); + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/MaxpTableTests.cs b/tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/MaxpTableTests.cs new file mode 100644 index 0000000000..e897bb82ce --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/MaxpTableTests.cs @@ -0,0 +1,233 @@ +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Media.Fonts.Tables; +using Avalonia.Platform; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media.Fonts.Tables +{ + public class MaxpTableTests + { + private static string s_InterFontUri = "resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests"; + + [Fact] + public void Should_Load_MaxpTable_From_Inter_Font() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var maxpTable = MaxpTable.Load(typeface); + + Assert.NotEqual(default, maxpTable); + } + + [Fact] + public void MaxpTable_Should_Have_Valid_NumGlyphs() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var maxpTable = MaxpTable.Load(typeface); + + Assert.Equal(2547, maxpTable.NumGlyphs); + } + + [Fact] + public void MaxpTable_TrueType_Should_Have_Version_1_0() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var maxpTable = MaxpTable.Load(typeface); + + Assert.Equal(1, maxpTable.Version.Major); + Assert.Equal(0, maxpTable.Version.Minor); + } + + [Fact] + public void MaxpTable_Version_1_0_Should_Have_Valid_MaxPoints() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var maxpTable = MaxpTable.Load(typeface); + + Assert.Equal(148, maxpTable.MaxPoints); + } + + [Fact] + public void MaxpTable_Version_1_0_Should_Have_Valid_MaxContours() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var maxpTable = MaxpTable.Load(typeface); + + Assert.Equal(12, maxpTable.MaxContours); + } + + [Fact] + public void MaxpTable_Version_1_0_Should_Have_Valid_MaxZones() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var maxpTable = MaxpTable.Load(typeface); + + Assert.Equal(1, maxpTable.MaxZones); + } + + [Fact] + public void MaxpTable_Should_Have_Valid_MaxCompositePoints() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var maxpTable = MaxpTable.Load(typeface); + + Assert.Equal(112, maxpTable.MaxCompositePoints); + } + + [Fact] + public void MaxpTable_Should_Have_Valid_MaxCompositeContours() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var maxpTable = MaxpTable.Load(typeface); + + Assert.Equal(7, maxpTable.MaxCompositeContours); + } + + [Fact] + public void MaxpTable_Should_Have_Valid_MaxStackElements() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var maxpTable = MaxpTable.Load(typeface); + + Assert.Equal(0, maxpTable.MaxStackElements); + } + + [Fact] + public void MaxpTable_Should_Have_Valid_MaxComponentDepth() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var maxpTable = MaxpTable.Load(typeface); + + Assert.Equal(1, maxpTable.MaxComponentDepth); + } + + [Fact] + public void MaxpTable_NumGlyphs_Should_Match_GlyphTypeface_GlyphCount() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var maxpTable = MaxpTable.Load(typeface); + + Assert.Equal(maxpTable.NumGlyphs, typeface.GlyphCount); + } + + private class CustomPlatformTypeface : IPlatformTypeface + { + private readonly UnmanagedFontMemory _fontMemory; + + public CustomPlatformTypeface(Stream stream, string fontFamily = "Custom") + { + _fontMemory = UnmanagedFontMemory.LoadFromStream(stream); + FamilyName = fontFamily; + } + + public FontWeight Weight => FontWeight.Normal; + + public FontStyle Style => FontStyle.Normal; + + public FontStretch Stretch => FontStretch.Normal; + + public string FamilyName { get; } + + public FontSimulations FontSimulations => FontSimulations.None; + + public void Dispose() + { + ((IDisposable)_fontMemory).Dispose(); + } + + public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream) + { + var memory = _fontMemory.Memory; + + var handle = memory.Pin(); + stream = new PinnedUnmanagedMemoryStream(handle, memory.Length); + + return true; + } + + private sealed class PinnedUnmanagedMemoryStream : UnmanagedMemoryStream + { + private MemoryHandle _handle; + + public unsafe PinnedUnmanagedMemoryStream(MemoryHandle handle, long length) + : base((byte*)handle.Pointer, length) + { + _handle = handle; + } + + protected override void Dispose(bool disposing) + { + try + { + base.Dispose(disposing); + } + finally + { + _handle.Dispose(); + } + } + } + + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory table) => _fontMemory.TryGetTable(tag, out table); + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/OS2TableTests.cs b/tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/OS2TableTests.cs new file mode 100644 index 0000000000..b52c710739 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/OS2TableTests.cs @@ -0,0 +1,233 @@ +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Media.Fonts.Tables; +using Avalonia.Platform; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media.Fonts.Tables +{ + public class OS2TableTests + { + private static string s_InterFontUri = "resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests"; + + [Fact] + public void Should_Load_OS2Table_From_Inter_Font() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var loaded = OS2Table.TryLoad(typeface, out var os2Table); + + Assert.True(loaded); + } + + [Fact] + public void OS2Table_Should_Have_Valid_WeightClass() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var loaded = OS2Table.TryLoad(typeface, out var os2Table); + + Assert.True(loaded); + Assert.Equal(400, os2Table.WeightClass); + } + + [Fact] + public void OS2Table_Should_Have_Valid_WidthClass() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var loaded = OS2Table.TryLoad(typeface, out var os2Table); + + Assert.True(loaded); + Assert.Equal(5, os2Table.WidthClass); + } + + [Fact] + public void OS2Table_Should_Have_Valid_TypoMetrics() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var loaded = OS2Table.TryLoad(typeface, out var os2Table); + + Assert.True(loaded); + Assert.Equal(2728, os2Table.TypoAscender); + Assert.Equal(-680, os2Table.TypoDescender); + Assert.True(os2Table.TypoAscender > os2Table.TypoDescender); + } + + [Fact] + public void OS2Table_Should_Have_Valid_WinMetrics() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var loaded = OS2Table.TryLoad(typeface, out var os2Table); + + Assert.True(loaded); + Assert.Equal(2728, os2Table.WinAscent); + Assert.Equal(680, os2Table.WinDescent); + } + + [Fact] + public void OS2Table_Should_Have_Valid_StrikeoutMetrics() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var loaded = OS2Table.TryLoad(typeface, out var os2Table); + + Assert.True(loaded); + Assert.Equal(192, os2Table.StrikeoutSize); + } + + [Fact] + public void OS2Table_Inter_Regular_Should_Be_Regular() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var loaded = OS2Table.TryLoad(typeface, out var os2Table); + + Assert.True(loaded); + Assert.True(os2Table.Selection.HasFlag(OS2Table.FontSelectionFlags.REGULAR)); + } + + [Fact] + public void OS2Table_Should_Have_Consistent_Ascent_Values() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var loaded = OS2Table.TryLoad(typeface, out var os2Table); + + Assert.True(loaded); + Assert.Equal(2728, os2Table.TypoAscender); + Assert.Equal(2728, os2Table.WinAscent); + } + + [Fact] + public void OS2Table_Should_Have_Consistent_Descent_Values() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var loaded = OS2Table.TryLoad(typeface, out var os2Table); + + Assert.True(loaded); + Assert.Equal(-680, os2Table.TypoDescender); + Assert.Equal(680, os2Table.WinDescent); + } + + [Fact] + public void OS2Table_Should_Have_Valid_Panose() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var loaded = OS2Table.TryLoad(typeface, out var os2Table); + + Assert.True(loaded); + var panose = os2Table.Panose; + Assert.Equal(PanoseFamilyKind.LatinText, panose.FamilyKind); + } + + private class CustomPlatformTypeface : IPlatformTypeface + { + private readonly UnmanagedFontMemory _fontMemory; + + public CustomPlatformTypeface(Stream stream, string fontFamily = "Custom") + { + _fontMemory = UnmanagedFontMemory.LoadFromStream(stream); + FamilyName = fontFamily; + } + + public FontWeight Weight => FontWeight.Normal; + + public FontStyle Style => FontStyle.Normal; + + public FontStretch Stretch => FontStretch.Normal; + + public string FamilyName { get; } + + public FontSimulations FontSimulations => FontSimulations.None; + + public void Dispose() + { + ((IDisposable)_fontMemory).Dispose(); + } + + public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream) + { + var memory = _fontMemory.Memory; + + var handle = memory.Pin(); + stream = new PinnedUnmanagedMemoryStream(handle, memory.Length); + + return true; + } + + private sealed class PinnedUnmanagedMemoryStream : UnmanagedMemoryStream + { + private MemoryHandle _handle; + + public unsafe PinnedUnmanagedMemoryStream(MemoryHandle handle, long length) + : base((byte*)handle.Pointer, length) + { + _handle = handle; + } + + protected override void Dispose(bool disposing) + { + try + { + base.Dispose(disposing); + } + finally + { + _handle.Dispose(); + } + } + } + + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory table) => _fontMemory.TryGetTable(tag, out table); + } + } +} 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 0000000000..c324102d7c --- /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 ca573fae90..737594e7af 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -180,9 +180,7 @@ namespace Avalonia.Base.UnitTests.Media glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]); } - return new GlyphRun( - new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, - FontStretch.Normal), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel); + return new GlyphRun(Typeface.Default.GlyphTypeface, 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel); } private static IDisposable Start() diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs new file mode 100644 index 0000000000..7f629c8e69 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs @@ -0,0 +1,446 @@ +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Platform; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media +{ + public class GlyphTypefaceTests + { + private static string s_InterFontUri = "resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests"; + + [Fact] + public void Should_Load_Inter_Font() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.Equal("Inter", typeface.FamilyName); + } + + [Fact] + public void Should_Have_CharacterToGlyphMap_For_Common_Characters() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var map = typeface.CharacterToGlyphMap; + + Assert.NotNull(map); + + Assert.True(map.ContainsGlyph('A')); + Assert.True(map['A'] != 0); + + Assert.True(map.ContainsGlyph('a')); + Assert.True(map['a'] != 0); + + Assert.True(map.ContainsGlyph(' ')); + Assert.True(map[' '] != 0); + } + + [Fact] + public void GetGlyphAdvance_Should_Return_Advance_For_GlyphId() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var map = typeface.CharacterToGlyphMap; + + Assert.True(map.ContainsGlyph('A')); + + var glyphId = map['A']; + + // Ensure metrics are available for this glyph + Assert.True(typeface.TryGetGlyphMetrics(glyphId, out var metrics)); + + // Ensure advance can be retrieved + Assert.True(typeface.TryGetHorizontalGlyphAdvance(glyphId, out var advance)); + + // Advance returned by GetGlyphAdvance should match the metrics width + Assert.Equal(metrics.Width, advance); + } + + [Fact] + public void Should_Have_Valid_FontMetrics() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var metrics = typeface.Metrics; + + Assert.True(metrics.DesignEmHeight > 0); + Assert.True(metrics.Ascent != 0); + Assert.True(metrics.Descent != 0); + Assert.True(metrics.LineSpacing > 0); + } + + [Fact] + public void Should_Have_Positive_GlyphCount() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(typeface.GlyphCount > 0); + } + + [Fact] + public void Should_Have_Correct_Font_Properties() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.Equal(FontWeight.Normal, typeface.Weight); + Assert.Equal(FontStyle.Normal, typeface.Style); + Assert.Equal(FontStretch.Normal, typeface.Stretch); + Assert.Equal(FontSimulations.None, typeface.FontSimulations); + } + + [Fact] + public void Should_Apply_Bold_Simulation() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream), FontSimulations.Bold); + + Assert.Equal(FontWeight.Bold, typeface.Weight); + Assert.Equal(FontSimulations.Bold, typeface.FontSimulations); + } + + [Fact] + public void Should_Apply_Oblique_Simulation() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream), FontSimulations.Oblique); + + Assert.Equal(FontStyle.Italic, typeface.Style); + Assert.Equal(FontSimulations.Oblique, typeface.FontSimulations); + } + + [Fact] + public void Should_Apply_Combined_Simulations() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream), + FontSimulations.Bold | FontSimulations.Oblique); + + Assert.Equal(FontWeight.Bold, typeface.Weight); + Assert.Equal(FontStyle.Italic, typeface.Style); + Assert.Equal(FontSimulations.Bold | FontSimulations.Oblique, typeface.FontSimulations); + } + + [Fact] + public void Should_Have_TypographicFamilyName() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.NotNull(typeface.TypographicFamilyName); + } + + [Fact] + public void Should_Have_FamilyNames_Dictionary() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.NotNull(typeface.FamilyNames); + Assert.NotEmpty(typeface.FamilyNames); + } + + [Fact] + public void Should_Have_FaceNames_Dictionary() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.NotNull(typeface.FaceNames); + Assert.NotEmpty(typeface.FaceNames); + } + + [Fact] + public void Should_Have_SupportedFeatures() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var features = typeface.SupportedFeatures; + + Assert.NotEmpty(features); + } + + [Fact] + public void Should_Cache_SupportedFeatures() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var features1 = typeface.SupportedFeatures; + var features2 = typeface.SupportedFeatures; + + Assert.Same(features1, features2); + } + + [Fact] + public void TryGetGlyphAdvance_Should_Return_False_For_Invalid_GlyphId() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.False(typeface.TryGetHorizontalGlyphAdvance(ushort.MaxValue, out var advance)); + } + + [Fact] + public void TryGetGlyphMetrics_Should_Return_False_For_Invalid_GlyphId() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var result = typeface.TryGetGlyphMetrics(ushort.MaxValue, out var metrics); + + Assert.False(result); + Assert.Equal(default, metrics); + } + + [Fact] + public void TryGetGlyphMetrics_Should_Return_Valid_Metrics() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var map = typeface.CharacterToGlyphMap; + Assert.True(map.ContainsGlyph('A')); + + var glyphId = map['A']; + var result = typeface.TryGetGlyphMetrics(glyphId, out var metrics); + + Assert.True(result); + Assert.True(metrics.Width > 0); + } + + [Fact] + public void Should_Have_Valid_PlatformTypeface() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var platformTypeface = new CustomPlatformTypeface(stream); + var typeface = new GlyphTypeface(platformTypeface); + + Assert.NotNull(typeface.PlatformTypeface); + Assert.Same(platformTypeface, typeface.PlatformTypeface); + } + + [Fact] + public void Should_Dispose_Properly() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + typeface.Dispose(); + + // Should not throw on double dispose + typeface.Dispose(); + } + + [Fact] + public void CharacterToGlyphMap_Should_Have_Different_Glyphs_For_Different_Characters() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var map = typeface.CharacterToGlyphMap; + + Assert.True(map.ContainsGlyph('A')); + Assert.True(map.ContainsGlyph('B')); + + var glyphA = map['A']; + var glyphB = map['B']; + + Assert.NotEqual(glyphA, glyphB); + } + + [Fact] + public void FontMetrics_LineSpacing_Should_Be_Calculated_Correctly() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var metrics = typeface.Metrics; + + var expectedLineSpacing = metrics.Descent - metrics.Ascent + metrics.LineGap; + + Assert.Equal(expectedLineSpacing, metrics.LineSpacing); + } + + [Fact] + public void Should_Support_Multiple_Characters_In_CharacterToGlyphMap() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var map = typeface.CharacterToGlyphMap; + + var testCharacters = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + + foreach (var ch in testCharacters) + { + Assert.True(map.ContainsGlyph(ch), $"Character '{ch}' not found in glyph map"); + } + } + + [Fact] + public void FamilyNames_Should_Contain_InvariantCulture_Entry() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(typeface.FamilyNames.ContainsKey(CultureInfo.InvariantCulture) || + typeface.FamilyNames.Count > 0); + } + + [Fact] + public void FaceNames_Should_Contain_InvariantCulture_Entry() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_InterFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + Assert.True(typeface.FaceNames.ContainsKey(CultureInfo.InvariantCulture) || + typeface.FaceNames.Count > 0); + } + + private class CustomPlatformTypeface : IPlatformTypeface + { + private readonly UnmanagedFontMemory _fontMemory; + + public CustomPlatformTypeface(Stream stream, string fontFamily = "Custom") + { + _fontMemory = UnmanagedFontMemory.LoadFromStream(stream); + FamilyName = fontFamily; + } + + public FontWeight Weight => FontWeight.Normal; + + public FontStyle Style => FontStyle.Normal; + + public FontStretch Stretch => FontStretch.Normal; + + public string FamilyName { get; } + + public FontSimulations FontSimulations => FontSimulations.None; + + public void Dispose() + { + ((IDisposable)_fontMemory).Dispose(); + } + + public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream) + { + var memory = _fontMemory.Memory; + + var handle = memory.Pin(); + stream = new PinnedUnmanagedMemoryStream(handle, memory.Length); + + return true; + } + + private sealed class PinnedUnmanagedMemoryStream : UnmanagedMemoryStream + { + private MemoryHandle _handle; + + public unsafe PinnedUnmanagedMemoryStream(MemoryHandle handle, long length) + : base((byte*)handle.Pointer, length) + { + _handle = handle; + } + + protected override void Dispose(bool disposing) + { + try + { + base.Dispose(disposing); + } + finally + { + _handle.Dispose(); + } + } + } + + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory table) => _fontMemory.TryGetTable(tag, out table); + } + } +} diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index aae46782c9..9406aaf3f7 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index bc90abad7e..da2519188e 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 class HugeTextLayout : IDisposable 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 94324491fd..b4e0a055e3 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -10,6 +10,7 @@ using Xunit; using System.Collections.ObjectModel; using System.Reactive.Subjects; using Avalonia.Headless; +using Avalonia.Harfbuzz; using Avalonia.Input; using Avalonia.Platform; using Moq; @@ -367,7 +368,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(textbox.Text, control.Text); }); } - + [Fact] public void Custom_TextSelector() { @@ -386,7 +387,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString())); }); } - + [Fact] public void Custom_ItemSelector() { @@ -1272,7 +1273,7 @@ namespace Avalonia.Controls.UnitTests 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 30cfa68c7e..75e15a71d8 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -15,6 +15,7 @@ + diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index e8a8e75e66..fc0c64d5d9 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Platform; using Avalonia.Threading; @@ -274,7 +275,7 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => 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 a625634ac7..a15b8d8c77 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1817,6 +1817,7 @@ namespace Avalonia.Controls.UnitTests [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 @@ -1877,6 +1878,7 @@ namespace Avalonia.Controls.UnitTests { [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 79e93603d9..85d366953f 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Layout; @@ -1239,7 +1240,7 @@ namespace Avalonia.Controls.UnitTests keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), assetLoader: new StandardAssetLoader())); } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 1a02f20057..c76369ef0b 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -1216,10 +1216,10 @@ namespace Avalonia.Controls.UnitTests 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 ca4e33c3fd..4ff22cf402 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -6,6 +6,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Platform; @@ -926,13 +927,13 @@ namespace Avalonia.Controls.UnitTests 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 4082bb47a1..f0f4fe05ae 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.Primitives; using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Layout; @@ -1352,7 +1353,7 @@ namespace Avalonia.Controls.UnitTests.Primitives keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), assetLoader: new StandardAssetLoader())); } diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index 3de0359c5e..2df686bbe8 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 { @@ -74,7 +72,7 @@ namespace Avalonia.Controls.UnitTests var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.Width, textLayout.Height), 1); - Assert.Equal(textBlock.DesiredSize, constraint); + Assert.Equal(constraint, textBlock.DesiredSize); } } @@ -378,13 +376,13 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(target.DesiredSize)); Assert.True(button.IsMeasureValid); - Assert.Equal(80, button.DesiredSize.Width); + Assert.Equal(58, button.DesiredSize.Width); target.Arrange(new Rect(new Size(200, 50))); Assert.True(button.IsArrangeValid); - Assert.Equal(60, button.Bounds.Left); + Assert.Equal(43, button.Bounds.Left); } } @@ -498,7 +496,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - Assert.Equal(target.DesiredSize, new Size(40, 10)); + Assert.Equal(new Size(28, 15), target.DesiredSize); } [Fact] @@ -510,7 +508,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - Assert.Equal(target.DesiredSize, new Size(44, 14)); + Assert.Equal(new Size(32, 19), target.DesiredSize); } [Fact] @@ -522,7 +520,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - Assert.Equal(target.DesiredSize, new Size(40, 9.6)); + Assert.Equal(new Size(27.954545454545453, 14.522727272727273), target.DesiredSize); } [Fact] @@ -535,7 +533,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); target.Arrange(new Rect(default, target.DesiredSize)); - Assert.Equal(target.Bounds, new Rect(0, 0, 40, 9.6)); + Assert.Equal(new Rect(0, 0, 27.954545454545453, 14.522727272727273), target.Bounds); } [Fact] @@ -547,7 +545,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - Assert.Equal(target.DesiredSize, new Size(44.5, 14.1)); + Assert.Equal(new Size(32.45454545454545, 19.022727272727273), target.DesiredSize); } [Fact] @@ -560,7 +558,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); target.Arrange(new Rect(default, target.DesiredSize)); - Assert.Equal(target.Bounds, new Rect(0, 0, 44.5, 14.1)); + Assert.Equal(new Rect(0, 0, 32.45454545454545, 19.022727272727273), target.Bounds); } private class TestTextBlock : TextBlock diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 42e1033b6d..3e830859a4 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Platform; @@ -2153,14 +2154,14 @@ namespace Avalonia.Controls.UnitTests keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), standardCursorFactory: Mock.Of(), - textShaperImpl: new HeadlessTextShaperStub(), - fontManagerImpl: new HeadlessFontManagerStub()); + textShaperImpl: new HarfBuzzTextShaper(), + fontManagerImpl: new TestFontManager()); private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub(), - fontManagerImpl: new HeadlessFontManagerStub(), + textShaperImpl: new HarfBuzzTextShaper(), + fontManagerImpl: new TestFontManager(), assetLoader: new StandardAssetLoader()); internal static IControlTemplate CreateTemplate() diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index 752e588b38..77dad85e03 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.MarkupExtensions; @@ -150,7 +151,7 @@ namespace Avalonia.Controls.UnitTests 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 5877b6edfc..11c52392f1 100644 --- a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Platform; using Avalonia.Threading; @@ -292,7 +293,7 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => 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 b604ad2cfc..8db2702e35 100644 --- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Avalonia.Animation; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Layout; using Avalonia.Platform; @@ -327,7 +328,7 @@ namespace Avalonia.Controls.UnitTests TestServices.MockThreadingInterface.With( fontManagerImpl: new HeadlessFontManagerStub(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), assetLoader: new StandardAssetLoader())); } diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 6ba7255a27..bf35bc117d 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Harfbuzz; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Platform; @@ -1852,7 +1853,7 @@ namespace Avalonia.Controls.UnitTests keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), renderInterface: new HeadlessPlatformRenderInterface(), - textShaperImpl: new HeadlessTextShaperStub(), + textShaperImpl: new HarfBuzzTextShaper(), assetLoader: new StandardAssetLoader())); } diff --git a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj index bd476849ac..2baca8435d 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.PerAssembly.UnitTests/Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj b/tests/Avalonia.Headless.NUnit.PerAssembly.UnitTests/Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj index 8be738c356..a701fcefe8 100644 --- a/tests/Avalonia.Headless.NUnit.PerAssembly.UnitTests/Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj +++ b/tests/Avalonia.Headless.NUnit.PerAssembly.UnitTests/Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj @@ -26,6 +26,7 @@ + diff --git a/tests/Avalonia.Headless.NUnit.PerTest.UnitTests/Avalonia.Headless.NUnit.PerTest.UnitTests.csproj b/tests/Avalonia.Headless.NUnit.PerTest.UnitTests/Avalonia.Headless.NUnit.PerTest.UnitTests.csproj index 8be738c356..a701fcefe8 100644 --- a/tests/Avalonia.Headless.NUnit.PerTest.UnitTests/Avalonia.Headless.NUnit.PerTest.UnitTests.csproj +++ b/tests/Avalonia.Headless.NUnit.PerTest.UnitTests/Avalonia.Headless.NUnit.PerTest.UnitTests.csproj @@ -26,6 +26,7 @@ + diff --git a/tests/Avalonia.Headless.UnitTests/TestApplication.cs b/tests/Avalonia.Headless.UnitTests/TestApplication.cs index ae923bf52e..db23c521d4 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 class TestApplication : Application } public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseHarfBuzz() .UseSkia() .UseHeadless(new AvaloniaHeadlessPlatformOptions { diff --git a/tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj b/tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj index ed0dbe6273..e638032870 100644 --- a/tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj +++ b/tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj @@ -20,6 +20,7 @@ + diff --git a/tests/Avalonia.Headless.XUnit.PerTest.UnitTests/Avalonia.Headless.XUnit.PerTest.UnitTests.csproj b/tests/Avalonia.Headless.XUnit.PerTest.UnitTests/Avalonia.Headless.XUnit.PerTest.UnitTests.csproj index ed0dbe6273..e638032870 100644 --- a/tests/Avalonia.Headless.XUnit.PerTest.UnitTests/Avalonia.Headless.XUnit.PerTest.UnitTests.csproj +++ b/tests/Avalonia.Headless.XUnit.PerTest.UnitTests/Avalonia.Headless.XUnit.PerTest.UnitTests.csproj @@ -20,6 +20,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 739da81465..ce33d93e09 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -13,6 +13,7 @@ + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs index 6ad6944fa6..63c7e2eaf6 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -134,10 +134,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public object? Foo { get; } = null; } - private static IDisposable StyledWindow(params (string, string)[] assets) + private static IDisposable StyledWindow() { var services = TestServices.StyledWindow.With( - assetLoader: new MockAssetLoader(assets), theme: () => new Styles { WindowStyle(), diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index 2853a28ff7..7161a92552 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -961,10 +961,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions window.Resources.Clear(); } - private IDisposable StyledWindow(params (string, string)[] assets) + private IDisposable StyledWindow() { var services = TestServices.StyledWindow.With( - assetLoader: new MockAssetLoader(assets), theme: () => new Styles { WindowStyle(), diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs index ec5acc4066..44ce0ef909 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs @@ -583,7 +583,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions private static IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( - assetLoader: new MockAssetLoader(assets), theme: () => new Styles { WindowStyle(), diff --git a/tests/Avalonia.RenderTests/Assets/NotoSansDeseret-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoSansDeseret-Regular.ttf new file mode 100644 index 0000000000..0d55a52256 Binary files /dev/null and b/tests/Avalonia.RenderTests/Assets/NotoSansDeseret-Regular.ttf differ diff --git a/tests/Avalonia.RenderTests/Assets/NotoSansTamil-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoSansTamil-Regular.ttf new file mode 100644 index 0000000000..1776798018 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 b8398ea09e..b28762d08d 100644 --- a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -16,7 +16,7 @@ namespace Avalonia.Skia.RenderTests : base(@"Media\GlyphRun") { } - + [Fact] public async Task Should_Render_GlyphRun_Geometry() { @@ -136,7 +136,7 @@ namespace Avalonia.Skia.RenderTests { 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' }; @@ -145,7 +145,7 @@ namespace Avalonia.Skia.RenderTests Geometry = glyphRun.BuildGeometry(); } - public Geometry Geometry { get; } + public Geometry Geometry { get; } public override void Render(DrawingContext context) { @@ -161,7 +161,7 @@ namespace Avalonia.Skia.RenderTests { 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' }; @@ -184,17 +184,19 @@ namespace Avalonia.Skia.RenderTests { 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; - var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[0]) * scale; + glyphTypeface.TryGetHorizontalGlyphAdvance(glyphIndices[0], out var advance); + + var glyphAdvance = advance * scale; var glyphInfos = new[] { - new GlyphInfo(glyphIndices[0], 0, advance), - new GlyphInfo(glyphIndices[1], 1, advance), - new GlyphInfo(glyphIndices[2], 2, advance) + new GlyphInfo(glyphIndices[0], 0, glyphAdvance), + new GlyphInfo(glyphIndices[1], 1, glyphAdvance), + new GlyphInfo(glyphIndices[2], 2, glyphAdvance) }; var characters = new[] { 'A', 'B', 'C' }; diff --git a/tests/Avalonia.RenderTests/TestRenderHelper.cs b/tests/Avalonia.RenderTests/TestRenderHelper.cs index f77e58e7d5..48a63bc716 100644 --- a/tests/Avalonia.RenderTests/TestRenderHelper.cs +++ b/tests/Avalonia.RenderTests/TestRenderHelper.cs @@ -21,6 +21,7 @@ using Avalonia.UnitTests; using Avalonia.Utilities; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; +using Avalonia.Harfbuzz; using Avalonia.Skia; namespace Avalonia.Skia.RenderTests; @@ -38,6 +39,7 @@ static class 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 c240dacf9e..3376aaef27 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -1,15 +1,13 @@ -#nullable enable - -using System; +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 { @@ -28,7 +26,7 @@ namespace Avalonia.Skia.UnitTests.Media { get { - if(_systemFonts is null) + if (_systemFonts is null) { var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"); @@ -82,42 +80,46 @@ namespace Avalonia.Skia.UnitTests.Media } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - string? familyName, CultureInfo? culture, out Typeface typeface) + string? familyName, CultureInfo? culture, out IPlatformTypeface platformTypeface) { - if(SystemFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) + if (SystemFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out var glyphTypeface)) { + platformTypeface = glyphTypeface.GlyphTypeface.PlatformTypeface; + return true; } - var fallback = SKFontManager.Default.MatchCharacter(null, (SKFontStyleWeight)fontWeight, + var fallback = SKFontManager.Default.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight, (SKFontStyleWidth)fontStretch, (SKFontStyleSlant)fontStyle, _bcp47, codepoint); - typeface = new Typeface(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); + platformTypeface = new SkiaTypeface(fallback, FontSimulations.None); return true; } public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + FontStretch stretch, [NotNullWhen(true)] out IPlatformTypeface platformTypeface) { - if (SystemFonts.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + if (SystemFonts.TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface)) { + platformTypeface = glyphTypeface.PlatformTypeface; + return true; } var skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)style); - glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + platformTypeface = new SkiaTypeface(skTypeface, FontSimulations.None); return true; } - public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IPlatformTypeface platformTypeface) { var skTypeface = SKTypeface.FromStream(stream); - glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations); + platformTypeface = new SkiaTypeface(skTypeface, fontSimulations); return true; } @@ -126,5 +128,31 @@ namespace Avalonia.Skia.UnitTests.Media { _systemFonts?.Dispose(); } + + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + { + if (SystemFonts.TryGetFamilyTypefaces(familyName, out familyTypefaces)) + { + return true; + } + + var set = SKFontManager.Default.GetFontStyles(familyName); + + if (set.Count == 0) + { + return false; + } + + var typefaces = new List(set.Count); + + foreach (var fontStyle in set) + { + typefaces.Add(new Typeface(familyName, fontStyle.Slant.ToAvalonia(), (FontWeight)fontStyle.Weight, (FontStretch)fontStyle.Width)); + } + + familyTypefaces = typefaces; + + return true; + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs index ef55f1d6df..87a2b867d2 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs @@ -82,11 +82,7 @@ namespace Avalonia.Skia.UnitTests.Media Assert.Equal("Manrope Light", glyphTypeface.FamilyName); - Assert.True(glyphTypeface is IGlyphTypeface2); - - var glyphTypeface2 = (IGlyphTypeface2)glyphTypeface; - - Assert.Equal("Manrope", glyphTypeface2.TypographicFamilyName); + Assert.Equal("Manrope", glyphTypeface.TypographicFamilyName); } } @@ -143,14 +139,14 @@ namespace Avalonia.Skia.UnitTests.Media _createSyntheticTypefaces = createSyntheticTypefaces; } - public IDictionary> GlyphTypefaceCache => _glyphTypefaceCache; + public IDictionary> GlyphTypefaceCache => _glyphTypefaceCache; public override bool TryCreateSyntheticGlyphTypeface( - IGlyphTypeface glyphTypeface, + GlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, - [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface) + [NotNullWhen(true)] out GlyphTypeface? syntheticGlyphTypeface) { if (!_createSyntheticTypefaces) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs index 9aae98efe9..494fa4462d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs @@ -45,7 +45,7 @@ namespace Avalonia.Skia.UnitTests.Media { } - public IDictionary> GlyphTypefaceCache => _glyphTypefaceCache; + public IDictionary> GlyphTypefaceCache => _glyphTypefaceCache; } [Fact] @@ -126,11 +126,11 @@ namespace Avalonia.Skia.UnitTests.Media } public override bool TryCreateSyntheticGlyphTypeface( - IGlyphTypeface glyphTypeface, + GlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, - [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface) + [NotNullWhen(true)] out GlyphTypeface? syntheticGlyphTypeface) { syntheticGlyphTypeface = null; @@ -138,7 +138,7 @@ namespace Avalonia.Skia.UnitTests.Media { foreach (var ignorable in _ignorables) { - if (glyphTypeface.FamilyName == ignorable.Name || glyphTypeface is IGlyphTypeface2 glyphTypeface2 && glyphTypeface2.TypographicFamilyName == ignorable.Name) + if (glyphTypeface.FamilyName == ignorable.Name || glyphTypeface.TypographicFamilyName == ignorable.Name) { return false; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index b30b023f58..994edc6095 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -95,18 +95,20 @@ namespace Avalonia.Skia.UnitTests.Media [Fact] public void Should_Only_Try_To_Create_GlyphTypeface_Once() { - var fontManagerImpl = new HeadlessFontManagerStub(); + var fontManagerImpl = new TestFontManager(); using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: fontManagerImpl))) { 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); } } @@ -311,13 +313,18 @@ namespace Avalonia.Skia.UnitTests.Media new Uri(s_fontUri, UriKind.Absolute))); Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Noto Mono", FontStyle.Italic, FontWeight.Bold), - out var glyphTypeface)); + out var italicBoldTypeface)); - Assert.Equal("Noto Mono", glyphTypeface.FamilyName); + Assert.Equal("Noto Mono", italicBoldTypeface.FamilyName); - Assert.Equal(FontWeight.Bold, glyphTypeface.Weight); + Assert.True(italicBoldTypeface.PlatformTypeface.FontSimulations.HasFlag(FontSimulations.Bold)); - Assert.Equal(FontStyle.Italic, glyphTypeface.Style); + Assert.True(italicBoldTypeface.PlatformTypeface.FontSimulations.HasFlag(FontSimulations.Oblique)); + + Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Noto Mono", FontStyle.Normal, FontWeight.Normal), + out var regularTypeface)); + + Assert.NotEqual(((SkiaTypeface)regularTypeface.PlatformTypeface).SKTypeface, ((SkiaTypeface)italicBoldTypeface.PlatformTypeface).SKTypeface); } } } @@ -351,7 +358,7 @@ namespace Avalonia.Skia.UnitTests.Media Assert.Equal("Inter", glyphTypeface.FamilyName); - var features = ((IGlyphTypeface2)glyphTypeface).SupportedFeatures; + var features = glyphTypeface.SupportedFeatures; Assert.NotEmpty(features); } @@ -469,7 +476,7 @@ namespace Avalonia.Skia.UnitTests.Media Assert.Equal(FontStyle.Normal, regularTypeface.Style); - Assert.NotEqual(((GlyphTypefaceImpl)italicTypeface.GlyphTypeface).SKTypeface, ((GlyphTypefaceImpl)regularTypeface.GlyphTypeface).SKTypeface); + Assert.NotEqual(((SkiaTypeface)italicTypeface.GlyphTypeface.PlatformTypeface).SKTypeface, ((SkiaTypeface)regularTypeface.GlyphTypeface.PlatformTypeface).SKTypeface); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index ab469d7d1b..1882ef9ccb 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -474,7 +474,6 @@ namespace Avalonia.Skia.UnitTests.Media { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(renderInterface: new PlatformRenderInterface(), - textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); return disposable; 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 0000000000..524267ed6c --- /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 3e2c12a912..182c633418 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -476,12 +476,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var formatter = new TextFormatterImpl(); - var glyph = typeface.GlyphTypeface.GetGlyph('a'); + var glyph = typeface.GlyphTypeface.CharacterToGlyphMap['a']; - var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) * - (12.0 / typeface.GlyphTypeface.Metrics.DesignEmHeight); + typeface.GlyphTypeface.TryGetHorizontalGlyphAdvance(glyph, out var advance); - var paragraphWidth = advance * numberOfCharactersPerLine; + var scale = 12.0 / typeface.GlyphTypeface.Metrics.DesignEmHeight; + + var paragraphWidth = advance * scale * numberOfCharactersPerLine; var currentPosition = 0; @@ -1350,8 +1351,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(), - textShaperImpl: new TextShaperImpl())); + .With(renderInterface: new PlatformRenderInterface())); var customFontManagerImpl = new CustomFontManagerImpl(); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 8622b52c6d..ed5947690f 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -571,7 +571,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(7, textRun.Length); - var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint); + var replacementGlyph = Typeface.Default.GlyphTypeface.CharacterToGlyphMap[Codepoint.ReplacementCodepoint]; foreach (var glyphInfo in textRun.GlyphRun.GlyphInfos) { @@ -1194,7 +1194,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var typeFace = new Typeface("Courier New"); + const string monospaceFont = "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"; + + var typeFace = new Typeface(monospaceFont); + + var glyphTypeface = typeFace.GlyphTypeface; + var textLayout0 = new TextLayout("aaaa", typeFace, 12.0, Brushes.White); Assert.Equal(textLayout0.WidthIncludingTrailingWhitespace, textLayout0.Width); @@ -1222,7 +1227,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(renderInterface: new PlatformRenderInterface(null), - textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); return disposable; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 1ac54721c4..fa2884bacc 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -2521,7 +2521,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(renderInterface: new PlatformRenderInterface(null), - textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); return disposable; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 5280f1790f..7ce98a6bde 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -117,7 +117,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(renderInterface: new PlatformRenderInterface(null), - textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); return disposable; diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 7076b1afc7..66e0fb0719 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/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs deleted file mode 100644 index d96c7e7a42..0000000000 --- a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs +++ /dev/null @@ -1,78 +0,0 @@ -#nullable enable - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Linq; -using Avalonia.Media; -using Avalonia.Platform; - -namespace Avalonia.UnitTests -{ - public class HarfBuzzFontManagerImpl : 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 = - new Typeface(new FontFamily("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Sans")); - 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") - { - _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; - _defaultFamilyName = defaultFamilyName; - } - - public string GetDefaultFontFamilyName() - { - return _defaultFamilyName; - } - - string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) - { - return _customTypefaces.Select(x => x.FontFamily.Name).ToArray(); - } - - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey) - { - foreach (var customTypeface in _customTypefaces) - { - var glyphTypeface = customTypeface.GlyphTypeface; - - if (!glyphTypeface.TryGetGlyph((uint)codepoint, out _)) - { - continue; - } - - fontKey = customTypeface; - - return true; - } - - fontKey = default; - - return false; - } - - public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) - { - glyphTypeface = new HarfBuzzGlyphTypefaceImpl(stream); - - return true; - } - - public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) - { - glyphTypeface = null; - - return false; - } - } -} diff --git a/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs deleted file mode 100644 index 42736141ad..0000000000 --- a/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -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, [NotNullWhen(true)] 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 eec577f5c8..0000000000 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ /dev/null @@ -1,179 +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 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/HeadlessFontManagerImpl.cs b/tests/Avalonia.UnitTests/HeadlessFontManagerImpl.cs new file mode 100644 index 0000000000..e419e286f2 --- /dev/null +++ b/tests/Avalonia.UnitTests/HeadlessFontManagerImpl.cs @@ -0,0 +1,135 @@ +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 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 = + new Typeface(new FontFamily("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Sans")); + private static readonly Typeface _emojiTypeface = + new Typeface(new FontFamily("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Twitter Color Emoji")); + + public HeadlessFontManagerImpl(string defaultFamilyName = "Noto Mono") + { + _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; + _defaultFamilyName = defaultFamilyName; + } + + public string GetDefaultFontFamilyName() + { + return _defaultFamilyName; + } + + string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) + { + return _customTypefaces.Select(x => x.FontFamily!.Name).ToArray(); + } + + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, + FontStretch fontStretch, string? familyName, CultureInfo? culture, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + foreach (var customTypeface in _customTypefaces) + { + var glyphTypeface = customTypeface.GlyphTypeface; + + if (!glyphTypeface.CharacterToGlyphMap.TryGetGlyph(codepoint, out _)) + { + continue; + } + + platformTypeface = glyphTypeface.PlatformTypeface; + + return true; + } + + platformTypeface = null; + + return false; + } + + public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + platformTypeface = new HeadlessPlatformTypeface(stream); + + return true; + } + + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + platformTypeface = null; + + // Search through custom typefaces for matching family name and style + foreach (var customTypeface in _customTypefaces) + { + var glyphTypeface = customTypeface.GlyphTypeface; + + // Check if family name matches + if (!string.Equals(glyphTypeface.FamilyName, familyName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Check if style properties match (exact match preferred, but any match is acceptable) + var styleMatches = glyphTypeface.Style == style; + var weightMatches = glyphTypeface.Weight == weight; + var stretchMatches = glyphTypeface.Stretch == stretch; + + // Exact match - return immediately + if (styleMatches && weightMatches && stretchMatches) + { + platformTypeface = glyphTypeface.PlatformTypeface; + return true; + } + + // If family matches but style doesn't, keep searching + // but remember first family match as fallback + if (platformTypeface == null) + { + platformTypeface = glyphTypeface.PlatformTypeface; + } + } + + // Return true if we found at least a family match (even if style doesn't match exactly) + return platformTypeface != null; + } + + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + { + // Find all typefaces that belong to the specified family + var typefaces = new List(); + + foreach (var customTypeface in _customTypefaces) + { + var glyphTypeface = customTypeface.GlyphTypeface; + + if (string.Equals(glyphTypeface.FamilyName, familyName, StringComparison.OrdinalIgnoreCase)) + { + typefaces.Add(customTypeface); + } + } + + if (typefaces.Count > 0) + { + familyTypefaces = typefaces; + return true; + } + + familyTypefaces = null; + return false; + } + } +} diff --git a/tests/Avalonia.UnitTests/TestFontManager.cs b/tests/Avalonia.UnitTests/TestFontManager.cs new file mode 100644 index 0000000000..9b0dbf4eb5 --- /dev/null +++ b/tests/Avalonia.UnitTests/TestFontManager.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using Avalonia.Headless; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.UnitTests; + +public class TestFontManager : IFontManagerImpl +{ + private readonly string _interFontUri = "avares://Avalonia.Fonts.Inter/Assets/Inter-Regular.ttf"; + private readonly string _defaultFamilyName = "avares://Avalonia.Fonts.Inter/Assets#Inter"; + + public int TryCreateGlyphTypefaceCount { get; private set; } + + public string GetDefaultFontFamilyName() => _defaultFamilyName; + + string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) + { + return new[] { _defaultFamilyName }; + } + + public bool TryMatchCharacter( + int codepoint, + FontStyle fontStyle, + FontWeight fontWeight, + FontStretch fontStretch, + string? familyName, + CultureInfo? culture, + out IPlatformTypeface platformTypeface) + { + platformTypeface = null!; + + return false; + } + + public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + platformTypeface = null; + + if (familyName == "MyFont") + { + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var stream = assetLoader.Open(new Uri(_interFontUri)); + + platformTypeface = new HeadlessPlatformTypeface(stream); + } + + TryCreateGlyphTypefaceCount++; + + return platformTypeface != null; + } + + public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, + [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + platformTypeface = new HeadlessPlatformTypeface(stream); + + TryCreateGlyphTypefaceCount++; + + return true; + } + + public bool TryGetFamilyTypefaces(string familyName, + [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + { + familyTypefaces = null; + + return false; + } +} diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index fef60693be..9d475c06ca 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 { @@ -21,21 +22,22 @@ namespace Avalonia.UnitTests standardCursorFactory: new HeadlessCursorFactoryStub(), theme: () => CreateSimpleTheme(), dispatcherImpl: new NullDispatcherImpl(), - fontManagerImpl: new HeadlessFontManagerStub(), - textShaperImpl: new HeadlessTextShaperStub(), + fontManagerImpl: new TestFontManager(), + 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()); + fontManagerImpl: new TestFontManager(), + 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()); @@ -46,8 +48,8 @@ namespace Avalonia.UnitTests inputManager: new InputManager(), assetLoader: new StandardAssetLoader(), renderInterface: new HeadlessPlatformRenderInterface(), - fontManagerImpl: new HeadlessFontManagerStub(), - textShaperImpl: new HeadlessTextShaperStub()); + fontManagerImpl: new TestFontManager(), + textShaperImpl: new HarfBuzzTextShaper()); public static readonly TestServices FocusableWindow = new TestServices( keyboardDevice: () => new KeyboardDevice(), @@ -59,16 +61,16 @@ namespace Avalonia.UnitTests standardCursorFactory: new HeadlessCursorFactoryStub(), theme: () => CreateSimpleTheme(), dispatcherImpl: new NullDispatcherImpl(), - fontManagerImpl: new HeadlessFontManagerStub(), - textShaperImpl: new HeadlessTextShaperStub(), + fontManagerImpl: new TestFontManager(), + 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 TestFontManager(), + textShaperImpl: new HarfBuzzTextShaper()); + internal TestServices( IAssetLoader? assetLoader = null, IInputManager? inputManager = null,