diff --git a/Microsoft.Dotnet.Wpf.sln b/Microsoft.Dotnet.Wpf.sln
index 09509d6f269..b50aa32c677 100644
--- a/Microsoft.Dotnet.Wpf.sln
+++ b/Microsoft.Dotnet.Wpf.sln
@@ -386,6 +386,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Printing.Tests", "sr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresentationFramework.Tests", "src\Microsoft.DotNet.Wpf\tests\UnitTests\PresentationFramework.Tests\PresentationFramework.Tests.csproj", "{33BA28FA-887A-45AE-BEC2-7254E0044DE0}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DirectWriteForwarder.Tests", "src\Microsoft.DotNet.Wpf\tests\UnitTests\DirectWriteForwarder.Tests\DirectWriteForwarder.Tests.csproj", "{67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|arm64 = Debug|arm64
@@ -1682,6 +1684,18 @@ Global
{33BA28FA-887A-45AE-BEC2-7254E0044DE0}.Release|x64.Build.0 = Release|x64
{33BA28FA-887A-45AE-BEC2-7254E0044DE0}.Release|x86.ActiveCfg = Release|x86
{33BA28FA-887A-45AE-BEC2-7254E0044DE0}.Release|x86.Build.0 = Release|x86
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Debug|arm64.ActiveCfg = Debug|arm64
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Debug|arm64.Build.0 = Debug|arm64
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Debug|x64.ActiveCfg = Debug|x64
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Debug|x64.Build.0 = Debug|x64
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Debug|x86.ActiveCfg = Debug|x86
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Debug|x86.Build.0 = Debug|x86
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Release|arm64.ActiveCfg = Release|arm64
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Release|arm64.Build.0 = Release|arm64
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Release|x64.ActiveCfg = Release|x64
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Release|x64.Build.0 = Release|x64
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Release|x86.ActiveCfg = Release|x86
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1811,6 +1825,7 @@ Global
{56833D74-2D0B-5516-C1D6-B93D4FFF7612} = {A48B585E-6AB0-4F8D-8484-77F37CB44437}
{762F6671-44CA-672D-B9C5-CFB69999F152} = {A48B585E-6AB0-4F8D-8484-77F37CB44437}
{33BA28FA-887A-45AE-BEC2-7254E0044DE0} = {A48B585E-6AB0-4F8D-8484-77F37CB44437}
+ {67AE2A3E-9E50-E2D7-6913-9399F6D4EDDF} = {A48B585E-6AB0-4F8D-8484-77F37CB44437}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B4340004-DAC0-497D-B69D-CFA7CD93F567}
diff --git a/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/OtherAssemblyAttrs.cpp b/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/OtherAssemblyAttrs.cpp
index a1f2761ff63..e64d9f0e693 100644
--- a/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/OtherAssemblyAttrs.cpp
+++ b/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/OtherAssemblyAttrs.cpp
@@ -6,4 +6,5 @@ using namespace System::Runtime::CompilerServices;
#using WINDOWS_BASE_DLL
[assembly:InternalsVisibleTo("PresentationCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")];
+[assembly:InternalsVisibleTo("DirectWriteForwarder.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")];
[assembly:System::Runtime::CompilerServices::TypeForwardedTo(System::Windows::Media::TextFormattingMode::typeid)] ;
diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationCore/OtherAssemblyAttrs.cs b/src/Microsoft.DotNet.Wpf/src/PresentationCore/OtherAssemblyAttrs.cs
index 7cf2993f90c..862a7f081bc 100644
--- a/src/Microsoft.DotNet.Wpf/src/PresentationCore/OtherAssemblyAttrs.cs
+++ b/src/Microsoft.DotNet.Wpf/src/PresentationCore/OtherAssemblyAttrs.cs
@@ -17,6 +17,7 @@
[assembly: InternalsVisibleTo(BuildInfo.SystemWindowsControlsRibbon)]
[assembly: InternalsVisibleTo(BuildInfo.WindowsFormsIntegration)]
[assembly: InternalsVisibleTo($"PresentationCore.Tests, PublicKey={BuildInfo.WCP_PUBLIC_KEY_STRING}")]
+[assembly: InternalsVisibleTo($"DirectWriteForwarder.Tests, PublicKey={BuildInfo.WCP_PUBLIC_KEY_STRING}")]
[assembly: TypeForwardedTo(typeof(System.Windows.Markup.IUriContext))]
[assembly: TypeForwardedTo(typeof(System.Windows.Media.TextFormattingMode))]
[assembly: TypeForwardedTo(typeof(System.Windows.Input.ICommand))]
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/AdditionalCoverageTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/AdditionalCoverageTests.cs
new file mode 100644
index 00000000000..6869607022f
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/AdditionalCoverageTests.cs
@@ -0,0 +1,619 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Globalization;
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Additional tests to increase code coverage for DirectWriteForwarder.
+/// Targets uncovered methods in LocalizedStrings, Font, FontList, FontFace, FontCollection.
+///
+public class AdditionalCoverageTests
+{
+ #region LocalizedStrings IDictionary method coverage
+
+ [Fact]
+ public void LocalizedStrings_SetItem_ShouldThrowNotSupportedException()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ Action act = () => localizedStrings[CultureInfo.InvariantCulture] = "Test";
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void LocalizedStrings_AddKeyValuePair_ShouldThrowNotSupportedException()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var pair = new KeyValuePair(CultureInfo.InvariantCulture, "Test");
+ Action act = () => ((ICollection>)localizedStrings).Add(pair);
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void LocalizedStrings_RemoveKeyValuePair_ShouldThrowNotSupportedException()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var firstCulture = TestHelpers.GetFirstCultureOrSkip(localizedStrings);
+
+ var pair = new KeyValuePair(firstCulture, localizedStrings[firstCulture]);
+ Action act = () => ((ICollection>)localizedStrings).Remove(pair);
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void LocalizedStrings_GetEnumerator2_NonGeneric_ShouldWork()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ // Cast to non-generic IEnumerable and get enumerator
+ var enumerable = (IEnumerable)localizedStrings;
+ var enumerator = enumerable.GetEnumerator();
+
+ enumerator.Should().NotBeNull();
+
+ // Should be able to enumerate
+ int count = 0;
+ while (enumerator.MoveNext())
+ {
+ enumerator.Current.Should().NotBeNull();
+ count++;
+ }
+
+ count.Should().Be(localizedStrings.Count);
+ }
+
+ [Fact]
+ public void LocalizedStrings_Indexer_WithNonExistentCulture_ShouldReturnNull()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ // Use a culture very unlikely to be in the font
+ var rareCulture = new CultureInfo("zu-ZA"); // Zulu - South Africa
+
+ // If this culture doesn't exist, the indexer should return null
+ if (!localizedStrings.ContainsKey(rareCulture))
+ {
+ var result = localizedStrings[rareCulture];
+ result.Should().BeNull();
+ }
+ }
+
+ [Fact]
+ public void LocalizedStrings_TryGetValue_WithNonExistentCulture_ShouldReturnFalse()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var rareCulture = new CultureInfo("zu-ZA");
+
+ if (!localizedStrings.ContainsKey(rareCulture))
+ {
+ bool result = localizedStrings.TryGetValue(rareCulture, out _);
+ result.Should().BeFalse();
+ }
+ }
+
+ #endregion
+
+ #region FontList coverage
+
+ [Fact]
+ public void FontList_GetEnumerator2_NonGeneric_ShouldWork()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ // FontFamily inherits from FontList, so we can test the non-generic IEnumerable
+ var enumerable = (IEnumerable)arialFamily;
+ var enumerator = enumerable.GetEnumerator();
+
+ enumerator.Should().NotBeNull();
+
+ int count = 0;
+ while (enumerator.MoveNext())
+ {
+ enumerator.Current.Should().NotBeNull();
+ enumerator.Current.Should().BeOfType();
+ count++;
+ }
+
+ count.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void FontList_FontsCollection_ShouldReturnFontCollection()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+
+ // FontFamily has FontsCollection property from FontList
+ var fontsCollection = arialFamily.FontsCollection;
+
+ fontsCollection.Should().NotBeNull();
+ fontsCollection.FamilyCount.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void FontList_Enumerator_Reset_ShouldResetPosition()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var enumerator = arialFamily.GetEnumerator();
+
+ // Move forward
+ enumerator.MoveNext().Should().BeTrue();
+ var firstFontWeight = enumerator.Current.Weight;
+
+ // Move more
+ if (arialFamily.Count > 1)
+ {
+ enumerator.MoveNext();
+ }
+
+ // Reset
+ enumerator.Reset();
+
+ // After reset, MoveNext should give us the first element again
+ enumerator.MoveNext().Should().BeTrue();
+ enumerator.Current.Weight.Should().Be(firstFontWeight);
+
+ enumerator.Dispose();
+ }
+
+ [Fact]
+ public void FontList_Enumerator_CurrentBeforeMoveNext_ShouldThrow()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var enumerator = arialFamily.GetEnumerator();
+
+ // Accessing Current before MoveNext should throw
+ Action act = () => { var _ = enumerator.Current; };
+ act.Should().Throw();
+
+ enumerator.Dispose();
+ }
+
+ [Fact]
+ public void FontList_Enumerator_CurrentAfterEnd_ShouldThrow()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var enumerator = arialFamily.GetEnumerator();
+
+ // Move past the end
+ while (enumerator.MoveNext()) { }
+
+ // Accessing Current after enumeration ends should throw
+ Action act = () => { var _ = enumerator.Current; };
+ act.Should().Throw();
+
+ enumerator.Dispose();
+ }
+
+ #endregion
+
+ #region FontCollection coverage
+
+ [Fact]
+ public void FontCollection_GetFontFromFontFace_ShouldReturnMatchingFont()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+ var factory = DWriteFactory.Instance;
+
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ var font = fontCollection.GetFontFromFontFace(fontFace);
+
+ font.Should().NotBeNull();
+ // The font should be from the Arial family
+ font!.Family.FamilyNames.GetString(0).Should().Contain("Arial");
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void FontCollection_GetFontFromFontFace_WithSimulations_ShouldReturnFont()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+ var factory = DWriteFactory.Instance;
+
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ // Create a font face with Bold simulation
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0, FontSimulations.Bold);
+ try
+ {
+ var font = fontCollection.GetFontFromFontFace(fontFace);
+
+ // Note: This may or may not return a font depending on how DirectWrite handles simulated faces
+ // The important thing is it doesn't throw
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ #endregion
+
+ #region Font class coverage
+
+ [Fact]
+ public void Font_DWriteFontAddRef_ShouldReturnValidPointer()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var font = arialFamily[0u];
+
+ // Access the DWriteFontAddRef property - this adds a reference to the native object
+ var ptr = font.DWriteFontAddRef;
+
+ ptr.Should().NotBe(IntPtr.Zero);
+ }
+
+ [Fact]
+ public void Font_Version_ShouldBeConsistent()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var font = arialFamily[0u];
+
+ // Call Version multiple times - it should be cached and return same value
+ var version1 = font.Version;
+ var version2 = font.Version;
+
+ version1.Should().Be(version2);
+ version1.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void Font_GetFontFace_MultipleCalls_ShouldUseCacheEfficiently()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var font = arialFamily[0u];
+
+ // Create multiple font faces - the caching logic should be exercised
+ var fontFaces = new List();
+ for (int i = 0; i < 10; i++)
+ {
+ var fontFace = font.GetFontFace();
+ fontFaces.Add(fontFace);
+ }
+
+ try
+ {
+ // All should be valid
+ foreach (var ff in fontFaces)
+ {
+ ff.Should().NotBeNull();
+ ff.GlyphCount.Should().BeGreaterThan(0);
+ }
+ }
+ finally
+ {
+ foreach (var ff in fontFaces)
+ {
+ ff.Release();
+ }
+ }
+ }
+
+ [Fact]
+ public void Font_ResetFontFaceCache_ShouldNotThrow()
+ {
+ // ResetFontFaceCache is a static method that clears cached FontFace instances
+ // We need to call it via reflection since it's internal
+ var fontType = typeof(Font);
+ var resetMethod = fontType.GetMethod("ResetFontFaceCache",
+ System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
+
+ if (resetMethod != null)
+ {
+ Action act = () => resetMethod.Invoke(null, null);
+ act.Should().NotThrow();
+ }
+ }
+
+ [Fact]
+ public void Font_GetFontFace_AfterCacheReset_ShouldStillWork()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var font = arialFamily[0u];
+
+ // Get a font face first to populate the cache
+ var fontFace1 = font.GetFontFace();
+ fontFace1.Release();
+
+ // Reset the cache
+ var fontType = typeof(Font);
+ var resetMethod = fontType.GetMethod("ResetFontFaceCache",
+ System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
+ resetMethod?.Invoke(null, null);
+
+ // Get another font face - should work after cache reset
+ var fontFace2 = font.GetFontFace();
+ try
+ {
+ fontFace2.Should().NotBeNull();
+ fontFace2.GlyphCount.Should().BeGreaterThan(0);
+ }
+ finally
+ {
+ fontFace2.Release();
+ }
+ }
+
+ #endregion
+
+ #region FontFace coverage
+
+ [Fact]
+ public void FontFace_DWriteFontFaceAddRef_ShouldReturnValidPointer()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ var ptr = fontFace.DWriteFontFaceAddRef;
+ ptr.Should().NotBe(IntPtr.Zero);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void FontFace_GetFileZero_ShouldReturnValidFontFile()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ var fontFile = fontFace.GetFileZero();
+ fontFile.Should().NotBeNull();
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void FontFace_IsSymbolFont_ShouldReturnCorrectValue()
+ {
+ var factory = DWriteFactory.Instance;
+
+ // Test with Arial (non-symbol)
+ var arialPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "arial.ttf");
+ if (File.Exists(arialPath))
+ {
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ fontFace.IsSymbolFont.Should().BeFalse();
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ // Test with Symbol font if available
+ var symbolPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "symbol.ttf");
+ if (File.Exists(symbolPath))
+ {
+ var fontFace = factory.CreateFontFace(new Uri(symbolPath), 0);
+ try
+ {
+ fontFace.IsSymbolFont.Should().BeTrue();
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+ }
+
+ #endregion
+
+ #region LocalizedStrings Enumerator coverage
+
+ [Fact]
+ public void LocalizedStringsEnumerator_Reset_ShouldResetPosition()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+ Assert.SkipUnless(localizedStrings.Count > 0, "Localized strings is empty");
+
+ var enumerator = localizedStrings.GetEnumerator();
+
+ // Move forward
+ enumerator.MoveNext().Should().BeTrue();
+ var firstPair = enumerator.Current;
+
+ // Move more
+ if (localizedStrings.Count > 1)
+ {
+ enumerator.MoveNext();
+ }
+
+ // Reset
+ enumerator.Reset();
+
+ // After reset, MoveNext should give us the first element again
+ enumerator.MoveNext().Should().BeTrue();
+ enumerator.Current.Key.Should().Be(firstPair.Key);
+
+ enumerator.Dispose();
+ }
+
+ [Fact]
+ public void LocalizedStringsEnumerator_MoveNextPastEnd_ShouldReturnFalse()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var enumerator = localizedStrings.GetEnumerator();
+
+ // Move to the end
+ while (enumerator.MoveNext()) { }
+
+ // Calling MoveNext again should return false (not throw)
+ enumerator.MoveNext().Should().BeFalse();
+ enumerator.MoveNext().Should().BeFalse(); // Multiple calls should still return false
+
+ enumerator.Dispose();
+ }
+
+ [Fact]
+ public void LocalizedStringsEnumerator_Current2_NonGeneric_ShouldWork()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+ Assert.SkipUnless(localizedStrings.Count > 0, "Localized strings is empty");
+
+ var enumerator = (IEnumerator)localizedStrings.GetEnumerator();
+
+ enumerator.MoveNext();
+ var current = enumerator.Current;
+
+ current.Should().NotBeNull();
+ current.Should().BeOfType>();
+ }
+
+ #endregion
+
+ #region Multiple fonts caching test (exercises LookupFontFaceSlow)
+
+ [Fact]
+ public void Font_MultipleFonts_ShouldExerciseCacheLookup()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count >= 2, "Arial font family has fewer than 2 fonts");
+
+ var fontFaces = new List();
+
+ try
+ {
+ // Get font faces from multiple fonts in the same family
+ // This exercises the cache lookup paths
+ for (uint i = 0; i < Math.Min(arialFamily.Count, 5u); i++)
+ {
+ var font = arialFamily[i];
+ var fontFace = font.GetFontFace();
+ fontFaces.Add(fontFace);
+ }
+
+ // Now access them again - should use cache
+ for (uint i = 0; i < Math.Min(arialFamily.Count, 5u); i++)
+ {
+ var font = arialFamily[i];
+ var fontFace = font.GetFontFace();
+ fontFace.Should().NotBeNull();
+ fontFace.Release();
+ }
+ }
+ finally
+ {
+ foreach (var ff in fontFaces)
+ {
+ ff.Release();
+ }
+ }
+ }
+
+ [Fact]
+ public void Font_DifferentFamilies_ShouldExerciseCacheEviction()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+ var fontFaces = new List();
+
+ try
+ {
+ // Get fonts from multiple different families to fill and evict from cache
+ // Cache size is 4, so accessing more than 4 different fonts should trigger eviction
+ string[] familyNames = ["Arial", "Times New Roman", "Courier New", "Verdana", "Tahoma", "Georgia"];
+
+ foreach (var familyName in familyNames)
+ {
+ var family = fontCollection[familyName];
+ if (family != null && family.Count > 0)
+ {
+ var font = family[0u];
+ var fontFace = font.GetFontFace();
+ fontFaces.Add(fontFace);
+ }
+ }
+
+ // All should be valid
+ foreach (var ff in fontFaces)
+ {
+ ff.GlyphCount.Should().BeGreaterThan(0);
+ }
+ }
+ finally
+ {
+ foreach (var ff in fontFaces)
+ {
+ ff.Release();
+ }
+ }
+ }
+
+ #endregion
+
+ #region FontFileEnumerator coverage
+
+ [Fact]
+ public void FontFileEnumerator_MoveNextPastEnd_ShouldReturnFalse()
+ {
+ // FontFileEnumerator is accessed via factory.CreateFontFileEnumerator
+ // which is internal to the font loading infrastructure
+ // We test it indirectly through font file operations
+
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ // Creating a font face exercises the font file loading infrastructure
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ fontFace.Should().NotBeNull();
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ #endregion
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/DWriteMatrixTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/DWriteMatrixTests.cs
new file mode 100644
index 00000000000..24ab75f1f19
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/DWriteMatrixTests.cs
@@ -0,0 +1,145 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for value struct.
+///
+public class DWriteMatrixTests
+{
+ [Fact]
+ public void DefaultValues_ShouldBeZero()
+ {
+ var matrix = new DWriteMatrix();
+
+ matrix.M11.Should().Be(0);
+ matrix.M12.Should().Be(0);
+ matrix.M21.Should().Be(0);
+ matrix.M22.Should().Be(0);
+ matrix.Dx.Should().Be(0);
+ matrix.Dy.Should().Be(0);
+ }
+
+ [Fact]
+ public void IdentityMatrix_Values()
+ {
+ // An identity matrix should have M11=1, M22=1, all others=0
+ var identity = new DWriteMatrix
+ {
+ M11 = 1.0f,
+ M12 = 0.0f,
+ M21 = 0.0f,
+ M22 = 1.0f,
+ Dx = 0.0f,
+ Dy = 0.0f
+ };
+
+ identity.M11.Should().Be(1.0f);
+ identity.M12.Should().Be(0.0f);
+ identity.M21.Should().Be(0.0f);
+ identity.M22.Should().Be(1.0f);
+ identity.Dx.Should().Be(0.0f);
+ identity.Dy.Should().Be(0.0f);
+ }
+
+ [Fact]
+ public void TranslationMatrix_Values()
+ {
+ // A translation matrix should have M11=1, M22=1, Dx and Dy set to translation values
+ var translation = new DWriteMatrix
+ {
+ M11 = 1.0f,
+ M12 = 0.0f,
+ M21 = 0.0f,
+ M22 = 1.0f,
+ Dx = 100.0f,
+ Dy = 50.0f
+ };
+
+ translation.Dx.Should().Be(100.0f);
+ translation.Dy.Should().Be(50.0f);
+ }
+
+ [Fact]
+ public void ScaleMatrix_Values()
+ {
+ // A scale matrix has M11 and M22 set to scale factors
+ var scale = new DWriteMatrix
+ {
+ M11 = 2.0f,
+ M12 = 0.0f,
+ M21 = 0.0f,
+ M22 = 0.5f,
+ Dx = 0.0f,
+ Dy = 0.0f
+ };
+
+ scale.M11.Should().Be(2.0f);
+ scale.M22.Should().Be(0.5f);
+ }
+
+ [Fact]
+ public void RotationMatrix_90Degrees()
+ {
+ // 90-degree rotation: M11=cos(90)=0, M12=sin(90)=1, M21=-sin(90)=-1, M22=cos(90)=0
+ var rotation90 = new DWriteMatrix
+ {
+ M11 = 0.0f,
+ M12 = 1.0f,
+ M21 = -1.0f,
+ M22 = 0.0f,
+ Dx = 0.0f,
+ Dy = 0.0f
+ };
+
+ rotation90.M11.Should().BeApproximately(0.0f, 0.0001f);
+ rotation90.M12.Should().BeApproximately(1.0f, 0.0001f);
+ rotation90.M21.Should().BeApproximately(-1.0f, 0.0001f);
+ rotation90.M22.Should().BeApproximately(0.0f, 0.0001f);
+ }
+
+ [Theory]
+ [InlineData(1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f)] // Identity
+ [InlineData(2.0f, 0.0f, 0.0f, 2.0f, 0.0f, 0.0f)] // Uniform scale 2x
+ [InlineData(1.0f, 0.0f, 0.0f, 1.0f, 10.0f, 20.0f)] // Translation
+ [InlineData(-1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f)] // Horizontal flip
+ [InlineData(1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f)] // Vertical flip
+ public void AllFields_CanBeSetAndRetrieved(float m11, float m12, float m21, float m22, float dx, float dy)
+ {
+ var matrix = new DWriteMatrix
+ {
+ M11 = m11,
+ M12 = m12,
+ M21 = m21,
+ M22 = m22,
+ Dx = dx,
+ Dy = dy
+ };
+
+ matrix.M11.Should().Be(m11);
+ matrix.M12.Should().Be(m12);
+ matrix.M21.Should().Be(m21);
+ matrix.M22.Should().Be(m22);
+ matrix.Dx.Should().Be(dx);
+ matrix.Dy.Should().Be(dy);
+ }
+
+ [Fact]
+ public void ShearMatrix_Values()
+ {
+ // A shear/skew matrix modifies M12 and M21
+ var shear = new DWriteMatrix
+ {
+ M11 = 1.0f,
+ M12 = 0.5f, // Horizontal shear
+ M21 = 0.25f, // Vertical shear
+ M22 = 1.0f,
+ Dx = 0.0f,
+ Dy = 0.0f
+ };
+
+ shear.M12.Should().Be(0.5f);
+ shear.M21.Should().Be(0.25f);
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/DWriteTypeConverterTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/DWriteTypeConverterTests.cs
new file mode 100644
index 00000000000..ae5774ccfd6
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/DWriteTypeConverterTests.cs
@@ -0,0 +1,908 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for internal class.
+/// These tests exercise the type conversion methods indirectly through public APIs.
+/// Note: Enum types are internal, so we use integer values and cast internally.
+///
+public class DWriteTypeConverterTests
+{
+ #region FactoryType conversion
+
+ [Fact]
+ public void Convert_FactoryType_ShouldConvertWithoutException()
+ {
+ // FactoryType.Shared (0) is used by DWriteFactory.Instance
+ var factory = DWriteFactory.Instance;
+ factory.Should().NotBeNull();
+ }
+
+ #endregion
+
+ #region FontWeight conversion
+
+ [Theory]
+ [InlineData(100)] // Thin
+ [InlineData(200)] // ExtraLight
+ [InlineData(300)] // Light
+ [InlineData(400)] // Normal
+ [InlineData(500)] // Medium
+ [InlineData(600)] // SemiBold
+ [InlineData(700)] // Bold
+ [InlineData(800)] // ExtraBold
+ [InlineData(900)] // Black
+ [InlineData(950)] // ExtraBlack
+ public void Convert_FontWeight_ToNative_ShouldSucceed(int weightValue)
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+
+ var weight = (FontWeight)weightValue;
+ var matchingFonts = arialFamily.GetMatchingFonts(weight, FontStretch.Normal, FontStyle.Normal);
+ matchingFonts.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void Convert_FontWeight_FromNative_ShouldSucceed()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ foreach (var font in arialFamily)
+ {
+ var weight = font.Weight;
+ weight.Should().BeDefined();
+ }
+ }
+
+ [Theory]
+ [InlineData(350)] // Non-standard weight
+ [InlineData(450)]
+ [InlineData(550)]
+ [InlineData(650)]
+ [InlineData(750)]
+ [InlineData(850)]
+ public void Convert_FontWeight_CustomValues_ShouldSucceed(int weightValue)
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+
+ var weight = (FontWeight)weightValue;
+ var matchingFonts = arialFamily.GetMatchingFonts(weight, FontStretch.Normal, FontStyle.Normal);
+ matchingFonts.Should().NotBeNull();
+ }
+
+ #endregion
+
+ #region FontStretch conversion
+
+ [Theory]
+ [InlineData(1)] // UltraCondensed
+ [InlineData(2)] // ExtraCondensed
+ [InlineData(3)] // Condensed
+ [InlineData(4)] // SemiCondensed
+ [InlineData(5)] // Normal
+ [InlineData(6)] // SemiExpanded
+ [InlineData(7)] // Expanded
+ [InlineData(8)] // ExtraExpanded
+ [InlineData(9)] // UltraExpanded
+ public void Convert_FontStretch_ToNative_ShouldSucceed(int stretchValue)
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+
+ var stretch = (FontStretch)stretchValue;
+ var matchingFonts = arialFamily.GetMatchingFonts(FontWeight.Normal, stretch, FontStyle.Normal);
+ matchingFonts.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void Convert_FontStretch_FromNative_ShouldSucceed()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+ string[] familyNames = ["Arial", "Arial Narrow", "Verdana"];
+
+ foreach (var familyName in familyNames)
+ {
+ var family = fontCollection[familyName];
+ if (family == null) continue;
+
+ foreach (var font in family)
+ {
+ var stretch = font.Stretch;
+ stretch.Should().BeDefined();
+ }
+ }
+ }
+
+ #endregion
+
+ #region FontStyle conversion
+
+ [Theory]
+ [InlineData(0)] // Normal
+ [InlineData(1)] // Oblique
+ [InlineData(2)] // Italic
+ public void Convert_FontStyle_ToNative_ShouldSucceed(int styleValue)
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+
+ var style = (FontStyle)styleValue;
+ var matchingFonts = arialFamily.GetMatchingFonts(FontWeight.Normal, FontStretch.Normal, style);
+ matchingFonts.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void Convert_FontStyle_FromNative_ShouldSucceed()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+
+ foreach (var font in arialFamily)
+ {
+ var style = font.Style;
+ style.Should().BeDefined();
+ }
+ }
+
+ #endregion
+
+ #region FontSimulations conversion
+
+ [Theory]
+ [InlineData(0)] // None
+ [InlineData(1)] // Bold
+ [InlineData(2)] // Oblique
+ [InlineData(3)] // Bold | Oblique
+ public void Convert_FontSimulations_ToNative_ShouldSucceed(int simulationsValue)
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var simulations = (FontSimulations)simulationsValue;
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0, simulations);
+ try
+ {
+ fontFace.Should().NotBeNull();
+ fontFace.SimulationFlags.Should().Be(simulations);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void Convert_FontSimulations_FromNative_ShouldSucceed()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var font = arialFamily[0u];
+ // SimulationFlags calls DWriteTypeConverter.Convert(DWRITE_FONT_SIMULATIONS)
+ font.SimulationFlags.Should().BeDefined();
+ }
+
+ #endregion
+
+ #region FontFaceType conversion
+
+ [Fact]
+ public void Convert_FontFaceType_TrueType_FromNative_ShouldSucceed()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ fontFace.Type.Should().Be(FontFaceType.TrueType);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void Convert_FontFaceType_TrueTypeCollection_FromNative_ShouldSucceed()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfFontNotAvailable(TestHelpers.CambriaPath, "Cambria");
+ var cambriaPath = TestHelpers.CambriaPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(cambriaPath), 0);
+ try
+ {
+ fontFace.Type.Should().Be(FontFaceType.TrueTypeCollection);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void Convert_FontFaceType_VariousFonts_FromNative_ShouldSucceed()
+ {
+ var factory = DWriteFactory.Instance;
+ var fontsPath = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
+ string[] fontFiles = ["arial.ttf", "arialbd.ttf", "times.ttf", "cour.ttf", "verdana.ttf"];
+
+ foreach (var fontFile in fontFiles)
+ {
+ var fontPath = Path.Combine(fontsPath, fontFile);
+ if (!File.Exists(fontPath)) continue;
+
+ var fontFace = factory.CreateFontFace(new Uri(fontPath), 0);
+ try
+ {
+ fontFace.Type.Should().BeDefined();
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+ }
+
+ #endregion
+
+ #region InformationalStringID conversion
+
+ [Theory]
+ [InlineData(0, null)] // None - no string expected
+ [InlineData(1, "Monotype")] // CopyrightNotice - contains "Monotype"
+ [InlineData(2, null)] // VersionStrings - version varies
+ [InlineData(3, "Arial")] // Trademark - contains "Arial"
+ [InlineData(4, "Monotype")] // Manufacturer - "Monotype" or "The Monotype Corporation"
+ [InlineData(5, null)] // Designer - may not exist
+ [InlineData(6, null)] // DesignerURL - may not exist
+ [InlineData(7, null)] // Description - may not exist
+ [InlineData(8, null)] // FontVendorURL - may not exist
+ [InlineData(9, null)] // LicenseDescription - may not exist
+ [InlineData(10, null)] // LicenseInfoURL - may not exist
+ [InlineData(11, "Arial")] // WIN32FamilyNames - "Arial"
+ [InlineData(12, "Normal")] // Win32SubFamilyNames - "Normal" (localized style name)
+ [InlineData(13, null)] // PreferredFamilyNames - may not exist
+ [InlineData(14, null)] // PreferredSubFamilyNames - may not exist
+ [InlineData(15, null)] // SampleText - may not exist
+ public void Convert_InformationalStringID_ToNative_ShouldSucceed(int stringIdValue, string? expectedSubstring)
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var font = arialFamily[0u];
+ var stringId = (InformationalStringID)stringIdValue;
+
+ // GetInformationalStrings calls DWriteTypeConverter.Convert(InformationalStringID)
+ var exists = font.GetInformationalStrings(stringId, out var localizedStrings);
+
+ if (expectedSubstring != null && exists)
+ {
+ // Validate the English string contains expected content
+ localizedStrings.Should().NotBeNull();
+ var englishValue = localizedStrings!.Values.FirstOrDefault() ?? "";
+ englishValue.Should().Contain(expectedSubstring,
+ $"InformationalStringID {stringId} should contain '{expectedSubstring}'");
+ }
+ }
+
+ #endregion
+
+ #region TextFormattingMode / MeasuringMode conversion
+
+ [Fact]
+ public unsafe void Convert_TextFormattingMode_Ideal_ShouldSucceed()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var font = arialFamily[0u];
+ var fontFace = font.GetFontFace();
+ try
+ {
+ uint[] codePoints = [65]; // 'A'
+ ushort[] glyphIndices = new ushort[1];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, 1, pGlyphIndices);
+ }
+
+ // useDisplayNatural: false = Ideal mode
+ GlyphMetrics[] metrics = new GlyphMetrics[1];
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ fixed (GlyphMetrics* pMetrics = metrics)
+ {
+ fontFace.GetDisplayGlyphMetrics(pGlyphIndices, 1, pMetrics,
+ 12.0f, useDisplayNatural: false, isSideways: false, pixelsPerDip: 1.0f);
+ }
+
+ metrics[0].AdvanceWidth.Should().BeGreaterThan(0);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public unsafe void Convert_TextFormattingMode_Display_ShouldSucceed()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ uint[] codePoints = [65]; // 'A'
+ ushort[] glyphIndices = new ushort[1];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, 1, pGlyphIndices);
+ }
+
+ // useDisplayNatural: true = Display mode
+ GlyphMetrics[] metrics = new GlyphMetrics[1];
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ fixed (GlyphMetrics* pMetrics = metrics)
+ {
+ fontFace.GetDisplayGlyphMetrics(pGlyphIndices, 1, pMetrics,
+ 12.0f, useDisplayNatural: true, isSideways: false, pixelsPerDip: 1.0f);
+ }
+
+ metrics[0].AdvanceWidth.Should().BeGreaterThan(0);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ #endregion
+
+ #region FontMetrics conversion
+
+ [Fact]
+ public void Convert_FontMetrics_FromNative_ShouldSucceed()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var font = arialFamily[0u];
+ var metrics = font.Metrics;
+
+ metrics.Should().NotBeNull();
+ metrics.Ascent.Should().BeGreaterThan(0);
+ metrics.Descent.Should().BeGreaterThan(0);
+ metrics.DesignUnitsPerEm.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void Convert_FontMetrics_DisplayMetrics_FromNative_ShouldSucceed()
+ {
+ var arialFamily = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(arialFamily.Count > 0, "Arial font family has no fonts");
+
+ var font = arialFamily[0u];
+ var displayMetrics = font.DisplayMetrics(12.0f, 96.0f);
+
+ displayMetrics.Should().NotBeNull();
+ displayMetrics.Ascent.Should().BeGreaterThan(0);
+ }
+
+ #endregion
+
+ #region DWriteMatrix struct
+
+ [Fact]
+ public void DWriteMatrix_DefaultValues_ShouldBeValid()
+ {
+ var matrix = new DWriteMatrix();
+
+ matrix.M11.Should().Be(0);
+ matrix.M12.Should().Be(0);
+ matrix.M21.Should().Be(0);
+ matrix.M22.Should().Be(0);
+ matrix.Dx.Should().Be(0);
+ matrix.Dy.Should().Be(0);
+ }
+
+ [Fact]
+ public void DWriteMatrix_IdentityMatrix_ShouldBeValid()
+ {
+ var matrix = new DWriteMatrix
+ {
+ M11 = 1.0f,
+ M12 = 0.0f,
+ M21 = 0.0f,
+ M22 = 1.0f,
+ Dx = 0.0f,
+ Dy = 0.0f
+ };
+
+ matrix.M11.Should().Be(1.0f);
+ matrix.M22.Should().Be(1.0f);
+ }
+
+ #endregion
+
+ #region Direct DWriteTypeConverter method calls
+
+ // The following tests call DWriteTypeConverter methods directly
+ // since it's a private ref class (internal in C#) with internal methods
+
+ // DWRITE_FACTORY_TYPE enum values (from dwrite.h)
+ private const int DWRITE_FACTORY_TYPE_SHARED = 0;
+ private const int DWRITE_FACTORY_TYPE_ISOLATED = 1;
+
+ [Theory]
+ [InlineData(0, DWRITE_FACTORY_TYPE_SHARED)] // Shared
+ [InlineData(1, DWRITE_FACTORY_TYPE_ISOLATED)] // Isolated
+ public void DWriteTypeConverter_Convert_FactoryType_ReturnsExpectedValue(int factoryTypeValue, int expectedNativeValue)
+ {
+ var factoryType = (FactoryType)factoryTypeValue;
+ int result = (int)DWriteTypeConverter.Convert(factoryType);
+ result.Should().Be(expectedNativeValue,
+ $"FactoryType.{factoryType} should convert to DWRITE_FACTORY_TYPE value {expectedNativeValue}");
+ }
+
+ [Fact]
+ public void DWriteTypeConverter_Convert_FactoryType_Invalid_ShouldThrow()
+ {
+ var invalidFactoryType = (FactoryType)99;
+ var act = () => DWriteTypeConverter.Convert(invalidFactoryType);
+ act.Should().Throw();
+ }
+
+ // DWRITE_FONT_WEIGHT enum values match the numeric weight values (100-950)
+ [Theory]
+ [InlineData(100, 100)] // Thin
+ [InlineData(200, 200)] // ExtraLight
+ [InlineData(300, 300)] // Light
+ [InlineData(350, 350)] // SemiLight - passes through as-is
+ [InlineData(400, 400)] // Normal
+ [InlineData(500, 500)] // Medium
+ [InlineData(600, 600)] // SemiBold
+ [InlineData(700, 700)] // Bold
+ [InlineData(800, 800)] // ExtraBold
+ [InlineData(900, 900)] // Black
+ [InlineData(950, 950)] // ExtraBlack
+ [InlineData(1, 1)] // Min valid custom weight
+ [InlineData(999, 999)] // Max valid custom weight
+ [InlineData(550, 550)] // Custom weight in middle
+ public void DWriteTypeConverter_Convert_FontWeight_PreservesNumericValue(int weightValue, int expectedNativeValue)
+ {
+ var weight = (FontWeight)weightValue;
+ int result = (int)DWriteTypeConverter.Convert(weight);
+ result.Should().Be(expectedNativeValue,
+ $"FontWeight({weightValue}) should convert to DWRITE_FONT_WEIGHT value {expectedNativeValue}");
+ }
+
+ [Theory]
+ [InlineData(0)] // Invalid - too low
+ [InlineData(1000)] // Invalid - too high
+ [InlineData(-1)] // Invalid - negative
+ public void DWriteTypeConverter_Convert_FontWeight_Invalid_ShouldThrow(int weightValue)
+ {
+ var weight = (FontWeight)weightValue;
+ var act = () => DWriteTypeConverter.Convert(weight);
+ act.Should().Throw();
+ }
+
+ // DWRITE_FONT_STRETCH enum values (0-9)
+ [Theory]
+ [InlineData(0, 0)] // Undefined
+ [InlineData(1, 1)] // UltraCondensed
+ [InlineData(2, 2)] // ExtraCondensed
+ [InlineData(3, 3)] // Condensed
+ [InlineData(4, 4)] // SemiCondensed
+ [InlineData(5, 5)] // Normal
+ [InlineData(6, 6)] // SemiExpanded
+ [InlineData(7, 7)] // Expanded
+ [InlineData(8, 8)] // ExtraExpanded
+ [InlineData(9, 9)] // UltraExpanded
+ public void DWriteTypeConverter_Convert_FontStretch_PreservesNumericValue(int stretchValue, int expectedNativeValue)
+ {
+ var stretch = (FontStretch)stretchValue;
+ int result = (int)DWriteTypeConverter.Convert(stretch);
+ result.Should().Be(expectedNativeValue,
+ $"FontStretch({stretchValue}) should convert to DWRITE_FONT_STRETCH value {expectedNativeValue}");
+ }
+
+ [Fact]
+ public void DWriteTypeConverter_Convert_FontStretch_Invalid_ShouldThrow()
+ {
+ var invalidStretch = (FontStretch)99;
+ var act = () => DWriteTypeConverter.Convert(invalidStretch);
+ act.Should().Throw();
+ }
+
+ // DWRITE_FONT_STYLE enum values (0-2)
+ [Theory]
+ [InlineData(0, 0)] // Normal
+ [InlineData(1, 1)] // Oblique
+ [InlineData(2, 2)] // Italic
+ public void DWriteTypeConverter_Convert_FontStyle_PreservesNumericValue(int styleValue, int expectedNativeValue)
+ {
+ var style = (FontStyle)styleValue;
+ int result = (int)DWriteTypeConverter.Convert(style);
+ result.Should().Be(expectedNativeValue,
+ $"FontStyle({styleValue}) should convert to DWRITE_FONT_STYLE value {expectedNativeValue}");
+ }
+
+ [Fact]
+ public void DWriteTypeConverter_Convert_FontStyle_Invalid_ShouldThrow()
+ {
+ var invalidStyle = (FontStyle)99;
+ var act = () => DWriteTypeConverter.Convert(invalidStyle);
+ act.Should().Throw();
+ }
+
+ // DWRITE_FONT_SIMULATIONS enum values (0-3)
+ [Theory]
+ [InlineData(0, 0)] // None
+ [InlineData(1, 1)] // Bold
+ [InlineData(2, 2)] // Oblique
+ [InlineData(3, 3)] // Bold | Oblique
+ public void DWriteTypeConverter_Convert_FontSimulations_PreservesNumericValue(int simValue, int expectedNativeValue)
+ {
+ var simulations = (FontSimulations)simValue;
+ int result = (int)DWriteTypeConverter.Convert(simulations);
+ result.Should().Be(expectedNativeValue,
+ $"FontSimulations({simValue}) should convert to DWRITE_FONT_SIMULATIONS value {expectedNativeValue}");
+ }
+
+ [Fact]
+ public void DWriteTypeConverter_Convert_FontSimulations_Invalid_ShouldThrow()
+ {
+ var invalidSim = (FontSimulations)99;
+ var act = () => DWriteTypeConverter.Convert(invalidSim);
+ act.Should().Throw();
+ }
+
+ // DWRITE_FONT_FACE_TYPE enum values - mapping from WPF FontFaceType to native DWRITE values
+ // WPF: CFF=0, TrueType=1, TrueTypeCollection=2, Type1=3, Vector=4, Bitmap=5, Unknown=6
+ // Native: Same ordering as WPF (they match)
+ [Theory]
+ [InlineData(0, 0)] // CFF -> DWRITE_FONT_FACE_TYPE_CFF
+ [InlineData(1, 1)] // TrueType -> DWRITE_FONT_FACE_TYPE_TRUETYPE
+ [InlineData(2, 2)] // TrueTypeCollection -> DWRITE_FONT_FACE_TYPE_TRUETYPE_COLLECTION
+ [InlineData(3, 3)] // Type1 -> DWRITE_FONT_FACE_TYPE_TYPE1
+ [InlineData(4, 4)] // Vector -> DWRITE_FONT_FACE_TYPE_VECTOR
+ [InlineData(5, 5)] // Bitmap -> DWRITE_FONT_FACE_TYPE_BITMAP
+ [InlineData(6, 6)] // Unknown -> DWRITE_FONT_FACE_TYPE_UNKNOWN
+ public void DWriteTypeConverter_Convert_FontFaceType_MapsToCorrectNativeValue(int faceTypeValue, int expectedNativeValue)
+ {
+ var faceType = (FontFaceType)faceTypeValue;
+ int result = (int)DWriteTypeConverter.Convert(faceType);
+ result.Should().Be(expectedNativeValue,
+ $"FontFaceType({faceTypeValue}) should convert to DWRITE_FONT_FACE_TYPE value {expectedNativeValue}");
+ }
+
+ [Fact]
+ public void DWriteTypeConverter_Convert_FontFaceType_Invalid_ShouldThrow()
+ {
+ var invalidFaceType = (FontFaceType)99;
+ var act = () => DWriteTypeConverter.Convert(invalidFaceType);
+ act.Should().Throw();
+ }
+
+ // DWRITE_INFORMATIONAL_STRING_ID enum values (0-15)
+ [Theory]
+ [InlineData(0, 0)] // None
+ [InlineData(1, 1)] // CopyrightNotice
+ [InlineData(2, 2)] // VersionStrings
+ [InlineData(3, 3)] // Trademark
+ [InlineData(4, 4)] // Manufacturer
+ [InlineData(5, 5)] // Designer
+ [InlineData(6, 6)] // DesignerURL
+ [InlineData(7, 7)] // Description
+ [InlineData(8, 8)] // FontVendorURL
+ [InlineData(9, 9)] // LicenseDescription
+ [InlineData(10, 10)] // LicenseInfoURL
+ [InlineData(11, 11)] // WIN32FamilyNames
+ [InlineData(12, 12)] // Win32SubFamilyNames
+ [InlineData(13, 13)] // PreferredFamilyNames
+ [InlineData(14, 14)] // PreferredSubFamilyNames
+ [InlineData(15, 15)] // SampleText
+ public void DWriteTypeConverter_Convert_InformationalStringID_PreservesNumericValue(int stringIdValue, int expectedNativeValue)
+ {
+ var stringId = (InformationalStringID)stringIdValue;
+ int result = (int)DWriteTypeConverter.Convert(stringId);
+ result.Should().Be(expectedNativeValue,
+ $"InformationalStringID({stringIdValue}) should convert to DWRITE_INFORMATIONAL_STRING_ID value {expectedNativeValue}");
+ }
+
+ [Fact]
+ public void DWriteTypeConverter_Convert_InformationalStringID_Invalid_ShouldThrow()
+ {
+ var invalidStringId = (InformationalStringID)99;
+ var act = () => DWriteTypeConverter.Convert(invalidStringId);
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void DWriteTypeConverter_Convert_FontMetrics_ToNative()
+ {
+ // Create a FontMetrics object and convert to native
+ var fontMetrics = new FontMetrics
+ {
+ Ascent = 1000,
+ Descent = 200,
+ LineGap = 50,
+ CapHeight = 700,
+ XHeight = 500,
+ UnderlinePosition = -100,
+ UnderlineThickness = 50,
+ StrikethroughPosition = 300,
+ StrikethroughThickness = 50,
+ DesignUnitsPerEm = 2048
+ };
+
+ _ = DWriteTypeConverter.Convert(fontMetrics);
+ }
+
+ [Fact]
+ public void DWriteTypeConverter_Convert_DWriteMatrix_ToNative()
+ {
+ // Create a DWriteMatrix and convert to native
+ var matrix = new DWriteMatrix
+ {
+ M11 = 1.0f,
+ M12 = 0.0f,
+ M21 = 0.0f,
+ M22 = 1.0f,
+ Dx = 10.0f,
+ Dy = 20.0f
+ };
+
+ _ = DWriteTypeConverter.Convert(matrix);
+ }
+
+ [Fact]
+ public void DWriteTypeConverter_Convert_DWriteMatrix_WithRotation_ToNative()
+ {
+ // Test with a rotation matrix
+ var matrix = new DWriteMatrix
+ {
+ M11 = 0.707f, // cos(45)
+ M12 = 0.707f, // sin(45)
+ M21 = -0.707f, // -sin(45)
+ M22 = 0.707f, // cos(45)
+ Dx = 0.0f,
+ Dy = 0.0f
+ };
+
+ _ = DWriteTypeConverter.Convert(matrix);
+ }
+
+ [Theory]
+ [InlineData(0)] // Ideal
+ [InlineData(1)] // Display
+ public void DWriteTypeConverter_Convert_TextFormattingMode_ToNative(int modeValue)
+ {
+ var mode = (System.Windows.Media.TextFormattingMode)modeValue;
+ _ = DWriteTypeConverter.Convert(mode);
+ }
+
+ [Fact]
+ public void DWriteTypeConverter_Convert_TextFormattingMode_Invalid_ShouldThrow()
+ {
+ var invalidMode = (System.Windows.Media.TextFormattingMode)99;
+ var act = () => DWriteTypeConverter.Convert(invalidMode);
+ act.Should().Throw();
+ }
+
+ #endregion
+
+ #region Native to Managed conversions via Font properties
+
+ // These tests exercise the DWRITE_* to managed conversions by accessing
+ // font properties that internally call DWriteTypeConverter.Convert(DWRITE_*)
+
+ [Fact]
+ public void Convert_FontWeight_FromNative_ViaAllSystemFonts()
+ {
+ // Iterate ALL system fonts to maximize coverage of different DWRITE_FONT_WEIGHT values
+ var fontCollection = DWriteFactory.SystemFontCollection;
+ var weightsFound = new HashSet();
+
+ // Iterate all families (not just first 50) to maximize coverage
+ for (uint i = 0; i < fontCollection.FamilyCount; i++)
+ {
+ var family = fontCollection[i];
+ foreach (var font in family)
+ {
+ var weight = font.Weight;
+ weightsFound.Add((int)weight);
+ }
+ }
+
+ // We should find Normal (400) and Bold (700) in system fonts
+ weightsFound.Should().Contain(400, "System should have Normal weight fonts");
+ weightsFound.Should().Contain(700, "System should have Bold weight fonts");
+
+ // Additional weights typically found on Windows: Light (300), Medium (500), SemiBold (600)
+ // We don't assert these as they depend on installed fonts, but we exercise all available values
+ }
+
+ [Fact]
+ public void Convert_FontStretch_FromNative_ViaAllSystemFonts()
+ {
+ // Iterate ALL system fonts to maximize coverage of different DWRITE_FONT_STRETCH values
+ var fontCollection = DWriteFactory.SystemFontCollection;
+ var stretchesFound = new HashSet();
+
+ // Iterate all families (not just first 50) to maximize coverage
+ for (uint i = 0; i < fontCollection.FamilyCount; i++)
+ {
+ var family = fontCollection[i];
+ foreach (var font in family)
+ {
+ var stretch = font.Stretch;
+ stretchesFound.Add((int)stretch);
+ }
+ }
+
+ // We should find Normal (5) in system fonts
+ stretchesFound.Should().Contain(5, "System should have Normal stretch fonts");
+
+ // Other stretches (Condensed=3, SemiCondensed=4, SemiExpanded=6, etc.) depend on installed fonts
+ }
+
+ [Fact]
+ public void Convert_FontStyle_FromNative_ViaAllSystemFonts()
+ {
+ // Iterate ALL system fonts to maximize coverage of different DWRITE_FONT_STYLE values
+ var fontCollection = DWriteFactory.SystemFontCollection;
+ var stylesFound = new HashSet();
+
+ // Iterate all families (not just first 50) to maximize coverage
+ for (uint i = 0; i < fontCollection.FamilyCount; i++)
+ {
+ var family = fontCollection[i];
+ foreach (var font in family)
+ {
+ var style = font.Style;
+ stylesFound.Add((int)style);
+ }
+ }
+
+ // We should find Normal (0) and Italic (2) in system fonts
+ stylesFound.Should().Contain(0, "System should have Normal style fonts");
+ stylesFound.Should().Contain(2, "System should have Italic style fonts");
+
+ // Oblique (1) is rare in system fonts but may be found depending on installed fonts
+ }
+
+ [Fact]
+ public void Convert_FontSimulations_FromNative_ViaFontFace()
+ {
+ // Test different font simulations via FontFace
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ FontSimulations[] simulations = [FontSimulations.None, FontSimulations.Bold, FontSimulations.Oblique, FontSimulations.Bold | FontSimulations.Oblique];
+
+ foreach (var sim in simulations)
+ {
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0, sim);
+ try
+ {
+ // This calls Convert(DWRITE_FONT_SIMULATIONS)
+ var resultSim = fontFace.SimulationFlags;
+ resultSim.Should().Be(sim);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+ }
+
+ [Fact]
+ public void Convert_FontFaceType_FromNative_ViaVariousFonts()
+ {
+ // Test different font face types
+ var factory = DWriteFactory.Instance;
+ var fontsPath = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
+ var typesFound = new HashSet();
+
+ // TTF files (TrueType)
+ string[] ttfFiles = ["arial.ttf", "times.ttf", "verdana.ttf", "cour.ttf"];
+ foreach (var file in ttfFiles)
+ {
+ var path = Path.Combine(fontsPath, file);
+ if (!File.Exists(path)) continue;
+
+ var fontFace = factory.CreateFontFace(new Uri(path), 0);
+ try
+ {
+ // This calls Convert(DWRITE_FONT_FACE_TYPE)
+ var faceType = fontFace.Type;
+ faceType.Should().Be(FontFaceType.TrueType);
+ typesFound.Add(faceType);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ // TTC file (TrueTypeCollection)
+ var ttcPath = Path.Combine(fontsPath, "cambria.ttc");
+ if (File.Exists(ttcPath))
+ {
+ var fontFace = factory.CreateFontFace(new Uri(ttcPath), 0);
+ try
+ {
+ var faceType = fontFace.Type;
+ faceType.Should().Be(FontFaceType.TrueTypeCollection);
+ typesFound.Add(faceType);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ // OTF files (CFF - OpenType with PostScript outlines)
+ string[] otfFiles = ["calibri.ttf", "consola.ttf", "segoeui.ttf"];
+ foreach (var file in otfFiles)
+ {
+ var path = Path.Combine(fontsPath, file);
+ if (!File.Exists(path)) continue;
+
+ var fontFace = factory.CreateFontFace(new Uri(path), 0);
+ try
+ {
+ // Most Windows fonts are TrueType; CFF fonts are less common
+ var faceType = fontFace.Type;
+ typesFound.Add(faceType);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ // We should have found at least TrueType
+ typesFound.Should().Contain(FontFaceType.TrueType, "Should find at least TrueType fonts");
+ }
+
+ [Fact]
+ public void Convert_FontMetrics_FromNative_ViaAllSystemFonts()
+ {
+ // Access FontMetrics from ALL system fonts to cover Convert(DWRITE_FONT_METRICS)
+ var fontCollection = DWriteFactory.SystemFontCollection;
+ int fontsChecked = 0;
+
+ for (uint i = 0; i < fontCollection.FamilyCount; i++)
+ {
+ var family = fontCollection[i];
+ if (family.Count == 0) continue;
+
+ var font = family[0u];
+ // This calls Convert(DWRITE_FONT_METRICS)
+ var metrics = font.Metrics;
+
+ metrics.Should().NotBeNull();
+ metrics.DesignUnitsPerEm.Should().BeGreaterThan(0);
+ metrics.Ascent.Should().BeGreaterThan(0);
+
+ fontsChecked++;
+ }
+
+ fontsChecked.Should().BeGreaterThan(0, "Should have checked at least one font");
+ }
+
+ #endregion
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/DirectWriteForwarder.Tests.csproj b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/DirectWriteForwarder.Tests.csproj
new file mode 100644
index 00000000000..06f7ae075f8
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/DirectWriteForwarder.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+ true
+ enable
+ x64;x86;arm64
+
+ $(TargetFramework)-windows
+ true
+ true
+ Exe
+ $(NoWarn);xUnit1025
+
+
+
+
+
+
+
+
+
+
+ TargetFramework;TargetFrameworks
+
+
+
+
+
+
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/EdgeCaseTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/EdgeCaseTests.cs
new file mode 100644
index 00000000000..0b49ef00eb4
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/EdgeCaseTests.cs
@@ -0,0 +1,740 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Edge case and error handling tests for DirectWriteForwarder classes.
+/// Tests boundary conditions, invalid inputs, and error scenarios.
+///
+public class EdgeCaseTests
+{
+ #region FontCollection Edge Cases
+
+ [Fact]
+ public void FontCollection_Indexer_WithOutOfRangeIndex_ShouldThrow()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+ var invalidIndex = fontCollection.FamilyCount + 100u;
+
+ Action act = () => _ = fontCollection[invalidIndex];
+
+ // DirectWrite throws COMException for out of range indices
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void FontCollection_Indexer_WithMaxUInt_ShouldThrow()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ Action act = () => _ = fontCollection[uint.MaxValue];
+
+ // DirectWrite throws COMException for out of range indices
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void FontCollection_Indexer_WithEmptyString_ShouldReturnNull()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ var result = fontCollection[string.Empty];
+
+ result.Should().BeNull();
+ }
+
+ [Fact]
+ public void FontCollection_FindFamilyName_WithEmptyString_ShouldReturnFalse()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ bool found = fontCollection.FindFamilyName(string.Empty, out _);
+
+ found.Should().BeFalse();
+ }
+
+ [Fact]
+ public void FontCollection_FindFamilyName_WithWhitespace_ShouldReturnFalse()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ bool found = fontCollection.FindFamilyName(" ", out _);
+
+ found.Should().BeFalse();
+ }
+
+ [Fact]
+ public void FontCollection_FindFamilyName_WithSpecialCharacters_ShouldReturnFalse()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ bool found = fontCollection.FindFamilyName("!@#$%^&*()", out _);
+
+ found.Should().BeFalse();
+ }
+
+ [Fact]
+ public void FontCollection_FindFamilyName_WithVeryLongName_ShouldReturnFalse()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+ var veryLongName = new string('A', 10000);
+
+ bool found = fontCollection.FindFamilyName(veryLongName, out _);
+
+ found.Should().BeFalse();
+ }
+
+ [Fact]
+ public void FontCollection_FindFamilyName_CaseSensitivity_ShouldBeInsensitive()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ bool foundUpper = fontCollection.FindFamilyName("ARIAL", out uint indexUpper);
+ bool foundLower = fontCollection.FindFamilyName("arial", out uint indexLower);
+ bool foundMixed = fontCollection.FindFamilyName("ArIaL", out uint indexMixed);
+
+ // All should find the same font (case-insensitive matching)
+ if (foundUpper && foundLower && foundMixed)
+ {
+ indexUpper.Should().Be(indexLower);
+ indexLower.Should().Be(indexMixed);
+ }
+ }
+
+ #endregion
+
+ #region FontFamily Edge Cases
+
+ [Fact]
+ public void FontFamily_Indexer_WithOutOfRangeIndex_ShouldThrow()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var invalidIndex = family.Count + 100;
+
+ Action act = () => _ = family[(uint)invalidIndex];
+
+ // DirectWrite throws COMException for out of range indices
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void FontFamily_GetFirstMatchingFont_WithExtremeWeight_ShouldReturnClosestMatch()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ // Request a weight that likely doesn't exist (e.g., 950)
+ var font = family.GetFirstMatchingFont((FontWeight)950, FontStretch.Normal, FontStyle.Normal);
+
+ font.Should().NotBeNull("Should return closest matching font");
+ }
+
+ [Fact]
+ public void FontFamily_GetFirstMatchingFont_WithMinWeight_ShouldReturnFont()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var font = family.GetFirstMatchingFont((FontWeight)1, FontStretch.Normal, FontStyle.Normal);
+
+ font.Should().NotBeNull("Should return closest matching font for minimum weight");
+ }
+
+ [Fact]
+ public void FontFamily_DisplayMetrics_WithZeroEmSize_ShouldThrowArgumentException()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ // Zero emSize is rejected by DirectWrite
+ Action act = () => family.DisplayMetrics(emSize: 0.0f, pixelsPerDip: 1.0f);
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void FontFamily_DisplayMetrics_WithNegativeEmSize_ShouldThrowArgumentException()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ // Negative emSize is rejected by DirectWrite
+ Action act = () => family.DisplayMetrics(emSize: -12.0f, pixelsPerDip: 1.0f);
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void FontFamily_DisplayMetrics_WithVeryLargeEmSize_ShouldReturnMetrics()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var metrics = family.DisplayMetrics(emSize: 10000.0f, pixelsPerDip: 1.0f);
+
+ metrics.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void FontFamily_DisplayMetrics_WithVerySmallPixelsPerDip_ShouldReturnMetrics()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var metrics = family.DisplayMetrics(emSize: 12.0f, pixelsPerDip: 0.001f);
+
+ metrics.Should().NotBeNull();
+ }
+
+ #endregion
+
+ #region Factory Edge Cases
+
+ [Fact]
+ public void Factory_CreateFontFace_WithNonExistentFile_ShouldThrow()
+ {
+ var factory = DWriteFactory.Instance;
+ var nonExistentPath = new Uri("file:///C:/NonExistent/totally_fake_font_12345.ttf");
+
+ Action act = () => factory.CreateFontFace(nonExistentPath, 0);
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void Factory_CreateFontFace_WithNegativeFaceIndex_ShouldThrow()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ // The API takes uint, so we can't pass negative directly, but we can test boundary
+ Action act = () => factory.CreateFontFace(new Uri(arialPath), 999);
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void Factory_IsLocalUri_WithFtpUri_ReturnsFalse()
+ {
+ var ftpUri = new Uri("ftp://example.com/fonts/myfont.ttf");
+
+ Factory.IsLocalUri(ftpUri).Should().BeFalse();
+ }
+
+ [Fact]
+ public void Factory_IsLocalUri_WithHttpsUri_ReturnsFalse()
+ {
+ var httpsUri = new Uri("https://example.com/fonts/myfont.ttf");
+
+ Factory.IsLocalUri(httpsUri).Should().BeFalse();
+ }
+
+ #endregion
+
+ #region FontFace Edge Cases
+
+ [Fact]
+ public void FontFace_TryGetFontTable_WithInvalidTableTag_ShouldReturnFalse()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ // Use a table tag that doesn't exist (custom/invalid tag)
+ bool found = fontFace.TryGetFontTable((OpenTypeTableTag)0x58585858, out byte[]? tableData); // 'XXXX'
+
+ found.Should().BeFalse();
+ tableData.Should().BeNull();
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public unsafe void FontFace_GetArrayOfGlyphIndices_WithZeroCount_ShouldNotThrow()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ uint[] codePoints = [65];
+ ushort[] glyphIndices = new ushort[1];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ // Zero count should be handled gracefully
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, 0, pGlyphIndices);
+ }
+
+ // No exception means success
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public unsafe void FontFace_GetDesignGlyphMetrics_WithZeroGlyphIndex_ShouldReturnMetrics()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ // Glyph index 0 is typically .notdef
+ ushort[] glyphIndices = [0];
+ GlyphMetrics[] metrics = new GlyphMetrics[1];
+
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ fixed (GlyphMetrics* pMetrics = metrics)
+ {
+ fontFace.GetDesignGlyphMetrics(pGlyphIndices, 1, pMetrics);
+ }
+
+ // .notdef glyph should still have metrics
+ metrics[0].Should().NotBeNull();
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public unsafe void FontFace_GetDisplayGlyphMetrics_WithZeroEmSize_ShouldThrowArgumentException()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ uint[] codePoints = [65]; // 'A'
+ ushort[] glyphIndices = new ushort[1];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, 1, pGlyphIndices);
+ }
+
+ GlyphMetrics[] metrics = new GlyphMetrics[1];
+
+ // Zero emSize should throw ArgumentException
+ Action act = () =>
+ {
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ fixed (GlyphMetrics* pMetrics = metrics)
+ {
+ fontFace.GetDisplayGlyphMetrics(
+ pGlyphIndices,
+ 1,
+ pMetrics,
+ emSize: 0.0f,
+ useDisplayNatural: false,
+ isSideways: false,
+ pixelsPerDip: 1.0f);
+ }
+ };
+
+ act.Should().Throw();
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public unsafe void FontFace_GetDisplayGlyphMetrics_WithSideways_ShouldReturnMetrics()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ uint[] codePoints = [65]; // 'A'
+ ushort[] glyphIndices = new ushort[1];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, 1, pGlyphIndices);
+ }
+
+ GlyphMetrics[] metrics = new GlyphMetrics[1];
+
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ fixed (GlyphMetrics* pMetrics = metrics)
+ {
+ // isSideways = true for vertical text layout
+ fontFace.GetDisplayGlyphMetrics(
+ pGlyphIndices,
+ 1,
+ pMetrics,
+ emSize: 12.0f,
+ useDisplayNatural: false,
+ isSideways: true,
+ pixelsPerDip: 1.0f);
+ }
+
+ metrics[0].AdvanceWidth.Should().BeGreaterThan(0);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public unsafe void FontFace_GetDisplayGlyphMetrics_WithUseDisplayNatural_ShouldReturnMetrics()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ uint[] codePoints = [65]; // 'A'
+ ushort[] glyphIndices = new ushort[1];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, 1, pGlyphIndices);
+ }
+
+ GlyphMetrics[] metrics = new GlyphMetrics[1];
+
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ fixed (GlyphMetrics* pMetrics = metrics)
+ {
+ // useDisplayNatural = true
+ fontFace.GetDisplayGlyphMetrics(
+ pGlyphIndices,
+ 1,
+ pMetrics,
+ emSize: 12.0f,
+ useDisplayNatural: true,
+ isSideways: false,
+ pixelsPerDip: 1.0f);
+ }
+
+ metrics[0].AdvanceWidth.Should().BeGreaterThan(0);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ #endregion
+
+ #region Font Edge Cases
+
+ [Fact]
+ public void Font_HasCharacter_WithZeroCodePoint_ShouldReturnFalse()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(family.Count > 0, "Arial font family has no fonts");
+
+ var font = family[0u];
+
+ // Code point 0 (null character)
+ font.HasCharacter(0).Should().BeFalse();
+ }
+
+ [Fact]
+ public void Font_HasCharacter_WithMaxCodePoint_ShouldReturnFalse()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(family.Count > 0, "Arial font family has no fonts");
+
+ var font = family[0u];
+
+ // Max Unicode code point
+ font.HasCharacter(0x10FFFF).Should().BeFalse();
+ }
+
+ [Fact]
+ public void Font_HasCharacter_WithSurrogateCodePoint_ShouldReturnFalse()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(family.Count > 0, "Arial font family has no fonts");
+
+ var font = family[0u];
+
+ // Surrogate code points are invalid
+ font.HasCharacter(0xD800).Should().BeFalse();
+ font.HasCharacter(0xDFFF).Should().BeFalse();
+ }
+
+ [Fact]
+ public void Font_HasCharacter_WithPrivateUseArea_ShouldReturnResult()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(family.Count > 0, "Arial font family has no fonts");
+
+ var font = family[0u];
+
+ // Private Use Area (U+E000 to U+F8FF)
+ // Result depends on font, but should not throw
+ _ = font.HasCharacter(0xE000);
+ }
+
+ [Fact]
+ public void Font_GetInformationalStrings_AllStringTypes_ShouldNotThrow()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(family.Count > 0, "Arial font family has no fonts");
+
+ var font = family[0u];
+
+ // Iterate through all InformationalStringID values
+ foreach (InformationalStringID stringId in Enum.GetValues(typeof(InformationalStringID)))
+ {
+ // Should not throw for any valid enum value
+ _ = font.GetInformationalStrings(stringId, out _);
+ }
+ }
+
+ [Fact]
+ public void Font_DisplayMetrics_WithExtremeValues_ShouldNotThrow()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(family.Count > 0, "Arial font family has no fonts");
+
+ var font = family[0u];
+
+ // Very large values
+ var metrics1 = font.DisplayMetrics(float.MaxValue / 2, 96.0f);
+ metrics1.Should().NotBeNull();
+
+ // Very small values
+ var metrics2 = font.DisplayMetrics(0.001f, 0.001f);
+ metrics2.Should().NotBeNull();
+ }
+
+ #endregion
+
+ #region FontMetrics Struct Edge Cases
+
+ [Fact]
+ public void FontMetrics_WithZeroDesignUnitsPerEm_Baseline_ReturnsInfinityOrNaN()
+ {
+ var metrics = new FontMetrics
+ {
+ DesignUnitsPerEm = 0,
+ Ascent = 1000,
+ Descent = 500,
+ LineGap = 100
+ };
+
+ // When DesignUnitsPerEm is 0, Baseline calculation involves division by zero
+ // Result should be infinity or NaN
+ var baseline = metrics.Baseline;
+ (double.IsInfinity(baseline) || double.IsNaN(baseline)).Should().BeTrue();
+ }
+
+ [Fact]
+ public void FontMetrics_LineSpacing_WithTypicalValues_ShouldCalculateCorrectly()
+ {
+ var metrics = new FontMetrics
+ {
+ DesignUnitsPerEm = 2048,
+ Ascent = 1854,
+ Descent = 434,
+ LineGap = 67
+ };
+
+ // Get line spacing
+ var lineSpacing = metrics.LineSpacing;
+
+ lineSpacing.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void FontMetrics_Baseline_ShouldBeConsistentWithFormula()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var metrics = family.Metrics;
+
+ // Baseline = (Ascent + LineGap * 0.5) / DesignUnitsPerEm
+ double expectedBaseline = (metrics.Ascent + metrics.LineGap * 0.5) / metrics.DesignUnitsPerEm;
+
+ metrics.Baseline.Should().BeApproximately(expectedBaseline, 0.0001);
+ }
+
+ #endregion
+
+ #region GlyphMetrics Struct Edge Cases
+
+ [Fact]
+ public void GlyphMetrics_DefaultValues_ShouldBeZero()
+ {
+ var metrics = new GlyphMetrics();
+
+ metrics.AdvanceWidth.Should().Be(0);
+ metrics.AdvanceHeight.Should().Be(0);
+ metrics.LeftSideBearing.Should().Be(0);
+ metrics.RightSideBearing.Should().Be(0);
+ metrics.TopSideBearing.Should().Be(0);
+ metrics.BottomSideBearing.Should().Be(0);
+ metrics.VerticalOriginY.Should().Be(0);
+ }
+
+ [Fact]
+ public void GlyphMetrics_NegativeSideBearings_AreValid()
+ {
+ // Some glyphs have negative side bearings (e.g., italic 'f')
+ var metrics = new GlyphMetrics
+ {
+ AdvanceWidth = 500,
+ LeftSideBearing = -50,
+ RightSideBearing = -30
+ };
+
+ metrics.LeftSideBearing.Should().Be(-50);
+ metrics.RightSideBearing.Should().Be(-30);
+ }
+
+ #endregion
+
+ #region DWriteMatrix Struct Edge Cases
+
+ [Fact]
+ public void DWriteMatrix_DefaultValues_ShouldBeZero()
+ {
+ var matrix = new DWriteMatrix();
+
+ matrix.M11.Should().Be(0.0f);
+ matrix.M12.Should().Be(0.0f);
+ matrix.M21.Should().Be(0.0f);
+ matrix.M22.Should().Be(0.0f);
+ matrix.Dx.Should().Be(0.0f);
+ matrix.Dy.Should().Be(0.0f);
+ }
+
+ [Fact]
+ public void DWriteMatrix_WithExtremeValues_ShouldStore()
+ {
+ var matrix = new DWriteMatrix
+ {
+ M11 = float.MaxValue,
+ M12 = float.MinValue,
+ M21 = float.Epsilon,
+ M22 = float.NegativeInfinity,
+ Dx = float.PositiveInfinity,
+ Dy = float.NaN
+ };
+
+ matrix.M11.Should().Be(float.MaxValue);
+ matrix.M12.Should().Be(float.MinValue);
+ matrix.M21.Should().Be(float.Epsilon);
+ matrix.M22.Should().Be(float.NegativeInfinity);
+ matrix.Dx.Should().Be(float.PositiveInfinity);
+ float.IsNaN(matrix.Dy).Should().BeTrue();
+ }
+
+ #endregion
+
+ #region LocalizedStrings Edge Cases
+
+ [Fact]
+ public void LocalizedStrings_NonExistentString_GetInformationalStrings_ReturnsFalse()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(family.Count > 0, "Arial font family has no fonts");
+
+ var font = family[0u];
+
+ // Try to get a string that might not exist (SampleText is often not present)
+ bool exists = font.GetInformationalStrings(InformationalStringID.SampleText, out var localizedStrings);
+
+ // If the string doesn't exist, exists should be false
+ // Just verify the call doesn't throw
+ if (exists)
+ {
+ localizedStrings.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public void LocalizedStrings_Enumeration_ShouldNotThrow()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var familyNames = family.FamilyNames;
+
+ // Enumerate should not throw
+ foreach (var kvp in familyNames)
+ {
+ kvp.Key.Should().NotBeNull();
+ kvp.Value.Should().NotBeNull();
+ }
+ }
+
+ #endregion
+
+ #region Unicode Boundary Tests
+
+ [Fact]
+ public void Font_HasCharacter_WithBMPBoundary_ShouldWork()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+ Assert.SkipUnless(family.Count > 0, "Arial font family has no fonts");
+
+ var font = family[0u];
+
+ // BMP boundary (U+FFFF)
+ _ = font.HasCharacter(0xFFFF);
+
+ // Just above BMP (U+10000) - Supplementary Multilingual Plane
+ _ = font.HasCharacter(0x10000);
+ }
+
+ [Fact]
+ public unsafe void FontFace_GetArrayOfGlyphIndices_WithSupplementaryPlaneCharacters_ShouldWork()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ try
+ {
+ // Supplementary plane characters (emoji, historic scripts, etc.)
+ uint[] codePoints = [0x1F600, 0x1F4A9, 0x1D11E]; // Emoji and musical symbol
+ ushort[] glyphIndices = new ushort[codePoints.Length];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, (uint)codePoints.Length, pGlyphIndices);
+ }
+
+ // These are likely not in Arial, so should be 0 (.notdef)
+ // But the call should not throw
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ #endregion
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/EnumTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/EnumTests.cs
new file mode 100644
index 00000000000..98e49c9ddc0
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/EnumTests.cs
@@ -0,0 +1,521 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for the FactoryType enum.
+///
+public class FactoryTypeTests
+{
+ [Fact]
+ public void FactoryType_HasExpectedValues()
+ {
+ // FactoryType should have Shared and Isolated values
+ FactoryType.Shared.Should().BeDefined();
+ FactoryType.Isolated.Should().BeDefined();
+ }
+
+ [Fact]
+ public void FactoryType_Shared_IsDefaultValue()
+ {
+ // Shared should be 0 (first enum value)
+ ((int)FactoryType.Shared).Should().Be(0);
+ }
+
+ [Fact]
+ public void FactoryType_Isolated_IsOne()
+ {
+ ((int)FactoryType.Isolated).Should().Be(1);
+ }
+
+ [Fact]
+ public void FactoryType_HasOnlyTwoValues()
+ {
+ var values = Enum.GetValues();
+ values.Should().HaveCount(2);
+ }
+}
+
+///
+/// Tests for the FontStyle enum.
+///
+public class FontStyleTests
+{
+ [Fact]
+ public void FontStyle_Normal_IsZero()
+ {
+ ((int)FontStyle.Normal).Should().Be(0);
+ }
+
+ [Fact]
+ public void FontStyle_Oblique_IsOne()
+ {
+ ((int)FontStyle.Oblique).Should().Be(1);
+ }
+
+ [Fact]
+ public void FontStyle_Italic_IsTwo()
+ {
+ ((int)FontStyle.Italic).Should().Be(2);
+ }
+
+ [Fact]
+ public void FontStyle_HasThreeValues()
+ {
+ var values = Enum.GetValues();
+ values.Should().HaveCount(3);
+ }
+
+ [Theory]
+ [InlineData(FontStyle.Normal)]
+ [InlineData(FontStyle.Oblique)]
+ [InlineData(FontStyle.Italic)]
+ public void FontStyle_AllValues_AreDefined(object style)
+ {
+ Enum.IsDefined(typeof(FontStyle), style).Should().BeTrue();
+ }
+}
+
+///
+/// Tests for the FontSimulations flags enum.
+///
+public class FontSimulationsTests
+{
+ [Fact]
+ public void FontSimulations_None_IsZero()
+ {
+ ((int)FontSimulations.None).Should().Be(0);
+ }
+
+ [Fact]
+ public void FontSimulations_Bold_IsOne()
+ {
+ ((int)FontSimulations.Bold).Should().Be(1);
+ }
+
+ [Fact]
+ public void FontSimulations_Oblique_IsTwo()
+ {
+ ((int)FontSimulations.Oblique).Should().Be(2);
+ }
+
+ [Fact]
+ public void FontSimulations_CanCombineBoldAndOblique()
+ {
+ // FontSimulations is a flags enum, so Bold | Oblique should be valid
+ var combined = FontSimulations.Bold | FontSimulations.Oblique;
+ ((int)combined).Should().Be(3);
+ }
+
+ [Fact]
+ public void FontSimulations_HasFlagsAttribute()
+ {
+ typeof(FontSimulations).GetCustomAttributes(typeof(FlagsAttribute), false)
+ .Should().NotBeEmpty();
+ }
+
+ [Fact]
+ public void FontSimulations_CombinedValue_ContainsBothFlags()
+ {
+ var combined = FontSimulations.Bold | FontSimulations.Oblique;
+
+ combined.HasFlag(FontSimulations.Bold).Should().BeTrue();
+ combined.HasFlag(FontSimulations.Oblique).Should().BeTrue();
+ combined.HasFlag(FontSimulations.None).Should().BeTrue(); // None (0) is always contained
+ }
+}
+
+///
+/// Tests for the FontWeight enum.
+///
+public class FontWeightTests
+{
+ [Theory]
+ [InlineData(FontWeight.Thin, 100)]
+ [InlineData(FontWeight.ExtraLight, 200)]
+ [InlineData(FontWeight.UltraLight, 200)]
+ [InlineData(FontWeight.Light, 300)]
+ [InlineData(FontWeight.Normal, 400)]
+ [InlineData(FontWeight.Regular, 400)]
+ [InlineData(FontWeight.Medium, 500)]
+ [InlineData(FontWeight.DemiBold, 600)]
+ [InlineData(FontWeight.SemiBOLD, 600)]
+ [InlineData(FontWeight.Bold, 700)]
+ [InlineData(FontWeight.ExtraBold, 800)]
+ [InlineData(FontWeight.UltraBold, 800)]
+ [InlineData(FontWeight.Black, 900)]
+ [InlineData(FontWeight.Heavy, 900)]
+ [InlineData(FontWeight.ExtraBlack, 950)]
+ [InlineData(FontWeight.UltraBlack, 950)]
+ public void FontWeight_HasExpectedValues(object weight, int expectedValue)
+ {
+ ((int)weight).Should().Be(expectedValue);
+ }
+
+ [Fact]
+ public void FontWeight_ExtraLightAndUltraLight_AreEqual()
+ {
+ // These are aliases with the same value
+ FontWeight.ExtraLight.Should().Be(FontWeight.UltraLight);
+ }
+
+ [Fact]
+ public void FontWeight_NormalAndRegular_AreEqual()
+ {
+ FontWeight.Normal.Should().Be(FontWeight.Regular);
+ }
+
+ [Fact]
+ public void FontWeight_DemiBoldAndSemiBold_AreEqual()
+ {
+ FontWeight.DemiBold.Should().Be(FontWeight.SemiBOLD);
+ }
+
+ [Fact]
+ public void FontWeight_BlackAndHeavy_AreEqual()
+ {
+ FontWeight.Black.Should().Be(FontWeight.Heavy);
+ }
+}
+
+///
+/// Tests for the FontStretch enum.
+///
+public class FontStretchTests
+{
+ [Theory]
+ [InlineData(FontStretch.Undefined, 0)]
+ [InlineData(FontStretch.UltraCondensed, 1)]
+ [InlineData(FontStretch.ExtraCondensed, 2)]
+ [InlineData(FontStretch.Condensed, 3)]
+ [InlineData(FontStretch.SemiCondensed, 4)]
+ [InlineData(FontStretch.Normal, 5)]
+ [InlineData(FontStretch.Medium, 5)]
+ [InlineData(FontStretch.SemiExpanded, 6)]
+ [InlineData(FontStretch.Expanded, 7)]
+ [InlineData(FontStretch.ExtraExpanded, 8)]
+ [InlineData(FontStretch.UltraExpanded, 9)]
+ public void FontStretch_HasExpectedValues(object stretch, int expectedValue)
+ {
+ ((int)stretch).Should().Be(expectedValue);
+ }
+
+ [Fact]
+ public void FontStretch_NormalAndMedium_AreEqual()
+ {
+ FontStretch.Normal.Should().Be(FontStretch.Medium);
+ }
+
+ [Fact]
+ public void FontStretch_ValuesAreSequential()
+ {
+ // Values should go from 0 (Undefined) to 9 (UltraExpanded)
+ ((int)FontStretch.Undefined).Should().Be(0);
+ ((int)FontStretch.UltraExpanded).Should().Be(9);
+ }
+}
+
+///
+/// Tests for the FontFaceType enum.
+///
+public class FontFaceTypeTests
+{
+ [Fact]
+ public void FontFaceType_HasExpectedValues()
+ {
+ FontFaceType.CFF.Should().BeDefined();
+ FontFaceType.TrueType.Should().BeDefined();
+ FontFaceType.TrueTypeCollection.Should().BeDefined();
+ FontFaceType.Type1.Should().BeDefined();
+ FontFaceType.Vector.Should().BeDefined();
+ FontFaceType.Bitmap.Should().BeDefined();
+ FontFaceType.Unknown.Should().BeDefined();
+ }
+
+ [Theory]
+ [InlineData(FontFaceType.CFF, 0)]
+ [InlineData(FontFaceType.TrueType, 1)]
+ [InlineData(FontFaceType.TrueTypeCollection, 2)]
+ [InlineData(FontFaceType.Type1, 3)]
+ [InlineData(FontFaceType.Vector, 4)]
+ [InlineData(FontFaceType.Bitmap, 5)]
+ [InlineData(FontFaceType.Unknown, 6)]
+ public void FontFaceType_HasSequentialValues(object faceType, int expectedValue)
+ {
+ ((int)faceType).Should().Be(expectedValue);
+ }
+}
+
+///
+/// Tests for the FontFileType enum.
+///
+public class FontFileTypeTests
+{
+ [Fact]
+ public void FontFileType_HasExpectedValues()
+ {
+ FontFileType.Unknown.Should().BeDefined();
+ FontFileType.CFF.Should().BeDefined();
+ FontFileType.TrueType.Should().BeDefined();
+ FontFileType.TrueTypeCollection.Should().BeDefined();
+ FontFileType.Type1PFM.Should().BeDefined();
+ FontFileType.Type1PFB.Should().BeDefined();
+ FontFileType.Vector.Should().BeDefined();
+ FontFileType.Bitmap.Should().BeDefined();
+ }
+
+ [Theory]
+ [InlineData(FontFileType.Unknown, 0)]
+ [InlineData(FontFileType.CFF, 1)]
+ [InlineData(FontFileType.TrueType, 2)]
+ [InlineData(FontFileType.TrueTypeCollection, 3)]
+ [InlineData(FontFileType.Type1PFM, 4)]
+ [InlineData(FontFileType.Type1PFB, 5)]
+ [InlineData(FontFileType.Vector, 6)]
+ [InlineData(FontFileType.Bitmap, 7)]
+ public void FontFileType_HasSequentialValues(object fileType, int expectedValue)
+ {
+ ((int)fileType).Should().Be(expectedValue);
+ }
+}
+
+///
+/// Tests for the InformationalStringID enum.
+///
+public class InformationalStringIDTests
+{
+ [Fact]
+ public void InformationalStringID_None_IsZero()
+ {
+ ((int)InformationalStringID.None).Should().Be(0);
+ }
+
+ [Fact]
+ public void InformationalStringID_HasAllExpectedValues()
+ {
+ InformationalStringID.None.Should().BeDefined();
+ InformationalStringID.CopyrightNotice.Should().BeDefined();
+ InformationalStringID.VersionStrings.Should().BeDefined();
+ InformationalStringID.Trademark.Should().BeDefined();
+ InformationalStringID.Manufacturer.Should().BeDefined();
+ InformationalStringID.Designer.Should().BeDefined();
+ InformationalStringID.DesignerURL.Should().BeDefined();
+ InformationalStringID.Description.Should().BeDefined();
+ InformationalStringID.FontVendorURL.Should().BeDefined();
+ InformationalStringID.LicenseDescription.Should().BeDefined();
+ InformationalStringID.LicenseInfoURL.Should().BeDefined();
+ InformationalStringID.WIN32FamilyNames.Should().BeDefined();
+ InformationalStringID.Win32SubFamilyNames.Should().BeDefined();
+ InformationalStringID.PreferredFamilyNames.Should().BeDefined();
+ InformationalStringID.PreferredSubFamilyNames.Should().BeDefined();
+ InformationalStringID.SampleText.Should().BeDefined();
+ }
+
+ [Fact]
+ public void InformationalStringID_ValuesAreSequential()
+ {
+ var values = Enum.GetValues();
+
+ // Should have 16 values (0-15)
+ values.Should().HaveCount(16);
+
+ // Values should be sequential from 0 to 15
+ for (int i = 0; i < values.Length; i++)
+ {
+ ((int)values[i]).Should().Be(i);
+ }
+ }
+}
+
+///
+/// Tests for the OpenTypeTableTag enum.
+///
+public class OpenTypeTableTagTests
+{
+ [Fact]
+ public void OpenTypeTableTag_HasExpectedTableTags()
+ {
+ // Core tables
+ OpenTypeTableTag.CharToIndexMap.Should().BeDefined(); // cmap
+ OpenTypeTableTag.FontHeader.Should().BeDefined(); // head
+ OpenTypeTableTag.HoriHeader.Should().BeDefined(); // hhea
+ OpenTypeTableTag.HorizontalMetrics.Should().BeDefined(); // hmtx
+ OpenTypeTableTag.ControlValue.Should().BeDefined(); // cvt
+ OpenTypeTableTag.FontProgram.Should().BeDefined(); // fpgm
+ OpenTypeTableTag.MaxProfile.Should().BeDefined(); // maxp
+ OpenTypeTableTag.NamingTable.Should().BeDefined(); // name
+ OpenTypeTableTag.IndexToLoc.Should().BeDefined(); // loca
+ OpenTypeTableTag.GlyphData.Should().BeDefined(); // glyf
+ }
+
+ [Fact]
+ public void OpenTypeTableTag_HasOpenTypeLayoutTables()
+ {
+ // OpenType layout tables
+ OpenTypeTableTag.TTO_GSUB.Should().BeDefined();
+ OpenTypeTableTag.TTO_GPOS.Should().BeDefined();
+ OpenTypeTableTag.TTO_GDEF.Should().BeDefined();
+ }
+
+ [Fact]
+ public void OpenTypeTableTag_HasOS2Table()
+ {
+ // OS/2 table (Windows-specific metrics)
+ OpenTypeTableTag.OS_2.Should().BeDefined();
+ }
+
+ [Fact]
+ public void OpenTypeTableTag_ValuesAreOpenTypeTags()
+ {
+ // OpenType table tags are 4-byte ASCII identifiers
+ // 'head' = 0x68656164
+ var headTag = (uint)OpenTypeTableTag.FontHeader;
+ var headBytes = BitConverter.GetBytes(headTag);
+
+ // Should decode to 'head' (little-endian on Windows)
+ var tagString = System.Text.Encoding.ASCII.GetString(headBytes);
+ tagString.Should().Be("head");
+ }
+
+ [Fact]
+ public void OpenTypeTableTag_CmapDecodesToCorrectString()
+ {
+ var tagBytes = BitConverter.GetBytes((uint)OpenTypeTableTag.CharToIndexMap);
+ var tagString = System.Text.Encoding.ASCII.GetString(tagBytes);
+ tagString.Should().Be("cmap");
+ }
+
+ [Fact]
+ public void OpenTypeTableTag_GsubDecodesToCorrectString()
+ {
+ var tagBytes = BitConverter.GetBytes((uint)OpenTypeTableTag.TTO_GSUB);
+ var tagString = System.Text.Encoding.ASCII.GetString(tagBytes);
+ tagString.Should().Be("GSUB");
+ }
+
+ [Fact]
+ public void OpenTypeTableTag_NameDecodesToCorrectString()
+ {
+ var tagBytes = BitConverter.GetBytes((uint)OpenTypeTableTag.NamingTable);
+ var tagString = System.Text.Encoding.ASCII.GetString(tagBytes);
+ tagString.Should().Be("name");
+ }
+}
+
+///
+/// Tests for the DWriteFontFeatureTag enum.
+///
+public class DWriteFontFeatureTagTests
+{
+ [Fact]
+ public void DWriteFontFeatureTag_HasCommonFeatures()
+ {
+ // Common OpenType features
+ DWriteFontFeatureTag.Kerning.Should().BeDefined(); // kern
+ DWriteFontFeatureTag.StandardLigatures.Should().BeDefined(); // liga
+ DWriteFontFeatureTag.SmallCapitals.Should().BeDefined(); // smcp
+ DWriteFontFeatureTag.Fractions.Should().BeDefined(); // frac
+ DWriteFontFeatureTag.SlashedZero.Should().BeDefined(); // zero
+ }
+
+ [Fact]
+ public void DWriteFontFeatureTag_HasStylisticSets()
+ {
+ // Stylistic sets ss01-ss20
+ DWriteFontFeatureTag.StylisticSet1.Should().BeDefined();
+ DWriteFontFeatureTag.StylisticSet10.Should().BeDefined();
+ DWriteFontFeatureTag.StylisticSet20.Should().BeDefined();
+ }
+
+ [Fact]
+ public void DWriteFontFeatureTag_HasPositioningFeatures()
+ {
+ DWriteFontFeatureTag.Superscript.Should().BeDefined(); // sups
+ DWriteFontFeatureTag.Subscript.Should().BeDefined(); // subs
+ DWriteFontFeatureTag.Ordinals.Should().BeDefined(); // ordn
+ }
+
+ [Fact]
+ public void DWriteFontFeatureTag_KerningDecodesToCorrectString()
+ {
+ var tagBytes = BitConverter.GetBytes((uint)DWriteFontFeatureTag.Kerning);
+ var tagString = System.Text.Encoding.ASCII.GetString(tagBytes);
+ tagString.Should().Be("kern");
+ }
+
+ [Fact]
+ public void DWriteFontFeatureTag_LigaDecodesToCorrectString()
+ {
+ var tagBytes = BitConverter.GetBytes((uint)DWriteFontFeatureTag.StandardLigatures);
+ var tagString = System.Text.Encoding.ASCII.GetString(tagBytes);
+ tagString.Should().Be("liga");
+ }
+
+ [Fact]
+ public void DWriteFontFeatureTag_SmcpDecodesToCorrectString()
+ {
+ var tagBytes = BitConverter.GetBytes((uint)DWriteFontFeatureTag.SmallCapitals);
+ var tagString = System.Text.Encoding.ASCII.GetString(tagBytes);
+ tagString.Should().Be("smcp");
+ }
+}
+
+///
+/// Tests for the GlyphOffset struct.
+///
+public class GlyphOffsetTests
+{
+ [Fact]
+ public void GlyphOffset_DefaultValues_AreZero()
+ {
+ var offset = new GlyphOffset();
+
+ offset.du.Should().Be(0);
+ offset.dv.Should().Be(0);
+ }
+
+ [Fact]
+ public void GlyphOffset_CanSetValues()
+ {
+ var offset = new GlyphOffset
+ {
+ du = 10,
+ dv = -5
+ };
+
+ offset.du.Should().Be(10);
+ offset.dv.Should().Be(-5);
+ }
+}
+
+///
+/// Tests for the DWriteFontFeature struct.
+///
+public class DWriteFontFeatureTests
+{
+ [Fact]
+ public void DWriteFontFeature_Constructor_SetsProperties()
+ {
+ var feature = new DWriteFontFeature(DWriteFontFeatureTag.Kerning, 1);
+
+ feature.nameTag.Should().Be(DWriteFontFeatureTag.Kerning);
+ feature.parameter.Should().Be(1);
+ }
+
+ [Fact]
+ public void DWriteFontFeature_DisabledFeature_HasZeroParameter()
+ {
+ var feature = new DWriteFontFeature(DWriteFontFeatureTag.StandardLigatures, 0);
+
+ feature.parameter.Should().Be(0);
+ }
+
+ [Fact]
+ public void DWriteFontFeature_EnabledFeature_HasNonZeroParameter()
+ {
+ var feature = new DWriteFontFeature(DWriteFontFeatureTag.SmallCapitals, 1);
+
+ feature.parameter.Should().BeGreaterThan(0);
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FactoryTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FactoryTests.cs
new file mode 100644
index 00000000000..f5964b6a426
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FactoryTests.cs
@@ -0,0 +1,136 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for class.
+///
+public class FactoryTests
+{
+ [Fact]
+ public void Instance_ShouldNotBeNull()
+ {
+ var factory = DWriteFactory.Instance;
+ factory.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void GetSystemFontCollection_ShouldReturnNonEmptyCollection()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ fontCollection.Should().NotBeNull();
+ fontCollection.FamilyCount.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void GetSystemFontCollection_CalledMultipleTimes_ReturnsSameInstance()
+ {
+ var collection1 = DWriteFactory.SystemFontCollection;
+ var collection2 = DWriteFactory.SystemFontCollection;
+
+ // Should be cached
+ collection1.Should().BeSameAs(collection2);
+ }
+
+ [Fact]
+ public void IsLocalUri_WithFileUri_ReturnsTrue()
+ {
+ var localUri = new Uri("file:///C:/Windows/Fonts/arial.ttf");
+
+ Factory.IsLocalUri(localUri).Should().BeTrue();
+ }
+
+ [Fact]
+ public void IsLocalUri_WithHttpUri_ReturnsFalse()
+ {
+ var httpUri = new Uri("http://example.com/font.ttf");
+
+ Factory.IsLocalUri(httpUri).Should().BeFalse();
+ }
+
+ [Fact]
+ public void IsLocalUri_WithUncPath_ReturnsFalse()
+ {
+ var uncUri = new Uri("file://server/share/font.ttf");
+
+ Factory.IsLocalUri(uncUri).Should().BeFalse();
+ }
+
+ [Fact]
+ public void CreateTextAnalyzer_ShouldReturnNonNull()
+ {
+ var factory = DWriteFactory.Instance;
+
+ var textAnalyzer = factory.CreateTextAnalyzer();
+
+ textAnalyzer.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void CreateFontFile_WithSystemFont_ShouldSucceed()
+ {
+ var factory = DWriteFactory.Instance;
+ var arialPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "arial.ttf");
+
+ if (File.Exists(arialPath))
+ {
+ var fontFile = factory.CreateFontFile(new Uri(arialPath));
+ fontFile.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public void CreateFontFace_WithSystemFont_ShouldSucceed()
+ {
+ var factory = DWriteFactory.Instance;
+ var arialPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "arial.ttf");
+
+ if (File.Exists(arialPath))
+ {
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0);
+ fontFace.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public void CreateFontFace_WithSimulations_ShouldSucceed()
+ {
+ var factory = DWriteFactory.Instance;
+ var arialPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "arial.ttf");
+
+ if (File.Exists(arialPath))
+ {
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0, FontSimulations.Bold);
+ fontFace.Should().NotBeNull();
+ fontFace.SimulationFlags.Should().Be(FontSimulations.Bold);
+ }
+ }
+
+ [Fact]
+ public void CreateFontFile_WithNonExistentFile_ShouldThrow()
+ {
+ var factory = DWriteFactory.Instance;
+ var nonExistentPath = new Uri("file:///C:/NonExistent/fake_font.ttf");
+
+ Action act = () => factory.CreateFontFile(nonExistentPath);
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void CreateFontFace_WithInvalidFaceIndex_ShouldThrow()
+ {
+ var factory = DWriteFactory.Instance;
+ var arialPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "arial.ttf");
+
+ if (File.Exists(arialPath))
+ {
+ // Arial.ttf typically has only one face (index 0), so index 999 should fail
+ Action act = () => factory.CreateFontFace(new Uri(arialPath), 999);
+
+ act.Should().Throw();
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontCollectionTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontCollectionTests.cs
new file mode 100644
index 00000000000..66b87d088da
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontCollectionTests.cs
@@ -0,0 +1,265 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for class.
+///
+public class FontCollectionTests
+{
+ [Fact]
+ public void FamilyCount_ShouldBeGreaterThanZero()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ fontCollection.FamilyCount.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void Indexer_WithValidIndex_ShouldReturnFontFamily()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ var firstFamily = fontCollection[0u];
+
+ firstFamily.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void Indexer_WithFamilyName_ShouldReturnFontFamily()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ // Arial should exist on all Windows systems
+ var arialFamily = fontCollection["Arial"];
+
+ arialFamily.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void Indexer_WithInvalidFamilyName_ShouldReturnNull()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ var nonExistentFamily = fontCollection["NonExistentFontFamily12345"];
+
+ nonExistentFamily.Should().BeNull();
+ }
+
+ [Fact]
+ public void FindFamilyName_WithExistingFamily_ShouldReturnTrueAndIndex()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ bool found = fontCollection.FindFamilyName("Arial", out uint index);
+
+ found.Should().BeTrue();
+ index.Should().BeGreaterThanOrEqualTo(0);
+ }
+
+ [Fact]
+ public void FindFamilyName_WithNonExistingFamily_ShouldReturnFalse()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ bool found = fontCollection.FindFamilyName("NonExistentFontFamily12345", out _);
+
+ found.Should().BeFalse();
+ }
+
+ [Fact]
+ public void AllFamilies_ShouldBeAccessible()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ for (uint i = 0; i < Math.Min(fontCollection.FamilyCount, 10u); i++)
+ {
+ var family = fontCollection[i];
+ family.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public void CommonSystemFonts_ShouldExist()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ // These fonts should exist on all Windows installations
+ var commonFonts = new[] { "Arial", "Times New Roman", "Courier New", "Segoe UI" };
+
+ foreach (var fontName in commonFonts)
+ {
+ bool found = fontCollection.FindFamilyName(fontName, out _);
+ // At least some of these should exist
+ if (found)
+ {
+ var family = fontCollection[fontName];
+ family.Should().NotBeNull($"{fontName} should be accessible");
+ }
+ }
+ }
+}
+
+///
+/// Tests for class.
+///
+public class FontFamilyTests
+{
+ [Fact]
+ public void FamilyNames_ShouldNotBeEmpty()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var familyNames = family.FamilyNames;
+
+ familyNames.Should().NotBeNull();
+ familyNames.Count.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void IsPhysical_ShouldBeTrue()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ family.IsPhysical.Should().BeTrue();
+ }
+
+ [Fact]
+ public void IsComposite_ShouldBeFalse()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ family.IsComposite.Should().BeFalse();
+ }
+
+ [Fact]
+ public void Count_ShouldBeGreaterThanZero()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ family.Count.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void Indexer_ShouldReturnFont()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var firstFont = family[0u];
+
+ firstFont.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void GetEnumerator_ShouldEnumerateFonts()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var fonts = family.ToList();
+
+ fonts.Should().NotBeEmpty();
+ fonts.Should().AllSatisfy(f => f.Should().NotBeNull());
+ }
+
+ [Fact]
+ public void OrdinalName_ShouldNotBeEmpty()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ family.OrdinalName.Should().NotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public void Metrics_ShouldBeValid()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var metrics = family.Metrics;
+
+ metrics.Should().NotBeNull();
+ metrics.DesignUnitsPerEm.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void DisplayMetrics_ShouldReturnValidMetrics()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var displayMetrics = family.DisplayMetrics(emSize: 12.0f, pixelsPerDip: 1.0f);
+
+ displayMetrics.Should().NotBeNull();
+ displayMetrics.DesignUnitsPerEm.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void GetFirstMatchingFont_WithNormalProperties_ShouldReturnFont()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var font = family.GetFirstMatchingFont(FontWeight.Normal, FontStretch.Normal, FontStyle.Normal);
+
+ font.Should().NotBeNull();
+ font.Weight.Should().Be(FontWeight.Normal);
+ font.Style.Should().Be(FontStyle.Normal);
+ }
+
+ [Fact]
+ public void GetFirstMatchingFont_WithBoldWeight_ShouldReturnBoldFont()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var font = family.GetFirstMatchingFont(FontWeight.Bold, FontStretch.Normal, FontStyle.Normal);
+
+ font.Should().NotBeNull();
+ font.Weight.Should().Be(FontWeight.Bold);
+ }
+
+ [Fact]
+ public void GetFirstMatchingFont_WithItalicStyle_ShouldReturnItalicFont()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var font = family.GetFirstMatchingFont(FontWeight.Normal, FontStretch.Normal, FontStyle.Italic);
+
+ font.Should().NotBeNull();
+ font.Style.Should().Be(FontStyle.Italic);
+ }
+
+ [Fact]
+ public void GetMatchingFonts_WithNormalProperties_ShouldReturnFontList()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var matchingFonts = family.GetMatchingFonts(FontWeight.Normal, FontStretch.Normal, FontStyle.Normal);
+
+ matchingFonts.Should().NotBeNull();
+ matchingFonts.Count.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void GetMatchingFonts_ShouldReturnFontsRankedByMatch()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var matchingFonts = family.GetMatchingFonts(FontWeight.Bold, FontStretch.Normal, FontStyle.Normal);
+
+ matchingFonts.Should().NotBeNull();
+ // First font should be the best match (bold)
+ var firstFont = matchingFonts[0u];
+ firstFont.Should().NotBeNull();
+ firstFont.Weight.Should().Be(FontWeight.Bold);
+ }
+
+ [Fact]
+ public void GetMatchingFonts_ShouldBeEnumerable()
+ {
+ var family = TestHelpers.GetArialFamilyOrSkip();
+
+ var matchingFonts = family.GetMatchingFonts(FontWeight.Normal, FontStretch.Normal, FontStyle.Normal);
+
+ var fontList = matchingFonts.ToList();
+ fontList.Should().NotBeEmpty();
+ fontList.Should().AllSatisfy(f => f.Should().NotBeNull());
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontFileAndAnalyzerTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontFileAndAnalyzerTests.cs
new file mode 100644
index 00000000000..128f307a7d9
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontFileAndAnalyzerTests.cs
@@ -0,0 +1,683 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for class.
+///
+public class FontFileTests
+{
+ [Fact]
+ public void CreateFontFile_WithValidPath_ShouldSucceed()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var factory = DWriteFactory.Instance;
+ var fontFile = factory.CreateFontFile(new Uri(TestHelpers.ArialPath));
+
+ fontFile.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void GetUriPath_ShouldReturnPath()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var factory = DWriteFactory.Instance;
+ var fontFile = factory.CreateFontFile(new Uri(TestHelpers.ArialPath));
+
+ var uriPath = fontFile.GetUriPath();
+
+ uriPath.Should().NotBeNullOrEmpty();
+ uriPath.ToLowerInvariant().Should().Contain("arial");
+ }
+
+ [Fact]
+ public void DWriteFontFileNoAddRef_ShouldReturnNonNullPointer()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var factory = DWriteFactory.Instance;
+ var fontFile = factory.CreateFontFile(new Uri(TestHelpers.ArialPath));
+
+ // This returns the native pointer - just verify it doesn't throw
+ // The actual pointer is internal to DirectWrite
+ fontFile.Should().NotBeNull();
+ }
+}
+
+///
+/// Tests for and classes.
+///
+public class FontSourceTests
+{
+ [Fact]
+ public void FontSourceFactory_Create_ShouldReturnFontSource()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var factory = new FontSourceFactory();
+ var fontSource = factory.Create(TestHelpers.ArialPath);
+
+ fontSource.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void FontSource_Uri_ShouldReturnUri()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var fontSource = new FontSource(new Uri(TestHelpers.ArialPath));
+
+ fontSource.Uri.Should().NotBeNull();
+ fontSource.Uri.LocalPath.ToLowerInvariant().Should().Contain("arial");
+ }
+
+ [Fact]
+ public void FontSource_IsComposite_ShouldBeFalseForTtf()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var fontSource = new FontSource(new Uri(TestHelpers.ArialPath));
+
+ fontSource.IsComposite.Should().BeFalse();
+ }
+
+ [Fact]
+ public void FontSource_GetLastWriteTimeUtc_ShouldReturnValidTime()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var fontSource = new FontSource(new Uri(TestHelpers.ArialPath));
+ var lastWriteTime = fontSource.GetLastWriteTimeUtc();
+
+ lastWriteTime.Should().BeBefore(DateTime.UtcNow);
+ lastWriteTime.Year.Should().BeGreaterThan(1990);
+ }
+
+ [Fact]
+ public void FontSource_GetUnmanagedStream_ShouldReturnStream()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var fontSource = new FontSource(new Uri(TestHelpers.ArialPath));
+
+ using var stream = fontSource.GetUnmanagedStream();
+
+ stream.Should().NotBeNull();
+ stream.Length.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void FontSource_TestFileOpenable_ShouldNotThrowForValidFile()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var fontSource = new FontSource(new Uri(TestHelpers.ArialPath));
+
+ // Should not throw for valid file
+ Action act = () => fontSource.TestFileOpenable();
+ act.Should().NotThrow();
+ }
+}
+
+///
+/// Tests for class.
+///
+public class FontFileLoaderTests
+{
+ [Fact]
+ public void FontFileLoader_CanBeConstructedWithFactory()
+ {
+ var factory = new FontSourceFactory();
+ var loader = new FontFileLoader(factory);
+
+ loader.Should().NotBeNull();
+ }
+}
+
+///
+/// Tests for class.
+///
+public class FontFileStreamTests
+{
+ [Fact]
+ public void FontFileStream_CanBeConstructedWithFontSource()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var fontSource = new FontSource(new Uri(TestHelpers.ArialPath));
+ var stream = new FontFileStream(fontSource);
+
+ stream.Should().NotBeNull();
+ }
+}
+
+///
+/// Tests for class.
+/// FontFileEnumerator is a C++/CLI class in DirectWriteForwarder that implements
+/// IDWriteFontFileEnumeratorMirror for enumerating font files in a collection.
+///
+public class FontFileEnumeratorTests
+{
+ [Fact]
+ public unsafe void FontFileEnumerator_CanBeConstructed()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var fontSourceFactory = new FontSourceFactory();
+ var fontFileLoader = new FontFileLoader(fontSourceFactory);
+ var fontSources = new IFontSource[] { new FontSource(new Uri(TestHelpers.ArialPath)) };
+
+ var factory = DWriteFactory.Instance;
+ var enumerator = new FontFileEnumerator(
+ fontSources,
+ fontFileLoader,
+ (Native.IDWriteFactory*)factory.DWriteFactory
+ );
+
+ enumerator.Should().NotBeNull();
+ }
+
+ [Fact]
+ public unsafe void FontFileEnumerator_MoveNext_ReturnsTrueForFirstFile()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var fontSourceFactory = new FontSourceFactory();
+ var fontFileLoader = new FontFileLoader(fontSourceFactory);
+ var fontSources = new IFontSource[] { new FontSource(new Uri(TestHelpers.ArialPath)) };
+
+ var factory = DWriteFactory.Instance;
+ var enumerator = new FontFileEnumerator(
+ fontSources,
+ fontFileLoader,
+ (Native.IDWriteFactory*)factory.DWriteFactory
+ );
+
+ bool hasCurrentFile = false;
+ var hr = enumerator.MoveNext(ref hasCurrentFile);
+
+ hr.Should().Be(0); // S_OK
+ hasCurrentFile.Should().BeTrue();
+ }
+
+ [Fact]
+ public unsafe void FontFileEnumerator_MoveNext_ReturnsFalseAfterLastFile()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var fontSourceFactory = new FontSourceFactory();
+ var fontFileLoader = new FontFileLoader(fontSourceFactory);
+ var fontSources = new IFontSource[] { new FontSource(new Uri(TestHelpers.ArialPath)) };
+
+ var factory = DWriteFactory.Instance;
+ var enumerator = new FontFileEnumerator(
+ fontSources,
+ fontFileLoader,
+ (Native.IDWriteFactory*)factory.DWriteFactory
+ );
+
+ bool hasCurrentFile = false;
+ enumerator.MoveNext(ref hasCurrentFile); // Move to first
+ hasCurrentFile.Should().BeTrue();
+
+ enumerator.MoveNext(ref hasCurrentFile); // Move past last
+ hasCurrentFile.Should().BeFalse();
+ }
+
+ [Fact]
+ public unsafe void FontFileEnumerator_MultipleFiles_EnumeratesAll()
+ {
+ // Get a few font files
+ var fontFiles = Directory.GetFiles(TestHelpers.FontsDirectory, "*.ttf").Take(3).ToArray();
+ Assert.SkipUnless(fontFiles.Length >= 2, "Need at least 2 fonts for this test");
+
+ var fontSourceFactory = new FontSourceFactory();
+ var fontFileLoader = new FontFileLoader(fontSourceFactory);
+ var fontSources = fontFiles.Select(f => (IFontSource)new FontSource(new Uri(f))).ToArray();
+
+ var factory = DWriteFactory.Instance;
+ var enumerator = new FontFileEnumerator(
+ fontSources,
+ fontFileLoader,
+ (Native.IDWriteFactory*)factory.DWriteFactory
+ );
+
+ int count = 0;
+ bool hasCurrentFile = false;
+ while (enumerator.MoveNext(ref hasCurrentFile) == 0 && hasCurrentFile)
+ {
+ count++;
+ }
+
+ count.Should().Be(fontSources.Length);
+ }
+
+ [Fact]
+ public unsafe void FontFileEnumerator_EmptyCollection_MoveNextReturnsFalse()
+ {
+ var fontSourceFactory = new FontSourceFactory();
+ var fontFileLoader = new FontFileLoader(fontSourceFactory);
+ var fontSources = Array.Empty();
+
+ var factory = DWriteFactory.Instance;
+ var enumerator = new FontFileEnumerator(
+ fontSources,
+ fontFileLoader,
+ (Native.IDWriteFactory*)factory.DWriteFactory
+ );
+
+ bool hasCurrentFile = false;
+ var hr = enumerator.MoveNext(ref hasCurrentFile);
+
+ hr.Should().Be(0); // S_OK
+ hasCurrentFile.Should().BeFalse();
+ }
+
+ [Fact]
+ public unsafe void FontFileEnumerator_GetCurrentFontFile_ReturnsValidPointer()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var fontSourceFactory = new FontSourceFactory();
+ var fontFileLoader = new FontFileLoader(fontSourceFactory);
+ var fontSources = new IFontSource[] { new FontSource(new Uri(TestHelpers.ArialPath)) };
+
+ var factory = DWriteFactory.Instance;
+ var enumerator = new FontFileEnumerator(
+ fontSources,
+ fontFileLoader,
+ (Native.IDWriteFactory*)factory.DWriteFactory
+ );
+
+ bool hasCurrentFile = false;
+ enumerator.MoveNext(ref hasCurrentFile);
+ hasCurrentFile.Should().BeTrue();
+
+ Native.IDWriteFontFile* fontFile = null;
+ var hr = enumerator.GetCurrentFontFile(&fontFile);
+
+ hr.Should().Be(0); // S_OK
+ ((IntPtr)fontFile).Should().NotBe(IntPtr.Zero);
+
+ // Release the font file via COM Release (method index 2 in IUnknown vtable)
+ if (fontFile != null)
+ {
+ Marshal.Release((IntPtr)fontFile);
+ }
+ }
+
+ [Fact]
+ public unsafe void FontFileEnumerator_GetCurrentFontFile_WithNullPointer_ReturnsInvalidArg()
+ {
+ TestHelpers.SkipIfArialNotAvailable();
+
+ var fontSourceFactory = new FontSourceFactory();
+ var fontFileLoader = new FontFileLoader(fontSourceFactory);
+ var fontSources = new IFontSource[] { new FontSource(new Uri(TestHelpers.ArialPath)) };
+
+ var factory = DWriteFactory.Instance;
+ var enumerator = new FontFileEnumerator(
+ fontSources,
+ fontFileLoader,
+ (Native.IDWriteFactory*)factory.DWriteFactory
+ );
+
+ bool hasCurrentFile = false;
+ enumerator.MoveNext(ref hasCurrentFile);
+
+ var hr = enumerator.GetCurrentFontFile(null);
+
+ // E_INVALIDARG = 0x80070057
+ hr.Should().Be(unchecked((int)0x80070057));
+ }
+
+ [Fact]
+ public unsafe void FontFileEnumerator_MultipleFontTypes_EnumeratesAll()
+ {
+ // Get different font types
+ var ttfFiles = Directory.GetFiles(TestHelpers.FontsDirectory, "*.ttf").Take(2);
+ var ttcFiles = Directory.GetFiles(TestHelpers.FontsDirectory, "*.ttc").Take(1);
+ var allFiles = ttfFiles.Concat(ttcFiles).ToArray();
+
+ Assert.SkipUnless(allFiles.Length >= 2, "Need at least 2 fonts for this test");
+
+ var fontSourceFactory = new FontSourceFactory();
+ var fontFileLoader = new FontFileLoader(fontSourceFactory);
+ var fontSources = allFiles.Select(f => (IFontSource)new FontSource(new Uri(f))).ToArray();
+
+ var factory = DWriteFactory.Instance;
+ var enumerator = new FontFileEnumerator(
+ fontSources,
+ fontFileLoader,
+ (Native.IDWriteFactory*)factory.DWriteFactory
+ );
+
+ int count = 0;
+ bool hasCurrentFile = false;
+ while (enumerator.MoveNext(ref hasCurrentFile) == 0 && hasCurrentFile)
+ {
+ count++;
+ }
+
+ count.Should().Be(fontSources.Length);
+ }
+}
+
+///
+/// Tests for class.
+///
+public class TextAnalyzerTests
+{
+ [Fact]
+ public void CreateTextAnalyzer_ShouldReturnNonNull()
+ {
+ var factory = DWriteFactory.Instance;
+
+ var textAnalyzer = factory.CreateTextAnalyzer();
+
+ textAnalyzer.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void TextAnalyzer_CanBeCreatedMultipleTimes()
+ {
+ var factory = DWriteFactory.Instance;
+
+ var analyzer1 = factory.CreateTextAnalyzer();
+ var analyzer2 = factory.CreateTextAnalyzer();
+
+ analyzer1.Should().NotBeNull();
+ analyzer2.Should().NotBeNull();
+ analyzer1.Should().NotBeSameAs(analyzer2);
+ }
+}
+
+///
+/// Tests for class.
+///
+public class LocalizedErrorMsgsTests
+{
+ [Fact]
+ public void EnumeratorNotStarted_ShouldBeSet()
+ {
+ // DWriteFactory static constructor sets these
+ var _ = DWriteFactory.Instance;
+
+ LocalizedErrorMsgs.EnumeratorNotStarted.Should().NotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public void EnumeratorReachedEnd_ShouldBeSet()
+ {
+ // DWriteFactory static constructor sets these
+ var _ = DWriteFactory.Instance;
+
+ LocalizedErrorMsgs.EnumeratorReachedEnd.Should().NotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public void ErrorMessages_CanBeSetAndRetrieved()
+ {
+ var originalNotStarted = LocalizedErrorMsgs.EnumeratorNotStarted;
+ var originalReachedEnd = LocalizedErrorMsgs.EnumeratorReachedEnd;
+
+ try
+ {
+ LocalizedErrorMsgs.EnumeratorNotStarted = "Custom not started message";
+ LocalizedErrorMsgs.EnumeratorReachedEnd = "Custom reached end message";
+
+ LocalizedErrorMsgs.EnumeratorNotStarted.Should().Be("Custom not started message");
+ LocalizedErrorMsgs.EnumeratorReachedEnd.Should().Be("Custom reached end message");
+ }
+ finally
+ {
+ // Restore original values
+ LocalizedErrorMsgs.EnumeratorNotStarted = originalNotStarted;
+ LocalizedErrorMsgs.EnumeratorReachedEnd = originalReachedEnd;
+ }
+ }
+}
+
+///
+/// Tests for struct.
+///
+public class ItemPropsTests
+{
+ [Fact]
+ public void ItemProps_DefaultConstructor_ShouldCreateWithDefaults()
+ {
+ var itemProps = new ItemProps();
+
+ itemProps.Should().NotBeNull();
+ itemProps.HasCombiningMark.Should().BeFalse();
+ itemProps.HasExtendedCharacter.Should().BeFalse();
+ itemProps.NeedsCaretInfo.Should().BeFalse();
+ itemProps.IsIndic.Should().BeFalse();
+ itemProps.IsLatin.Should().BeFalse();
+ itemProps.DigitCulture.Should().BeNull();
+ }
+
+ [Fact]
+ public unsafe void ItemProps_Create_WithNullPointers_ShouldSucceed()
+ {
+ var itemProps = ItemProps.Create(
+ scriptAnalysis: null,
+ numberSubstitution: null,
+ digitCulture: null,
+ hasCombiningMark: true,
+ needsCaretInfo: true,
+ hasExtendedCharacter: true,
+ isIndic: false,
+ isLatin: true
+ );
+
+ itemProps.Should().NotBeNull();
+ itemProps.HasCombiningMark.Should().BeTrue();
+ itemProps.NeedsCaretInfo.Should().BeTrue();
+ itemProps.HasExtendedCharacter.Should().BeTrue();
+ itemProps.IsIndic.Should().BeFalse();
+ itemProps.IsLatin.Should().BeTrue();
+ }
+
+ [Fact]
+ public unsafe void ItemProps_Create_WithDigitCulture_ShouldSetCulture()
+ {
+ var culture = System.Globalization.CultureInfo.GetCultureInfo("ar-SA");
+
+ var itemProps = ItemProps.Create(
+ scriptAnalysis: null,
+ numberSubstitution: null,
+ digitCulture: culture,
+ hasCombiningMark: false,
+ needsCaretInfo: false,
+ hasExtendedCharacter: false,
+ isIndic: false,
+ isLatin: false
+ );
+
+ itemProps.DigitCulture.Should().Be(culture);
+ }
+
+ [Fact]
+ public unsafe void ItemProps_CanShapeTogether_WithSameNullPointers_ShouldReturnTrue()
+ {
+ var props1 = ItemProps.Create(null, null, null, false, false, false, false, true);
+ var props2 = ItemProps.Create(null, null, null, true, true, true, true, false);
+
+ // Both have null script analysis and null number substitution, so they can shape together
+ props1.CanShapeTogether(props2).Should().BeTrue();
+ }
+
+ [Fact]
+ public unsafe void ItemProps_ScriptAnalysis_WithNullPointer_ShouldReturnNull()
+ {
+ var itemProps = ItemProps.Create(null, null, null, false, false, false, false, false);
+
+ // ScriptAnalysis returns void*, so cast to check for null
+ void* ptr = itemProps.ScriptAnalysis;
+ ((IntPtr)ptr).Should().Be(IntPtr.Zero);
+ }
+}
+
+///
+/// Tests for struct.
+///
+public class SpanTests
+{
+ [Fact]
+ public void Span_Constructor_ShouldSetElementAndLength()
+ {
+ var element = "test element";
+ var span = new MS.Internal.Span(element, 10);
+
+ span.element.Should().Be(element);
+ span.length.Should().Be(10);
+ }
+
+ [Fact]
+ public void Span_Constructor_WithNullElement_ShouldSucceed()
+ {
+ var span = new MS.Internal.Span(null, 5);
+
+ span.element.Should().BeNull();
+ span.length.Should().Be(5);
+ }
+
+ [Fact]
+ public void Span_Constructor_WithZeroLength_ShouldSucceed()
+ {
+ var span = new MS.Internal.Span("element", 0);
+
+ span.length.Should().Be(0);
+ }
+
+ [Fact]
+ public void Span_CanHoldItemProps()
+ {
+ var itemProps = new ItemProps();
+ var span = new MS.Internal.Span(itemProps, 100);
+
+ span.element.Should().Be(itemProps);
+ span.length.Should().Be(100);
+ }
+}
+
+///
+/// Tests for class.
+///
+public class ClassificationUtilityTests
+{
+ [Fact]
+ public void ClassificationUtility_Instance_ShouldNotBeNull()
+ {
+ var instance = ClassificationUtility.Instance;
+
+ instance.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void GetCharAttribute_ForLatinA_ShouldReturnLatinAndNotDigit()
+ {
+ var instance = ClassificationUtility.Instance;
+
+ instance.GetCharAttribute(
+ unicodeScalar: 'A', // Latin capital A
+ out bool isCombining,
+ out bool needsCaretInfo,
+ out bool isIndic,
+ out bool isDigit,
+ out bool isLatin,
+ out bool isStrong
+ );
+
+ isCombining.Should().BeFalse();
+ needsCaretInfo.Should().BeFalse();
+ isDigit.Should().BeFalse();
+ isLatin.Should().BeTrue();
+ isIndic.Should().BeFalse();
+ isStrong.Should().BeTrue(); // Latin A is a strong character
+ }
+
+ [Fact]
+ public void GetCharAttribute_ForDigit_ShouldReturnIsDigit()
+ {
+ var instance = ClassificationUtility.Instance;
+
+ instance.GetCharAttribute(
+ unicodeScalar: '0', // Digit zero
+ out bool isCombining,
+ out bool needsCaretInfo,
+ out bool isIndic,
+ out bool isDigit,
+ out bool isLatin,
+ out bool isStrong
+ );
+
+ isCombining.Should().BeFalse();
+ needsCaretInfo.Should().BeFalse();
+ isDigit.Should().BeTrue();
+ isLatin.Should().BeFalse();
+ isIndic.Should().BeFalse();
+ // Digits may or may not be "strong" depending on classification - don't assert
+ _ = isStrong; // suppress IDE0059
+ }
+
+ [Fact]
+ public void GetCharAttribute_ForDevanagari_ShouldReturnIndicAndNeedsCaretInfo()
+ {
+ var instance = ClassificationUtility.Instance;
+
+ // Devanagari letter A (U+0905)
+ instance.GetCharAttribute(
+ unicodeScalar: 0x0905,
+ out bool isCombining,
+ out bool needsCaretInfo,
+ out bool isIndic,
+ out bool isDigit,
+ out bool isLatin,
+ out bool isStrong
+ );
+
+ isCombining.Should().BeFalse();
+ isIndic.Should().BeTrue();
+ needsCaretInfo.Should().BeTrue();
+ isLatin.Should().BeFalse();
+ isDigit.Should().BeFalse();
+ isStrong.Should().BeTrue();
+ }
+
+ [Fact]
+ public void GetCharAttribute_ForCombiningMark_ShouldReturnIsCombining()
+ {
+ var instance = ClassificationUtility.Instance;
+
+ // Combining Acute Accent (U+0301) is in the Latin Extended block
+ instance.GetCharAttribute(
+ unicodeScalar: 0x0301,
+ out bool isCombining,
+ out bool needsCaretInfo,
+ out bool isIndic,
+ out bool isDigit,
+ out bool isLatin,
+ out bool isStrong
+ );
+
+ isCombining.Should().BeTrue();
+ needsCaretInfo.Should().BeFalse();
+ isIndic.Should().BeFalse();
+ isDigit.Should().BeFalse();
+ // isLatin may be true since U+0301 is in the Latin Extended block
+ _ = isLatin;
+ isStrong.Should().BeFalse(); // Combining marks are not strong
+ }
+
+ [Fact]
+ public void ScriptCaretInfo_ShouldHaveExpectedLength()
+ {
+ ClassificationUtility.ScriptCaretInfo.Should().NotBeEmpty();
+ ClassificationUtility.ScriptCaretInfo.Length.Should().BeGreaterThan(50);
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontMetricsTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontMetricsTests.cs
new file mode 100644
index 00000000000..15085177476
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontMetricsTests.cs
@@ -0,0 +1,158 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for value struct.
+///
+public class FontMetricsTests
+{
+ [Fact]
+ public void DefaultValues_ShouldBeZero()
+ {
+ var metrics = new FontMetrics();
+
+ metrics.DesignUnitsPerEm.Should().Be(0);
+ metrics.Ascent.Should().Be(0);
+ metrics.Descent.Should().Be(0);
+ metrics.LineGap.Should().Be(0);
+ metrics.CapHeight.Should().Be(0);
+ metrics.XHeight.Should().Be(0);
+ metrics.UnderlinePosition.Should().Be(0);
+ metrics.UnderlineThickness.Should().Be(0);
+ metrics.StrikethroughPosition.Should().Be(0);
+ metrics.StrikethroughThickness.Should().Be(0);
+ }
+
+ [Fact]
+ public void Baseline_ShouldCalculateCorrectly()
+ {
+ // Baseline = (Ascent + LineGap * 0.5) / DesignUnitsPerEm
+ var metrics = new FontMetrics
+ {
+ DesignUnitsPerEm = 1000,
+ Ascent = 800,
+ LineGap = 100
+ };
+
+ // Expected: (800 + 100 * 0.5) / 1000 = 850 / 1000 = 0.85
+ metrics.Baseline.Should().BeApproximately(0.85, 0.0001);
+ }
+
+ [Fact]
+ public void Baseline_WithNegativeLineGap_ShouldCalculateCorrectly()
+ {
+ var metrics = new FontMetrics
+ {
+ DesignUnitsPerEm = 2048,
+ Ascent = 1900,
+ LineGap = -100
+ };
+
+ // Expected: (1900 + (-100) * 0.5) / 2048 = 1850 / 2048
+ double expected = 1850.0 / 2048.0;
+ metrics.Baseline.Should().BeApproximately(expected, 0.0001);
+ }
+
+ [Fact]
+ public void LineSpacing_ShouldCalculateCorrectly()
+ {
+ // LineSpacing = (Ascent + Descent + LineGap) / DesignUnitsPerEm
+ var metrics = new FontMetrics
+ {
+ DesignUnitsPerEm = 1000,
+ Ascent = 800,
+ Descent = 200,
+ LineGap = 100
+ };
+
+ // Expected: (800 + 200 + 100) / 1000 = 1100 / 1000 = 1.1
+ metrics.LineSpacing.Should().BeApproximately(1.1, 0.0001);
+ }
+
+ [Fact]
+ public void LineSpacing_WithZeroLineGap_ShouldCalculateCorrectly()
+ {
+ var metrics = new FontMetrics
+ {
+ DesignUnitsPerEm = 2048,
+ Ascent = 1900,
+ Descent = 500,
+ LineGap = 0
+ };
+
+ // Expected: (1900 + 500 + 0) / 2048 = 2400 / 2048
+ double expected = 2400.0 / 2048.0;
+ metrics.LineSpacing.Should().BeApproximately(expected, 0.0001);
+ }
+
+ [Fact]
+ public void LineSpacing_WithNegativeLineGap_ShouldCalculateCorrectly()
+ {
+ var metrics = new FontMetrics
+ {
+ DesignUnitsPerEm = 1000,
+ Ascent = 800,
+ Descent = 200,
+ LineGap = -50
+ };
+
+ // Expected: (800 + 200 + (-50)) / 1000 = 950 / 1000 = 0.95
+ metrics.LineSpacing.Should().BeApproximately(0.95, 0.0001);
+ }
+
+ [Theory]
+ [InlineData((ushort)1000, (ushort)750, (ushort)250, (short)0)]
+ [InlineData((ushort)2048, (ushort)1900, (ushort)500, (short)100)]
+ [InlineData((ushort)1024, (ushort)800, (ushort)200, (short)-50)]
+ public void FontMetrics_VariousValues_CalculationsAreConsistent(
+ ushort designUnitsPerEm,
+ ushort ascent,
+ ushort descent,
+ short lineGap)
+ {
+ var metrics = new FontMetrics
+ {
+ DesignUnitsPerEm = designUnitsPerEm,
+ Ascent = ascent,
+ Descent = descent,
+ LineGap = lineGap
+ };
+
+ double expectedBaseline = (ascent + lineGap * 0.5) / designUnitsPerEm;
+ double expectedLineSpacing = (double)(ascent + descent + lineGap) / designUnitsPerEm;
+
+ metrics.Baseline.Should().BeApproximately(expectedBaseline, 0.0001);
+ metrics.LineSpacing.Should().BeApproximately(expectedLineSpacing, 0.0001);
+ }
+
+ [Fact]
+ public void AllFields_CanBeSetAndRetrieved()
+ {
+ var metrics = new FontMetrics
+ {
+ DesignUnitsPerEm = 2048,
+ Ascent = 1900,
+ Descent = 500,
+ LineGap = 100,
+ CapHeight = 1400,
+ XHeight = 1000,
+ UnderlinePosition = -200,
+ UnderlineThickness = 100,
+ StrikethroughPosition = 600,
+ StrikethroughThickness = 80
+ };
+
+ metrics.DesignUnitsPerEm.Should().Be(2048);
+ metrics.Ascent.Should().Be(1900);
+ metrics.Descent.Should().Be(500);
+ metrics.LineGap.Should().Be(100);
+ metrics.CapHeight.Should().Be(1400);
+ metrics.XHeight.Should().Be(1000);
+ metrics.UnderlinePosition.Should().Be(-200);
+ metrics.UnderlineThickness.Should().Be(100);
+ metrics.StrikethroughPosition.Should().Be(600);
+ metrics.StrikethroughThickness.Should().Be(80);
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontTests.cs
new file mode 100644
index 00000000000..4947b80e25e
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/FontTests.cs
@@ -0,0 +1,724 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for class.
+/// Arial Regular is used as a known reference font with documented properties.
+///
+public class FontTests
+{
+ // Known Arial Regular properties (standard Windows font)
+ // These values are consistent across Windows versions
+ private const int ArialRegularWeight = 400; // Normal
+ private const int ArialRegularStretch = 5; // Normal
+ private const int ArialRegularStyle = 0; // Normal
+ private const ushort ArialDesignUnitsPerEm = 2048;
+
+ [Fact]
+ public void Weight_ArialRegular_ShouldBeNormal()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ // Arial Regular should have weight 400 (Normal)
+ font.Weight.Should().Be(FontWeight.Normal);
+ ((int)font.Weight).Should().Be(ArialRegularWeight);
+ }
+
+ [Fact]
+ public void Stretch_ArialRegular_ShouldBeNormal()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ // Arial Regular should have stretch 5 (Normal)
+ font.Stretch.Should().Be(FontStretch.Normal);
+ ((int)font.Stretch).Should().Be(ArialRegularStretch);
+ }
+
+ [Fact]
+ public void Style_ArialRegular_ShouldBeNormal()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ // Arial Regular should have style 0 (Normal)
+ font.Style.Should().Be(FontStyle.Normal);
+ ((int)font.Style).Should().Be(ArialRegularStyle);
+ }
+
+ [Fact]
+ public void Family_ShouldNotBeNull()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ font.Family.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void FaceNames_ShouldContainRegular()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ var faceNames = font.FaceNames;
+
+ faceNames.Should().NotBeNull();
+ faceNames.Count.Should().BeGreaterThan(0);
+
+ // Arial Regular should have "Regular" as a face name
+ faceNames.Values.Should().Contain("Regular");
+ }
+
+ [Fact]
+ public void Metrics_ArialDesignUnitsPerEm_ShouldBe2048()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ var metrics = font.Metrics;
+
+ metrics.Should().NotBeNull();
+ // Arial has 2048 design units per em (standard for TrueType fonts)
+ metrics.DesignUnitsPerEm.Should().Be(ArialDesignUnitsPerEm);
+ metrics.Ascent.Should().BeGreaterThan(0);
+ metrics.Descent.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void Metrics_AscentPlusDescent_ShouldBeLessThanDesignUnits()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ var metrics = font.Metrics;
+
+ // Ascent + Descent should not exceed design units per em (basic sanity check)
+ (metrics.Ascent + metrics.Descent).Should().BeLessThanOrEqualTo(metrics.DesignUnitsPerEm * 2);
+ }
+
+ [Fact]
+ public void DisplayMetrics_ShouldReturnValidGdiCompatibleMetrics()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ var displayMetrics = font.DisplayMetrics(12.0f, 96.0f);
+
+ displayMetrics.Should().NotBeNull();
+ // GDI compatible metrics are still in design units, but snapped to pixel grid
+ // They should be comparable to the regular design metrics
+ var designMetrics = font.Metrics;
+ displayMetrics.DesignUnitsPerEm.Should().Be(designMetrics.DesignUnitsPerEm);
+ displayMetrics.Ascent.Should().BeGreaterThan(0);
+ displayMetrics.Descent.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void GetFontFace_ShouldReturnValidFontFace()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ var fontFace = font.GetFontFace();
+
+ fontFace.Should().NotBeNull();
+ fontFace.Release();
+ }
+
+ [Fact]
+ public void SimulationFlags_ShouldBeNoneForRegularFont()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ // A regular font without simulations should have None
+ font.SimulationFlags.Should().Be(FontSimulations.None);
+ }
+
+ [Fact]
+ public void Version_ShouldBePositive()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ font.Version.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void IsSymbolFont_ForArial_ShouldBeFalse()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ // Arial is not a symbol font
+ font.IsSymbolFont.Should().BeFalse();
+ }
+
+ [Fact]
+ public void SymbolFont_ShouldBeIdentifiedCorrectly()
+ {
+ var fontCollection = DWriteFactory.SystemFontCollection;
+
+ // Try to find a symbol font like Wingdings or Symbol
+ var symbolFamilyNames = new[] { "Wingdings", "Symbol", "Webdings" };
+
+ foreach (var familyName in symbolFamilyNames)
+ {
+ var family = fontCollection[familyName];
+ if (family != null && family.Count > 0)
+ {
+ var font = family[0u];
+ font.IsSymbolFont.Should().BeTrue($"{familyName} should be identified as a symbol font");
+ return;
+ }
+ }
+
+ Assert.Skip("No symbol fonts found on system");
+ }
+
+ [Fact]
+ public void HasCharacter_WithAsciiLetters_ShouldReturnTrue()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ // All ASCII letters should be in Arial
+ for (uint codePoint = 'A'; codePoint <= 'Z'; codePoint++)
+ {
+ font.HasCharacter(codePoint).Should().BeTrue($"Arial should have character U+{codePoint:X4} ('{(char)codePoint}')");
+ }
+ for (uint codePoint = 'a'; codePoint <= 'z'; codePoint++)
+ {
+ font.HasCharacter(codePoint).Should().BeTrue($"Arial should have character U+{codePoint:X4} ('{(char)codePoint}')");
+ }
+ }
+
+ [Fact]
+ public void HasCharacter_WithDigits_ShouldReturnTrue()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ // All ASCII digits should be in Arial
+ for (uint codePoint = '0'; codePoint <= '9'; codePoint++)
+ {
+ font.HasCharacter(codePoint).Should().BeTrue($"Arial should have character U+{codePoint:X4} ('{(char)codePoint}')");
+ }
+ }
+
+ [Fact]
+ public void HasCharacter_WithRareCharacter_ShouldReturnFalse()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ // Very high code point that's unlikely to be in Arial
+ font.HasCharacter(0x1F999).Should().BeFalse(); // Emoji unicorn
+ }
+
+ [Fact]
+ public void GetInformationalStrings_FamilyName_ShouldContainArial()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ bool exists = font.GetInformationalStrings(InformationalStringID.WIN32FamilyNames, out var familyNames);
+
+ exists.Should().BeTrue();
+ familyNames.Should().NotBeNull();
+ // Should contain "Arial" in the localized names
+ familyNames!.Values.Should().Contain("Arial");
+ }
+
+ [Fact]
+ public void GetInformationalStrings_DesignerName_ShouldNotThrow()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ // May or may not have designer info, but shouldn't throw
+ _ = font.GetInformationalStrings(InformationalStringID.Designer, out _);
+ // Result may be null if this info string is not present
+ }
+
+ [Fact]
+ public void GetInformationalStrings_CopyrightNotice_ShouldExistAndContainMicrosoft()
+ {
+ var font = TestHelpers.GetArialFontOrSkip();
+
+ bool exists = font.GetInformationalStrings(InformationalStringID.CopyrightNotice, out var copyright);
+
+ exists.Should().BeTrue("Arial should have copyright information");
+ copyright.Should().NotBeNull();
+
+ // Arial is a Microsoft/Monotype font
+ var copyrightText = copyright!.Values.FirstOrDefault() ?? "";
+ copyrightText.Should().NotBeEmpty();
+ }
+}
+
+///
+/// Tests for class.
+/// Arial is used as a known reference font with documented properties.
+///
+public class FontFaceTests
+{
+ // Known Arial metrics
+ private const ushort ArialDesignUnitsPerEm = 2048;
+ private const ushort ArialGlyphCountMinimum = 1000; // Arial has 1000+ glyphs
+
+ private FontFace GetArialFontFace()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ return factory.CreateFontFace(new Uri(arialPath), 0);
+ }
+
+ [Fact]
+ public void Type_ArialTtf_ShouldBeTrueType()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ // arial.ttf is a TrueType font (not TrueTypeCollection)
+ fontFace.Type.Should().Be(FontFaceType.TrueType);
+ // WPF FontFaceType enum: CFF=0, TrueType=1, TrueTypeCollection=2, etc.
+ ((int)fontFace.Type).Should().Be(1, "FontFaceType.TrueType has value 1 in WPF enum");
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void Index_ForSingleFontFile_ShouldBeZero()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ // Single font file (not TTC) should have index 0
+ fontFace.Index.Should().Be(0u);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void SimulationFlags_WithNoSimulations_ShouldBeNone()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ fontFace.SimulationFlags.Should().Be(FontSimulations.None);
+ ((int)fontFace.SimulationFlags).Should().Be(0);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void Metrics_Arial_ShouldHave2048DesignUnits()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ var metrics = fontFace.Metrics;
+
+ metrics.Should().NotBeNull();
+ metrics.DesignUnitsPerEm.Should().Be(ArialDesignUnitsPerEm);
+ metrics.Ascent.Should().BeGreaterThan(0);
+ metrics.Descent.Should().BeGreaterThan(0);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void IsSymbolFont_ForArial_ShouldBeFalse()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ fontFace.IsSymbolFont.Should().BeFalse();
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void GlyphCount_Arial_ShouldBeOver1000()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ // Arial has a large character set with 1000+ glyphs
+ fontFace.GlyphCount.Should().BeGreaterThanOrEqualTo(ArialGlyphCountMinimum);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void FontFace_WithBoldSimulation_ShouldHaveBoldFlag()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0, FontSimulations.Bold);
+ try
+ {
+ fontFace.SimulationFlags.Should().Be(FontSimulations.Bold);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void FontFace_WithObliqueSimulation_ShouldHaveObliqueFlag()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0, FontSimulations.Oblique);
+ try
+ {
+ fontFace.SimulationFlags.Should().Be(FontSimulations.Oblique);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void FontFace_WithCombinedSimulations_ShouldHaveBothFlags()
+ {
+ var factory = DWriteFactory.Instance;
+ TestHelpers.SkipIfArialNotAvailable();
+ var arialPath = TestHelpers.ArialPath;
+
+ var fontFace = factory.CreateFontFace(new Uri(arialPath), 0, FontSimulations.Bold | FontSimulations.Oblique);
+ try
+ {
+ fontFace.SimulationFlags.Should().HaveFlag(FontSimulations.Bold);
+ fontFace.SimulationFlags.Should().HaveFlag(FontSimulations.Oblique);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void GetFileZero_ShouldReturnFontFile()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ var fontFile = fontFace.GetFileZero();
+ fontFile.Should().NotBeNull();
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void TryGetFontTable_WithHeadTable_ShouldReturnValidHeader()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ // 'head' table should exist in all TrueType fonts
+ bool found = fontFace.TryGetFontTable(OpenTypeTableTag.FontHeader, out byte[]? tableData);
+
+ found.Should().BeTrue();
+ tableData.Should().NotBeNull();
+
+ // head table is exactly 54 bytes
+ tableData!.Length.Should().Be(54, "head table should be exactly 54 bytes");
+
+ // Verify magic number at offset 12 (should be 0x5F0F3CF5)
+ uint magicNumber = System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(tableData.AsSpan(12, 4));
+ magicNumber.Should().Be(0x5F0F3CF5, "head table should contain magic number 0x5F0F3CF5");
+
+ // Verify unitsPerEm at offset 18 matches what we expect
+ ushort unitsPerEm = System.Buffers.Binary.BinaryPrimitives.ReadUInt16BigEndian(tableData.AsSpan(18, 2));
+ unitsPerEm.Should().Be(ArialDesignUnitsPerEm, "unitsPerEm in head table should match font metrics");
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void TryGetFontTable_WithCmapTable_ShouldReturnValidStructure()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ // 'cmap' table (character to glyph mapping) should exist
+ bool found = fontFace.TryGetFontTable(OpenTypeTableTag.CharToIndexMap, out byte[]? tableData);
+
+ found.Should().BeTrue();
+ tableData.Should().NotBeNull();
+ tableData!.Length.Should().BeGreaterThan(4);
+
+ // cmap table version should be 0
+ ushort version = System.Buffers.Binary.BinaryPrimitives.ReadUInt16BigEndian(tableData.AsSpan(0, 2));
+ version.Should().Be(0, "cmap table version should be 0");
+
+ // Number of encoding tables should be at least 1
+ ushort numTables = System.Buffers.Binary.BinaryPrimitives.ReadUInt16BigEndian(tableData.AsSpan(2, 2));
+ numTables.Should().BeGreaterThan(0, "cmap should have at least one encoding table");
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void TryGetFontTable_WithNonExistentTable_ShouldReturnFalse()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ // 'JSTF' (Justification) table typically doesn't exist in Arial
+ bool found = fontFace.TryGetFontTable(OpenTypeTableTag.TTO_JSTF, out byte[]? tableData);
+
+ // May or may not exist, but should not throw
+ if (!found)
+ {
+ tableData.Should().BeNull();
+ }
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public void ReadFontEmbeddingRights_Arial_ShouldReturnInstallableValue()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ bool success = fontFace.ReadFontEmbeddingRights(out ushort fsType);
+
+ // Arial should have OS/2 table with embedding rights
+ success.Should().BeTrue();
+ // fsType bits 0-3 indicate embedding licensing rights
+ // Arial typically allows embedding (fsType & 0x000F should not be 0x0002 which is "restricted")
+ (fsType & 0x0002).Should().Be(0, "Arial should not have restricted embedding");
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetArrayOfGlyphIndices_WithValidCodePoints_ShouldReturnConsistentGlyphIndices()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ // Get glyph indices for 'A', 'B', 'C' (code points 65, 66, 67)
+ uint[] codePoints = [65, 66, 67];
+ ushort[] glyphIndices = new ushort[codePoints.Length];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, (uint)codePoints.Length, pGlyphIndices);
+ }
+
+ // All glyphs should be non-zero (0 is typically .notdef)
+ glyphIndices[0].Should().BeGreaterThan((ushort)0, "Glyph for 'A' should exist");
+ glyphIndices[1].Should().BeGreaterThan((ushort)0, "Glyph for 'B' should exist");
+ glyphIndices[2].Should().BeGreaterThan((ushort)0, "Glyph for 'C' should exist");
+
+ // Adjacent characters should have different glyph indices
+ glyphIndices[0].Should().NotBe(glyphIndices[1], "'A' and 'B' should have different glyph indices");
+ glyphIndices[1].Should().NotBe(glyphIndices[2], "'B' and 'C' should have different glyph indices");
+
+ // Glyph indices should be consistent when called again
+ ushort[] glyphIndices2 = new ushort[codePoints.Length];
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices2)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, (uint)codePoints.Length, pGlyphIndices);
+ }
+ glyphIndices.Should().Equal(glyphIndices2, "Glyph indices should be consistent across calls");
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetArrayOfGlyphIndices_WithUnsupportedCodePoint_ShouldReturnZero()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ // Very high code point unlikely to be in Arial
+ uint[] codePoints = [0x1F999]; // Unicorn emoji
+ ushort[] glyphIndices = new ushort[1];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, 1, pGlyphIndices);
+ }
+
+ // Should return 0 (.notdef) for unsupported character
+ glyphIndices[0].Should().Be(0);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetDesignGlyphMetrics_ShouldReturnValidMetricsInDesignUnits()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ // First get glyph indices for 'A'
+ uint[] codePoints = [65]; // 'A'
+ ushort[] glyphIndices = new ushort[1];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, 1, pGlyphIndices);
+ }
+
+ // Now get design metrics for that glyph
+ GlyphMetrics[] metrics = new GlyphMetrics[1];
+
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ fixed (GlyphMetrics* pMetrics = metrics)
+ {
+ fontFace.GetDesignGlyphMetrics(pGlyphIndices, 1, pMetrics);
+ }
+
+ // 'A' should have positive advance width
+ metrics[0].AdvanceWidth.Should().BeGreaterThan(0);
+
+ // Advance width should be reasonable (less than design units per em for a single glyph)
+ metrics[0].AdvanceWidth.Should().BeLessThan(ArialDesignUnitsPerEm,
+ "Advance width should be less than design units per em");
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetDisplayGlyphMetrics_ShouldReturnMetrics()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ // First get glyph indices for 'A'
+ uint[] codePoints = [65]; // 'A'
+ ushort[] glyphIndices = new ushort[1];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, 1, pGlyphIndices);
+ }
+
+ // Get display metrics at 12pt, 96 DPI
+ GlyphMetrics[] metrics = new GlyphMetrics[1];
+
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ fixed (GlyphMetrics* pMetrics = metrics)
+ {
+ fontFace.GetDisplayGlyphMetrics(
+ pGlyphIndices,
+ 1,
+ pMetrics,
+ emSize: 12.0f,
+ useDisplayNatural: false,
+ isSideways: false,
+ pixelsPerDip: 1.0f);
+ }
+
+ // Display metrics should have positive advance width
+ metrics[0].AdvanceWidth.Should().BeGreaterThan(0);
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetDesignGlyphMetrics_MultipleGlyphs_ShouldReturnAllMetrics()
+ {
+ var fontFace = GetArialFontFace();
+
+ try
+ {
+ // Get glyph indices for 'Hello'
+ uint[] codePoints = [72, 101, 108, 108, 111]; // H, e, l, l, o
+ ushort[] glyphIndices = new ushort[codePoints.Length];
+
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, (uint)codePoints.Length, pGlyphIndices);
+ }
+
+ // Get metrics for all glyphs
+ GlyphMetrics[] metrics = new GlyphMetrics[codePoints.Length];
+
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ fixed (GlyphMetrics* pMetrics = metrics)
+ {
+ fontFace.GetDesignGlyphMetrics(pGlyphIndices, (uint)codePoints.Length, pMetrics);
+ }
+
+ // All visible characters should have positive advance width
+ for (int i = 0; i < metrics.Length; i++)
+ {
+ metrics[i].AdvanceWidth.Should().BeGreaterThan(0, $"Glyph at index {i} should have positive advance width");
+ }
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/GlobalUsings.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/GlobalUsings.cs
new file mode 100644
index 00000000000..ce1409e264b
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/GlobalUsings.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+global using Xunit;
+global using FluentAssertions;
+
+global using System;
+global using System.Collections.Generic;
+global using System.IO;
+global using System.Linq;
+global using System.Runtime.InteropServices;
+
+global using MS.Internal.FontCache;
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/GlyphMetricsTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/GlyphMetricsTests.cs
new file mode 100644
index 00000000000..0c692589298
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/GlyphMetricsTests.cs
@@ -0,0 +1,113 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for value struct.
+///
+public class GlyphMetricsTests
+{
+ [Fact]
+ public void DefaultValues_ShouldBeZero()
+ {
+ var metrics = new GlyphMetrics();
+
+ metrics.LeftSideBearing.Should().Be(0);
+ metrics.AdvanceWidth.Should().Be(0);
+ metrics.RightSideBearing.Should().Be(0);
+ metrics.TopSideBearing.Should().Be(0);
+ metrics.AdvanceHeight.Should().Be(0);
+ metrics.BottomSideBearing.Should().Be(0);
+ metrics.VerticalOriginY.Should().Be(0);
+ }
+
+ [Fact]
+ public void AllFields_CanBeSetAndRetrieved()
+ {
+ var metrics = new GlyphMetrics
+ {
+ LeftSideBearing = -10,
+ AdvanceWidth = 500,
+ RightSideBearing = 20,
+ TopSideBearing = 50,
+ AdvanceHeight = 1000,
+ BottomSideBearing = -30,
+ VerticalOriginY = 800
+ };
+
+ metrics.LeftSideBearing.Should().Be(-10);
+ metrics.AdvanceWidth.Should().Be(500);
+ metrics.RightSideBearing.Should().Be(20);
+ metrics.TopSideBearing.Should().Be(50);
+ metrics.AdvanceHeight.Should().Be(1000);
+ metrics.BottomSideBearing.Should().Be(-30);
+ metrics.VerticalOriginY.Should().Be(800);
+ }
+
+ [Fact]
+ public void NegativeLeftSideBearing_IndicatesOverhangToLeft()
+ {
+ // A negative LeftSideBearing means the black box extends to the left of the origin
+ // (often true for lowercase italic 'f')
+ var metrics = new GlyphMetrics
+ {
+ LeftSideBearing = -50,
+ AdvanceWidth = 400
+ };
+
+ metrics.LeftSideBearing.Should().BeNegative();
+ }
+
+ [Fact]
+ public void NegativeRightSideBearing_IndicatesOverhangToRight()
+ {
+ // A negative RightSideBearing means the right edge of the black box
+ // overhangs the layout box
+ var metrics = new GlyphMetrics
+ {
+ RightSideBearing = -25,
+ AdvanceWidth = 500
+ };
+
+ metrics.RightSideBearing.Should().BeNegative();
+ }
+
+ [Theory]
+ [InlineData(-100, 600, 50)]
+ [InlineData(0, 500, 0)]
+ [InlineData(25, 450, -20)]
+ public void HorizontalMetrics_VariousValues(int leftBearing, uint advanceWidth, int rightBearing)
+ {
+ var metrics = new GlyphMetrics
+ {
+ LeftSideBearing = leftBearing,
+ AdvanceWidth = advanceWidth,
+ RightSideBearing = rightBearing
+ };
+
+ metrics.LeftSideBearing.Should().Be(leftBearing);
+ metrics.AdvanceWidth.Should().Be(advanceWidth);
+ metrics.RightSideBearing.Should().Be(rightBearing);
+ }
+
+ [Theory]
+ [InlineData(100, 1200, -50, 900)]
+ [InlineData(0, 1000, 0, 800)]
+ [InlineData(-20, 1100, 30, 850)]
+ public void VerticalMetrics_VariousValues(int topBearing, uint advanceHeight, int bottomBearing, int verticalOriginY)
+ {
+ var metrics = new GlyphMetrics
+ {
+ TopSideBearing = topBearing,
+ AdvanceHeight = advanceHeight,
+ BottomSideBearing = bottomBearing,
+ VerticalOriginY = verticalOriginY
+ };
+
+ metrics.TopSideBearing.Should().Be(topBearing);
+ metrics.AdvanceHeight.Should().Be(advanceHeight);
+ metrics.BottomSideBearing.Should().Be(bottomBearing);
+ metrics.VerticalOriginY.Should().Be(verticalOriginY);
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/LocalizedStringsTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/LocalizedStringsTests.cs
new file mode 100644
index 00000000000..fb3f8b23fa5
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/LocalizedStringsTests.cs
@@ -0,0 +1,214 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Globalization;
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for class.
+///
+public class LocalizedStringsTests
+{
+ [Fact]
+ public void Count_ShouldBeGreaterThanZero()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ localizedStrings.Count.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void Keys_ShouldContainCultureInfo()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var keys = localizedStrings.Keys;
+
+ keys.Should().NotBeEmpty();
+ keys.Should().AllBeOfType();
+ }
+
+ [Fact]
+ public void Values_ShouldContainStrings()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var values = localizedStrings.Values;
+
+ values.Should().NotBeEmpty();
+ values.Should().AllBeOfType();
+ }
+
+ [Fact]
+ public void Indexer_WithValidCulture_ShouldReturnName()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ // Get the first available culture
+ var firstCulture = TestHelpers.GetFirstCultureOrSkip(localizedStrings);
+
+ var name = localizedStrings[firstCulture];
+
+ name.Should().NotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public void ContainsKey_WithExistingCulture_ShouldReturnTrue()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var firstCulture = TestHelpers.GetFirstCultureOrSkip(localizedStrings);
+
+ localizedStrings.ContainsKey(firstCulture).Should().BeTrue();
+ }
+
+ [Fact]
+ public void TryGetValue_WithExistingCulture_ShouldReturnTrueAndValue()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var firstCulture = TestHelpers.GetFirstCultureOrSkip(localizedStrings);
+
+ bool result = localizedStrings.TryGetValue(firstCulture, out string value);
+
+ result.Should().BeTrue();
+ value.Should().NotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public void GetEnumerator_ShouldEnumerateAllPairs()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var pairs = localizedStrings.ToList();
+
+ pairs.Should().NotBeEmpty();
+ pairs.Should().AllSatisfy(kvp =>
+ {
+ kvp.Key.Should().NotBeNull();
+ kvp.Value.Should().NotBeNullOrEmpty();
+ });
+ }
+
+ [Fact]
+ public void EnglishName_ShouldExist()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ // Most fonts should have an English (US) name
+ var englishCulture = new CultureInfo("en-US");
+
+ if (localizedStrings.ContainsKey(englishCulture))
+ {
+ var englishName = localizedStrings[englishCulture];
+ englishName.Should().Contain("Arial");
+ }
+ }
+
+ [Fact]
+ public void IsReadOnly_ShouldBeTrue()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ localizedStrings.IsReadOnly.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Add_ShouldThrowNotSupportedException()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ Action act = () => localizedStrings.Add(CultureInfo.InvariantCulture, "Test");
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void Remove_ShouldThrowNotSupportedException()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var firstCulture = TestHelpers.GetFirstCultureOrSkip(localizedStrings);
+
+ Action act = () => localizedStrings.Remove(firstCulture);
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void Clear_ShouldThrowNotSupportedException()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ Action act = () => localizedStrings.Clear();
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void StringsCount_ShouldMatchCount()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ localizedStrings.StringsCount.Should().Be((uint)localizedStrings.Count);
+ }
+
+ [Fact]
+ public void FindLocaleName_WithEnUS_ShouldReturnTrueAndIndex()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ bool found = localizedStrings.FindLocaleName("en-US", out uint index);
+
+ // en-US should typically exist for Arial
+ if (found)
+ {
+ index.Should().BeGreaterThanOrEqualTo(0);
+ }
+ }
+
+ [Fact]
+ public void GetLocaleName_WithValidIndex_ShouldReturnLocaleName()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var localeName = localizedStrings.GetLocaleName(0);
+
+ localeName.Should().NotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public void GetString_WithValidIndex_ShouldReturnString()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var str = localizedStrings.GetString(0);
+
+ str.Should().NotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public void Contains_ShouldThrowNotImplementedException()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var firstCulture = TestHelpers.GetFirstCultureOrSkip(localizedStrings);
+
+ var pair = new KeyValuePair(firstCulture, localizedStrings[firstCulture]);
+
+ Action act = () => localizedStrings.Contains(pair);
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void CopyTo_ShouldCopyPairsToArray()
+ {
+ var localizedStrings = TestHelpers.GetArialLocalizedStringsOrSkip();
+
+ var array = new KeyValuePair[localizedStrings.Count];
+ localizedStrings.CopyTo(array, 0);
+
+ array.Should().AllSatisfy(kvp => kvp.Key.Should().NotBeNull());
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TestHelpers.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TestHelpers.cs
new file mode 100644
index 00000000000..959114f2c45
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TestHelpers.cs
@@ -0,0 +1,99 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Globalization;
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Helper methods for DirectWriteForwarder tests.
+///
+internal static class TestHelpers
+{
+ ///
+ /// Path to the Windows Fonts directory.
+ ///
+ public static string FontsDirectory => Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
+
+ ///
+ /// Path to Arial font file.
+ ///
+ public static string ArialPath => Path.Combine(FontsDirectory, "arial.ttf");
+
+ ///
+ /// Path to Cambria font file.
+ ///
+ public static string CambriaPath => Path.Combine(FontsDirectory, "cambria.ttc");
+
+ ///
+ /// Skips the test if Arial font is not available.
+ ///
+ public static void SkipIfArialNotAvailable()
+ {
+ Assert.SkipUnless(File.Exists(ArialPath), "Arial font not found");
+ }
+
+ ///
+ /// Skips the test if the specified font file is not available.
+ ///
+ public static void SkipIfFontNotAvailable(string fontPath, string fontName)
+ {
+ Assert.SkipUnless(File.Exists(fontPath), $"{fontName} font not found");
+ }
+
+ ///
+ /// Skips the test if Arial font family cannot be retrieved.
+ ///
+ public static FontFamily GetArialFamilyOrSkip()
+ {
+ var factory = DWriteFactory.Instance;
+ var fontCollection = factory.GetSystemFontCollection();
+ var arialFamily = fontCollection["Arial"];
+ Assert.SkipUnless(arialFamily != null, "Arial font family not found");
+ return arialFamily!;
+ }
+
+ ///
+ /// Skips the test if no font with the specified name can be found.
+ ///
+ public static FontFamily GetFontFamilyOrSkip(string familyName)
+ {
+ var factory = DWriteFactory.Instance;
+ var fontCollection = factory.GetSystemFontCollection();
+ var family = fontCollection[familyName];
+ Assert.SkipUnless(family != null, $"{familyName} font family not found");
+ return family!;
+ }
+
+ ///
+ /// Gets Arial font or skips the test.
+ ///
+ public static Font GetArialFontOrSkip()
+ {
+ var family = GetArialFamilyOrSkip();
+ Assert.SkipUnless(family.Count > 0, "Arial font family has no fonts");
+ return family[0];
+ }
+
+ ///
+ /// Gets a localized strings object from Arial or skips the test.
+ ///
+ public static LocalizedStrings GetArialLocalizedStringsOrSkip()
+ {
+ var family = GetArialFamilyOrSkip();
+ var localizedStrings = family.FamilyNames;
+ Assert.SkipUnless(localizedStrings != null, "Arial font family has no localized names");
+ return localizedStrings!;
+ }
+
+ ///
+ /// Gets the first culture from localized strings or skips the test.
+ ///
+ public static CultureInfo GetFirstCultureOrSkip(LocalizedStrings localizedStrings)
+ {
+ Assert.SkipUnless(localizedStrings.Count > 0, "Localized strings is empty");
+ var firstCulture = localizedStrings.Keys.FirstOrDefault();
+ Assert.SkipUnless(firstCulture != null, "No culture found in localized strings");
+ return firstCulture!;
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TextAnalyzerControlCharacterTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TextAnalyzerControlCharacterTests.cs
new file mode 100644
index 00000000000..5794d6b591e
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TextAnalyzerControlCharacterTests.cs
@@ -0,0 +1,980 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Globalization;
+using System.Windows.Media;
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Additional tests for to improve code coverage.
+/// Focuses on control character paths and edge cases not covered by existing tests.
+///
+public class TextAnalyzerControlCharacterTests
+{
+ private static Font GetTestFont()
+ {
+ return TestHelpers.GetArialFontOrSkip();
+ }
+
+ private static TextAnalyzer GetTextAnalyzer()
+ {
+ return DWriteFactory.Instance.CreateTextAnalyzer();
+ }
+
+ private static unsafe ushort GetBlankGlyphIndex(Font font)
+ {
+ var fontFace = font.GetFontFace();
+ try
+ {
+ uint spaceCodePoint = ' ';
+ ushort glyphIndex = 0;
+ fontFace.GetArrayOfGlyphIndices(&spaceCodePoint, 1, &glyphIndex);
+ return glyphIndex;
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ private static unsafe IList? Itemize(string text, CultureInfo? culture = null)
+ {
+ if (string.IsNullOrEmpty(text))
+ return null;
+
+ culture ??= CultureInfo.InvariantCulture;
+
+ fixed (char* pText = text)
+ {
+ return TextAnalyzer.Itemize(
+ pText,
+ (uint)text.Length,
+ culture,
+ (Native.IDWriteFactory*)DWriteFactory.Instance.DWriteFactory,
+ false,
+ null,
+ false,
+ 0,
+ ClassificationUtility.Instance,
+ MS.Internal.TextFormatting.UnsafeNativeMethods.CreateTextAnalysisSink,
+ MS.Internal.TextFormatting.UnsafeNativeMethods.GetScriptAnalysisList,
+ MS.Internal.TextFormatting.UnsafeNativeMethods.GetNumberSubstitutionList,
+ MS.Internal.TextFormatting.UnsafeNativeMethods.CreateTextAnalysisSource
+ );
+ }
+ }
+
+ #region Control Character Glyph Tests
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_SoftHyphen_ShouldSucceed()
+ {
+ // Soft hyphen (U+00AD) is replaced with visible hyphen (U+002D) during line breaking
+ // This tests the CharHyphen handling in GetBlankGlyphsForControlCharacters
+ var font = GetTestFont();
+ // The soft hyphen character
+ string text = "word\u00ADbreak";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false, // isSideways
+ false, // isRightToLeft
+ CultureInfo.InvariantCulture,
+ null, // features
+ null, // featureRangeLengths
+ 12.0, // fontSize
+ 1.0, // scalingFactor
+ 1.0f, // pixelsPerDip
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ glyphAdvances.Should().NotBeNull();
+ glyphOffsets.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_TabCharacter_ShouldSucceed()
+ {
+ // Tab character (U+0009) is a control character
+ var font = GetTestFont();
+ string text = "Hello\tWorld";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_NewlineCharacter_ShouldSucceed()
+ {
+ // Newline characters are control characters
+ var font = GetTestFont();
+ string text = "Line1\nLine2";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_CarriageReturn_ShouldSucceed()
+ {
+ // Carriage return (U+000D) is a control character
+ var font = GetTestFont();
+ string text = "Line1\rLine2";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_CRLFSequence_ShouldSucceed()
+ {
+ // Windows-style line ending
+ var font = GetTestFont();
+ string text = "Line1\r\nLine2";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_VerticalTab_ShouldSucceed()
+ {
+ // Vertical tab (U+000B) is a control character
+ var font = GetTestFont();
+ string text = "Hello\vWorld";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_FormFeed_ShouldSucceed()
+ {
+ // Form feed (U+000C) is a control character
+ var font = GetTestFont();
+ string text = "Page1\fPage2";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ #endregion
+
+ #region Hyphen Character Tests (exercises GetBlankGlyphsForControlCharacters hyphen path)
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_HyphenCharacter_ShouldSucceed()
+ {
+ // The actual hyphen character (U+002D) which is CharHyphen in the code
+ var font = GetTestFont();
+ string text = "word-break";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ // Hyphen should have a non-zero advance
+ glyphAdvances.Should().Contain(a => a > 0);
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_MultipleHyphens_ShouldSucceed()
+ {
+ var font = GetTestFont();
+ string text = "one-two-three-four";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ #endregion
+
+ #region TextFormattingMode Coverage (exercises different measuring mode paths)
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_DisplayMode_ShouldSucceed()
+ {
+ var font = GetTestFont();
+ string text = "Display Mode Test";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Display, // Use Display mode
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ glyphAdvances.Should().NotBeNull();
+ glyphOffsets.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_IdealMode_ShouldSucceed()
+ {
+ var font = GetTestFont();
+ string text = "Ideal Mode Test";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_DisplayMode_WithControlChars_ShouldSucceed()
+ {
+ // This exercises GetGlyphPlacementsForControlCharacters with Display mode
+ var font = GetTestFont();
+ string text = "Hello\tWorld";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Display,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ #endregion
+
+ #region Sideways Text Coverage
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_Sideways_ShouldSucceed()
+ {
+ var font = GetTestFont();
+ string text = "Sideways";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ true, // isSideways = true
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ #endregion
+
+ #region Different Font Sizes
+
+ [Theory]
+ [InlineData(8.0)]
+ [InlineData(12.0)]
+ [InlineData(24.0)]
+ [InlineData(48.0)]
+ [InlineData(72.0)]
+ public unsafe void GetGlyphsAndPlacements_VariousFontSizes_ShouldSucceed(double fontSize)
+ {
+ var font = GetTestFont();
+ string text = "Size Test";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ fontSize,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ // Larger font sizes should produce larger advances
+ glyphAdvances.Where(a => a > 0).Should().NotBeEmpty();
+ }
+ }
+
+ #endregion
+
+ #region Scaling Factor Coverage
+
+ [Theory]
+ [InlineData(0.5)]
+ [InlineData(1.0)]
+ [InlineData(2.0)]
+ [InlineData(3.0)]
+ public unsafe void GetGlyphsAndPlacements_VariousScalingFactors_ShouldSucceed(double scalingFactor)
+ {
+ var font = GetTestFont();
+ string text = "Scale Test";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ scalingFactor,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ #endregion
+
+ #region PixelsPerDip Coverage
+
+ [Theory]
+ [InlineData(1.0f)]
+ [InlineData(1.25f)]
+ [InlineData(1.5f)]
+ [InlineData(2.0f)]
+ public unsafe void GetGlyphsAndPlacements_VariousPixelsPerDip_ShouldSucceed(float pixelsPerDip)
+ {
+ var font = GetTestFont();
+ string text = "DPI Test";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ pixelsPerDip,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ #endregion
+
+ #region Zero-Width Characters
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_ZeroWidthSpace_ShouldSucceed()
+ {
+ var font = GetTestFont();
+ // Zero-width space (U+200B)
+ string text = "Hello\u200BWorld";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_ZeroWidthNonJoiner_ShouldSucceed()
+ {
+ var font = GetTestFont();
+ // Zero-width non-joiner (U+200C)
+ string text = "Hello\u200CWorld";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_ZeroWidthJoiner_ShouldSucceed()
+ {
+ var font = GetTestFont();
+ // Zero-width joiner (U+200D)
+ string text = "Hello\u200DWorld";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ #endregion
+
+ #region Bidi Control Characters
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_LeftToRightMark_ShouldSucceed()
+ {
+ var font = GetTestFont();
+ // Left-to-right mark (U+200E)
+ string text = "Hello\u200EWorld";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ public unsafe void GetGlyphsAndPlacements_RightToLeftMark_ShouldSucceed()
+ {
+ var font = GetTestFont();
+ // Right-to-left mark (U+200F)
+ string text = "Hello\u200FWorld";
+
+ var spans = Itemize(text);
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ false,
+ false,
+ CultureInfo.InvariantCulture,
+ null,
+ null,
+ 12.0,
+ 1.0,
+ 1.0f,
+ TextFormattingMode.Ideal,
+ (ItemProps)spans[0].element,
+ out ushort[] clusterMap,
+ out ushort[] glyphIndices,
+ out int[] glyphAdvances,
+ out GlyphOffset[] glyphOffsets
+ );
+
+ clusterMap.Should().NotBeNull();
+ glyphIndices.Should().NotBeNull();
+ }
+ }
+
+ #endregion
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TextAnalyzerIntegrationTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TextAnalyzerIntegrationTests.cs
new file mode 100644
index 00000000000..277f9e27b0f
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TextAnalyzerIntegrationTests.cs
@@ -0,0 +1,1341 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Globalization;
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Integration tests for class.
+/// These tests call the real TextAnalyzer.Itemize method with actual PresentationNative delegates.
+///
+public class TextAnalyzerIntegrationTests
+{
+ ///
+ /// Helper class to encapsulate the complexity of calling TextAnalyzer.Itemize
+ /// with the required native delegates from PresentationNative.dll.
+ ///
+ private static class ItemizeHelper
+ {
+ ///
+ /// Calls TextAnalyzer.Itemize with the provided text and returns the resulting spans.
+ /// This mirrors how TypefaceMap.GetShapeableText calls Itemize in WPF.
+ ///
+ public static unsafe IList? Itemize(
+ string text,
+ CultureInfo? culture = null,
+ bool isRightToLeft = false,
+ CultureInfo? numberCulture = null,
+ bool ignoreUserOverride = false,
+ uint numberSubstitutionMethod = 0)
+ {
+ if (string.IsNullOrEmpty(text))
+ return null;
+
+ culture ??= CultureInfo.InvariantCulture;
+
+ fixed (char* pText = text)
+ {
+ return TextAnalyzer.Itemize(
+ pText,
+ (uint)text.Length,
+ culture,
+ (Native.IDWriteFactory*)DWriteFactory.Instance.DWriteFactory,
+ isRightToLeft,
+ numberCulture,
+ ignoreUserOverride,
+ numberSubstitutionMethod,
+ ClassificationUtility.Instance,
+ MS.Internal.TextFormatting.UnsafeNativeMethods.CreateTextAnalysisSink,
+ MS.Internal.TextFormatting.UnsafeNativeMethods.GetScriptAnalysisList,
+ MS.Internal.TextFormatting.UnsafeNativeMethods.GetNumberSubstitutionList,
+ MS.Internal.TextFormatting.UnsafeNativeMethods.CreateTextAnalysisSource
+ );
+ }
+ }
+ }
+
+ ///
+ /// Gets a Font instance for testing. Uses Arial which should be available on all Windows systems.
+ ///
+ private static Font GetTestFont()
+ {
+ return TestHelpers.GetArialFontOrSkip();
+ }
+
+ ///
+ /// Gets a TextAnalyzer instance for testing.
+ ///
+ private static TextAnalyzer GetTextAnalyzer()
+ {
+ return DWriteFactory.Instance.CreateTextAnalyzer();
+ }
+
+ ///
+ /// Gets the blank glyph index for a font. This is typically glyph 3 for space character.
+ /// For most fonts, the space character maps to a low glyph index.
+ ///
+ private static unsafe ushort GetBlankGlyphIndex(Font font)
+ {
+ // Get a FontFace and look up the space character glyph index
+ var fontFace = font.GetFontFace();
+ try
+ {
+ uint spaceCodePoint = ' ';
+ ushort glyphIndex = 0;
+ fontFace.GetArrayOfGlyphIndices(&spaceCodePoint, 1, &glyphIndex);
+ return glyphIndex;
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+ }
+
+ ///
+ /// Helper to get glyphs and placements for simple text.
+ /// Returns the output arrays from GetGlyphsAndTheirPlacements.
+ ///
+ private static unsafe (ushort[] clusterMap, ushort[] glyphIndices, int[] glyphAdvances, GlyphOffset[] glyphOffsets)?
+ GetGlyphsAndPlacements(
+ string text,
+ Font font,
+ ItemProps itemProps,
+ double fontSize = 12.0,
+ bool isSideways = false,
+ bool isRightToLeft = false)
+ {
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ ushort[] clusterMap;
+ ushort[] glyphIndices;
+ int[] glyphAdvances;
+ GlyphOffset[] glyphOffsets;
+
+ fixed (char* pText = text)
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText,
+ (uint)text.Length,
+ font,
+ blankGlyphIndex,
+ isSideways,
+ isRightToLeft,
+ CultureInfo.InvariantCulture,
+ null, // features
+ null, // featureRangeLengths
+ fontSize,
+ 1.0, // scalingFactor
+ 1.0f, // pixelsPerDip
+ System.Windows.Media.TextFormattingMode.Ideal,
+ itemProps,
+ out clusterMap,
+ out glyphIndices,
+ out glyphAdvances,
+ out glyphOffsets
+ );
+ }
+
+ return (clusterMap, glyphIndices, glyphAdvances, glyphOffsets);
+ }
+
+ #region Infrastructure Verification Tests
+
+ [Fact]
+ public void TextAnalyzer_CanBeCreated()
+ {
+ var analyzer = GetTextAnalyzer();
+ analyzer.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void ClassificationUtility_IsAvailable()
+ {
+ var classification = ClassificationUtility.Instance;
+ classification.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void DWriteFactory_HasValidFactoryPointer()
+ {
+ var factory = DWriteFactory.Instance;
+ factory.Should().NotBeNull();
+
+ unsafe
+ {
+ var ptr = factory.DWriteFactory;
+ ((IntPtr)ptr).Should().NotBe(IntPtr.Zero);
+ }
+ }
+
+ [Fact]
+ public void TestFont_IsAvailable()
+ {
+ var font = GetTestFont();
+ // Skip test if Arial is not available
+ font.Weight.Should().Be(FontWeight.Normal);
+ }
+
+ #endregion
+
+ #region Basic Itemization Tests
+
+ [Fact]
+ public void Itemize_EmptyString_ReturnsNull()
+ {
+ var result = ItemizeHelper.Itemize("");
+ result.Should().BeNull();
+ }
+
+ [Fact]
+ public void Itemize_NullString_ReturnsNull()
+ {
+ var result = ItemizeHelper.Itemize(null!);
+ result.Should().BeNull();
+ }
+
+ [Fact]
+ public void Itemize_SingleLatinCharacter_ReturnsSingleSpan()
+ {
+ var result = ItemizeHelper.Itemize("A");
+
+ result.Should().NotBeNull();
+ result.Should().HaveCount(1);
+ result![0].length.Should().Be(1);
+ }
+
+ [Fact]
+ public void Itemize_SimpleLatinText_ReturnsSingleSpan()
+ {
+ var result = ItemizeHelper.Itemize("Hello World");
+
+ result.Should().NotBeNull();
+ result.Should().HaveCount(1);
+ result![0].length.Should().Be(11);
+
+ // The element should be an ItemProps
+ result[0].element.Should().BeOfType();
+ var props = (ItemProps)result[0].element;
+ props.IsLatin.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Itemize_LatinText_ItemPropsHasValidProperties()
+ {
+ var result = ItemizeHelper.Itemize("Test");
+
+ result.Should().NotBeNull();
+ var props = (ItemProps)result![0].element;
+
+ props.IsLatin.Should().BeTrue();
+ props.IsIndic.Should().BeFalse();
+ props.HasCombiningMark.Should().BeFalse();
+ }
+
+ [Fact]
+ public void Itemize_WithEnglishCulture_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize("Hello", CultureInfo.GetCultureInfo("en-US"));
+
+ result.Should().NotBeNull();
+ result.Should().HaveCount(1);
+ }
+
+ #endregion
+
+ #region Mixed Script Tests
+
+ [Fact]
+ public void Itemize_MixedLatinAndArabic_ReturnsMultipleSpans()
+ {
+ // "Hello" + Arabic "مرحبا" (Marhaba)
+ var result = ItemizeHelper.Itemize("Hello مرحبا");
+
+ result.Should().NotBeNull();
+ // Should have at least 2 spans: Latin and Arabic (possibly more for space)
+ result!.Count.Should().BeGreaterThanOrEqualTo(2);
+
+ // Total length should match input
+ int totalLength = result.Sum(s => s.length);
+ totalLength.Should().Be(11); // "Hello " (6) + "مرحبا" (5)
+ }
+
+ [Fact]
+ public void Itemize_MixedLatinAndChinese_ReturnsMultipleSpans()
+ {
+ // "Hello" + Chinese "你好" (Nǐ hǎo)
+ var result = ItemizeHelper.Itemize("Hello你好");
+
+ result.Should().NotBeNull();
+ // Should have at least 2 spans: Latin and Chinese
+ result!.Count.Should().BeGreaterThanOrEqualTo(2);
+
+ // First span should be Latin
+ var firstProps = (ItemProps)result[0].element;
+ firstProps.IsLatin.Should().BeTrue();
+
+ // Total length should match
+ int totalLength = result.Sum(s => s.length);
+ totalLength.Should().Be(7); // "Hello" (5) + "你好" (2)
+ }
+
+ [Fact]
+ public void Itemize_MixedLatinAndCyrillic_ReturnsMultipleSpans()
+ {
+ // "Hello" + Russian "Привет" (Privet)
+ var result = ItemizeHelper.Itemize("Hello Привет");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(2);
+
+ int totalLength = result.Sum(s => s.length);
+ totalLength.Should().Be(12); // "Hello " (6) + "Привет" (6)
+ }
+
+ [Fact]
+ public void Itemize_MixedLatinAndGreek_ReturnsMultipleSpans()
+ {
+ // "Hello" + Greek "Γειά" (Geia)
+ var result = ItemizeHelper.Itemize("Hello Γειά");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(2);
+
+ int totalLength = result.Sum(s => s.length);
+ totalLength.Should().Be(10);
+ }
+
+ [Fact]
+ public void Itemize_MixedLatinAndHebrew_ReturnsMultipleSpans()
+ {
+ // "Hello" + Hebrew "שלום" (Shalom)
+ var result = ItemizeHelper.Itemize("Hello שלום");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(2);
+
+ int totalLength = result.Sum(s => s.length);
+ totalLength.Should().Be(10);
+ }
+
+ [Fact]
+ public void Itemize_MixedLatinAndJapanese_ReturnsMultipleSpans()
+ {
+ // "Hello" + Japanese "こんにちは" (Konnichiwa)
+ var result = ItemizeHelper.Itemize("Helloこんにちは");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(2);
+
+ var firstProps = (ItemProps)result[0].element;
+ firstProps.IsLatin.Should().BeTrue();
+
+ int totalLength = result.Sum(s => s.length);
+ totalLength.Should().Be(10); // "Hello" (5) + "こんにちは" (5)
+ }
+
+ [Fact]
+ public void Itemize_MixedLatinAndKorean_ReturnsMultipleSpans()
+ {
+ // "Hello" + Korean "안녕하세요" (Annyeonghaseyo)
+ var result = ItemizeHelper.Itemize("Hello안녕하세요");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(2);
+
+ int totalLength = result.Sum(s => s.length);
+ totalLength.Should().Be(10); // "Hello" (5) + "안녕하세요" (5)
+ }
+
+ [Fact]
+ public void Itemize_ThreeScripts_ReturnsMultipleSpans()
+ {
+ // Latin + Chinese + Arabic
+ var result = ItemizeHelper.Itemize("Hi你好مرحبا");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(3);
+
+ int totalLength = result.Sum(s => s.length);
+ totalLength.Should().Be(9); // "Hi" (2) + "你好" (2) + "مرحبا" (5)
+ }
+
+ #endregion
+
+ #region RTL (Right-to-Left) Tests
+
+ [Fact]
+ public void Itemize_ArabicText_Succeeds()
+ {
+ // Arabic "مرحبا بالعالم" (Hello World)
+ var result = ItemizeHelper.Itemize("مرحبا بالعالم");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(1);
+
+ int totalLength = result.Sum(s => s.length);
+ totalLength.Should().Be(13);
+ }
+
+ [Fact]
+ public void Itemize_HebrewText_Succeeds()
+ {
+ // Hebrew "שלום עולם" (Hello World)
+ var result = ItemizeHelper.Itemize("שלום עולם");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(1);
+
+ int totalLength = result.Sum(s => s.length);
+ totalLength.Should().Be(9);
+ }
+
+ [Fact]
+ public void Itemize_WithRtlParagraphDirection_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize("Hello", isRightToLeft: true);
+
+ result.Should().NotBeNull();
+ result.Should().HaveCount(1);
+ }
+
+ [Fact]
+ public void Itemize_ArabicWithRtlParagraph_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize("مرحبا", isRightToLeft: true);
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(1);
+ }
+
+ #endregion
+
+ #region Indic Script Tests
+
+ [Fact]
+ public void Itemize_DevanagariText_FlaggedAsIndic()
+ {
+ // Hindi "नमस्ते" (Namaste)
+ var result = ItemizeHelper.Itemize("नमस्ते");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(1);
+
+ // At least one span should be marked as Indic
+ var hasIndicSpan = result.Any(s => ((ItemProps)s.element).IsIndic);
+ hasIndicSpan.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Itemize_BengaliText_FlaggedAsIndic()
+ {
+ // Bengali "নমস্কার" (Nomoshkar)
+ var result = ItemizeHelper.Itemize("নমস্কার");
+
+ result.Should().NotBeNull();
+ var hasIndicSpan = result!.Any(s => ((ItemProps)s.element).IsIndic);
+ hasIndicSpan.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Itemize_TamilText_FlaggedAsIndic()
+ {
+ // Tamil "வணக்கம்" (Vanakkam)
+ var result = ItemizeHelper.Itemize("வணக்கம்");
+
+ result.Should().NotBeNull();
+ var hasIndicSpan = result!.Any(s => ((ItemProps)s.element).IsIndic);
+ hasIndicSpan.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Itemize_TeluguText_FlaggedAsIndic()
+ {
+ // Telugu "నమస్కారం" (Namaskaram)
+ var result = ItemizeHelper.Itemize("నమస్కారం");
+
+ result.Should().NotBeNull();
+ var hasIndicSpan = result!.Any(s => ((ItemProps)s.element).IsIndic);
+ hasIndicSpan.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Itemize_ThaiText_Succeeds()
+ {
+ // Thai "สวัสดี" (Sawasdee)
+ var result = ItemizeHelper.Itemize("สวัสดี");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(1);
+ }
+
+ [Fact]
+ public void Itemize_MixedLatinAndDevanagari_ReturnsMultipleSpans()
+ {
+ var result = ItemizeHelper.Itemize("Hello नमस्ते");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(2);
+
+ // First span should be Latin
+ var firstProps = (ItemProps)result[0].element;
+ firstProps.IsLatin.Should().BeTrue();
+ firstProps.IsIndic.Should().BeFalse();
+
+ // Should have an Indic span
+ var hasIndicSpan = result.Any(s => ((ItemProps)s.element).IsIndic);
+ hasIndicSpan.Should().BeTrue();
+ }
+
+ #endregion
+
+ #region Combining Mark Tests
+
+ [Fact]
+ public void Itemize_LatinWithCombiningAccent_FlaggedAsCombining()
+ {
+ // 'e' followed by combining acute accent (U+0301) = é
+ var result = ItemizeHelper.Itemize("e\u0301");
+
+ result.Should().NotBeNull();
+
+ // Should have a span with combining mark flag
+ var hasCombiningSpan = result!.Any(s => ((ItemProps)s.element).HasCombiningMark);
+ hasCombiningSpan.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Itemize_PrecomposedAccent_NoCombiningFlag()
+ {
+ // Precomposed é (U+00E9) - no combining mark
+ var result = ItemizeHelper.Itemize("é");
+
+ result.Should().NotBeNull();
+
+ // Precomposed characters should not have combining mark flag
+ var props = (ItemProps)result![0].element;
+ props.HasCombiningMark.Should().BeFalse();
+ }
+
+ [Fact]
+ public void Itemize_MultipleCombiningMarks_FlaggedAsCombining()
+ {
+ // 'a' + combining grave (U+0300) + combining acute (U+0301)
+ var result = ItemizeHelper.Itemize("a\u0300\u0301");
+
+ result.Should().NotBeNull();
+
+ var hasCombiningSpan = result!.Any(s => ((ItemProps)s.element).HasCombiningMark);
+ hasCombiningSpan.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Itemize_CombiningDiaeresis_FlaggedAsCombining()
+ {
+ // 'u' + combining diaeresis (U+0308) = ü
+ var result = ItemizeHelper.Itemize("u\u0308");
+
+ result.Should().NotBeNull();
+
+ var hasCombiningSpan = result!.Any(s => ((ItemProps)s.element).HasCombiningMark);
+ hasCombiningSpan.Should().BeTrue();
+ }
+
+ #endregion
+
+ #region Number Substitution Tests
+
+ [Fact]
+ public void Itemize_DigitsWithNoCulture_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize("123456");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(1);
+ }
+
+ [Fact]
+ public void Itemize_DigitsWithEnglishCulture_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize("123", numberCulture: CultureInfo.GetCultureInfo("en-US"));
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(1);
+ }
+
+ [Fact]
+ public void Itemize_DigitsWithArabicCulture_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize("123", numberCulture: CultureInfo.GetCultureInfo("ar-SA"));
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(1);
+ }
+
+ [Fact]
+ public void Itemize_MixedTextAndDigits_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize("Price: $123.45");
+
+ result.Should().NotBeNull();
+
+ int totalLength = result!.Sum(s => s.length);
+ totalLength.Should().Be(14);
+ }
+
+ [Fact]
+ public void Itemize_DigitsWithNumberSubstitutionMethod_Succeeds()
+ {
+ // NumberSubstitutionMethod: 0=FromCulture, 1=None, 2=Context, 3=NativeNational
+ var result = ItemizeHelper.Itemize(
+ "123",
+ numberCulture: CultureInfo.GetCultureInfo("ar-SA"),
+ numberSubstitutionMethod: 3); // NativeNational
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(1);
+ }
+
+ #endregion
+
+ #region ItemProps Tests
+
+ [Fact]
+ public unsafe void ItemProps_FromItemize_HasValidScriptAnalysis()
+ {
+ var result = ItemizeHelper.Itemize("Hello");
+
+ result.Should().NotBeNull();
+ var props = (ItemProps)result![0].element;
+
+ // ScriptAnalysis should be a valid pointer (not null for real text)
+ void* scriptPtr = props.ScriptAnalysis;
+ ((IntPtr)scriptPtr).Should().NotBe(IntPtr.Zero);
+ }
+
+ [Fact]
+ public void ItemProps_LatinAndArabic_HaveDifferentScriptAnalysis()
+ {
+ var latinResult = ItemizeHelper.Itemize("Hello");
+ var arabicResult = ItemizeHelper.Itemize("مرحبا");
+
+ latinResult.Should().NotBeNull();
+ arabicResult.Should().NotBeNull();
+
+ var latinProps = (ItemProps)latinResult![0].element;
+ var arabicProps = (ItemProps)arabicResult![0].element;
+
+ // They should have different properties
+ latinProps.IsLatin.Should().BeTrue();
+ arabicProps.IsLatin.Should().BeFalse();
+ }
+
+ [Fact]
+ public void ItemProps_CanShapeTogether_SameScript_ReturnsTrue()
+ {
+ var result = ItemizeHelper.Itemize("Hello World");
+
+ result.Should().NotBeNull();
+ // If there's only one span, it can shape with itself
+ if (result!.Count == 1)
+ {
+ var props = (ItemProps)result[0].element;
+ props.CanShapeTogether(props).Should().BeTrue();
+ }
+ }
+
+ [Fact]
+ public void ItemProps_NeedsCaretInfo_TrueForIndic()
+ {
+ // Devanagari text should need caret info
+ var result = ItemizeHelper.Itemize("नमस्ते");
+
+ result.Should().NotBeNull();
+
+ // At least one span should need caret info (Indic scripts do)
+ var needsCaretInfo = result!.Any(s => ((ItemProps)s.element).NeedsCaretInfo);
+ needsCaretInfo.Should().BeTrue();
+ }
+
+ [Fact]
+ public void ItemProps_NeedsCaretInfo_FalseForLatin()
+ {
+ var result = ItemizeHelper.Itemize("Hello");
+
+ result.Should().NotBeNull();
+ var props = (ItemProps)result![0].element;
+
+ // Latin text should not need special caret info
+ props.NeedsCaretInfo.Should().BeFalse();
+ }
+
+ #endregion
+
+ #region Edge Cases
+
+ [Fact]
+ public void Itemize_SingleSpace_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize(" ");
+
+ result.Should().NotBeNull();
+ result!.Count.Should().BeGreaterThanOrEqualTo(1);
+ result.Sum(s => s.length).Should().Be(1);
+ }
+
+ [Fact]
+ public void Itemize_OnlySpaces_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize(" ");
+
+ result.Should().NotBeNull();
+ result!.Sum(s => s.length).Should().Be(5);
+ }
+
+ [Fact]
+ public void Itemize_Newlines_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize("Hello\nWorld");
+
+ result.Should().NotBeNull();
+ result!.Sum(s => s.length).Should().Be(11);
+ }
+
+ [Fact]
+ public void Itemize_Tabs_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize("Hello\tWorld");
+
+ result.Should().NotBeNull();
+ result!.Sum(s => s.length).Should().Be(11);
+ }
+
+ [Fact]
+ public void Itemize_ControlCharacters_Succeeds()
+ {
+ // Control characters may be filtered by DirectWrite
+ // The important thing is that it doesn't throw
+ var result = ItemizeHelper.Itemize("A\x01\x02\x03B");
+
+ result.Should().NotBeNull();
+ // Just verify we get some result - control char handling is implementation-defined
+ result!.Sum(s => s.length).Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public void Itemize_SurrogatePair_Succeeds()
+ {
+ // Emoji: 😀 (U+1F600) = surrogate pair
+ var result = ItemizeHelper.Itemize("😀");
+
+ result.Should().NotBeNull();
+ result!.Sum(s => s.length).Should().Be(2); // Surrogate pair = 2 chars
+ }
+
+ [Fact]
+ public void Itemize_MultipleSurrogatePairs_Succeeds()
+ {
+ // Multiple emoji
+ var result = ItemizeHelper.Itemize("😀😁😂");
+
+ result.Should().NotBeNull();
+ result!.Sum(s => s.length).Should().Be(6); // 3 emoji * 2 chars each
+ }
+
+ [Fact]
+ public void Itemize_MixedTextAndEmoji_Succeeds()
+ {
+ var result = ItemizeHelper.Itemize("Hello 😀 World");
+
+ result.Should().NotBeNull();
+ result!.Sum(s => s.length).Should().Be(14); // "Hello " (6) + 😀 (2) + " World" (6)
+ }
+
+ [Fact]
+ public void Itemize_LongText_Succeeds()
+ {
+ var longText = new string('A', 10000);
+ var result = ItemizeHelper.Itemize(longText);
+
+ result.Should().NotBeNull();
+ result!.Sum(s => s.length).Should().Be(10000);
+ }
+
+ [Fact]
+ public void Itemize_UnicodePrivateUseArea_Succeeds()
+ {
+ // Private Use Area character
+ var result = ItemizeHelper.Itemize("\uE000");
+
+ result.Should().NotBeNull();
+ result!.Sum(s => s.length).Should().Be(1);
+ }
+
+ [Fact]
+ public void Itemize_ZeroWidthChars_Succeeds()
+ {
+ // Zero-width space (U+200B) and zero-width joiner (U+200D)
+ var result = ItemizeHelper.Itemize("A\u200B\u200DB");
+
+ result.Should().NotBeNull();
+ result!.Sum(s => s.length).Should().Be(4);
+ }
+
+ [Fact]
+ public void Itemize_BidiControlChars_Succeeds()
+ {
+ // LRM (U+200E), RLM (U+200F)
+ var result = ItemizeHelper.Itemize("A\u200E\u200FB");
+
+ result.Should().NotBeNull();
+ result!.Sum(s => s.length).Should().Be(4);
+ }
+
+ [Fact]
+ public void Itemize_AllCultures_DoNotThrow()
+ {
+ var cultures = new[]
+ {
+ "en-US", "fr-FR", "de-DE", "ja-JP", "zh-CN", "ar-SA", "he-IL", "hi-IN", "ru-RU", "ko-KR"
+ };
+
+ foreach (var cultureName in cultures)
+ {
+ var culture = CultureInfo.GetCultureInfo(cultureName);
+ var result = ItemizeHelper.Itemize("Test 123", culture);
+
+ result.Should().NotBeNull($"Culture {cultureName} should not return null");
+ }
+ }
+
+ #endregion
+
+ #region Phase 3: Glyph Shaping Tests
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_SimpleAscii_ReturnsGlyphData()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("Hello");
+ spans.Should().NotBeNull();
+
+ var itemProps = (ItemProps)spans![0].element;
+ var result = GetGlyphsAndPlacements("Hello", font, itemProps);
+
+ result.Should().NotBeNull();
+ var (clusterMap, glyphIndices, glyphAdvances, glyphOffsets) = result!.Value;
+
+ // Should have cluster map entries for each character
+ clusterMap.Should().HaveCount(5);
+
+ // Should have glyph indices (at least one per character for simple text)
+ glyphIndices.Should().NotBeEmpty();
+ glyphIndices.Length.Should().BeGreaterThanOrEqualTo(5);
+
+ // All glyph indices should be non-zero (valid glyphs)
+ glyphIndices.Take(5).Should().AllSatisfy(g => g.Should().BeGreaterThan((ushort)0));
+
+ // Advances should be positive
+ glyphAdvances.Should().NotBeEmpty();
+ glyphAdvances.Take(5).Should().AllSatisfy(a => a.Should().BeGreaterThan(0));
+
+ // Offsets should exist
+ glyphOffsets.Should().NotBeEmpty();
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_SingleCharacter_ReturnsOneGlyph()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("A");
+ var itemProps = (ItemProps)spans![0].element;
+ var result = GetGlyphsAndPlacements("A", font, itemProps);
+
+ result.Should().NotBeNull();
+ var (clusterMap, glyphIndices, glyphAdvances, _) = result!.Value;
+
+ clusterMap.Should().HaveCount(1);
+ glyphIndices.Length.Should().BeGreaterThanOrEqualTo(1);
+ glyphAdvances.Length.Should().BeGreaterThanOrEqualTo(1);
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_WithSpaces_HandlesSpacesCorrectly()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("A B");
+ var itemProps = (ItemProps)spans![0].element;
+ var result = GetGlyphsAndPlacements("A B", font, itemProps);
+
+ result.Should().NotBeNull();
+ var (clusterMap, glyphIndices, glyphAdvances, glyphOffsets) = result!.Value;
+
+ clusterMap.Should().HaveCount(3);
+ // All characters should map to valid glyphs
+ glyphIndices.Take(3).Should().AllSatisfy(g => g.Should().BeGreaterThan((ushort)0));
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_DifferentFontSizes_ScalesAdvances()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("A");
+ var itemProps = (ItemProps)spans![0].element;
+
+ var result12 = GetGlyphsAndPlacements("A", font, itemProps, fontSize: 12.0);
+ var result24 = GetGlyphsAndPlacements("A", font, itemProps, fontSize: 24.0);
+
+ result12.Should().NotBeNull();
+ result24.Should().NotBeNull();
+
+ // Larger font size should produce larger advances
+ var advance12 = result12!.Value.glyphAdvances[0];
+ var advance24 = result24!.Value.glyphAdvances[0];
+
+ advance24.Should().BeGreaterThan(advance12);
+ // Should be approximately 2x (not exact due to rounding)
+ ((double)advance24 / advance12).Should().BeApproximately(2.0, 0.1);
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_ClusterMap_MapsCharactersToGlyphs()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("ABC");
+ var itemProps = (ItemProps)spans![0].element;
+ var result = GetGlyphsAndPlacements("ABC", font, itemProps);
+
+ result.Should().NotBeNull();
+ var clusterMap = result!.Value.clusterMap;
+
+ // Cluster map should be monotonically non-decreasing
+ for (int i = 1; i < clusterMap.Length; i++)
+ {
+ clusterMap[i].Should().BeGreaterThanOrEqualTo(clusterMap[i - 1]);
+ }
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_RightToLeft_Succeeds()
+ {
+ var font = GetTestFont();
+ // Use Arabic text for RTL
+ var spans = ItemizeHelper.Itemize("مرحبا");
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var itemProps = (ItemProps)spans[0].element;
+ var result = GetGlyphsAndPlacements("مرحبا", font, itemProps, isRightToLeft: true);
+
+ result.Should().NotBeNull();
+ result!.Value.glyphIndices.Should().NotBeEmpty();
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_LongText_Succeeds()
+ {
+ var font = GetTestFont();
+ var longText = new string('A', 1000);
+ var spans = ItemizeHelper.Itemize(longText);
+ var itemProps = (ItemProps)spans![0].element;
+ var result = GetGlyphsAndPlacements(longText, font, itemProps);
+
+ result.Should().NotBeNull();
+ var (clusterMap, glyphIndices, _, _) = result!.Value;
+
+ clusterMap.Should().HaveCount(1000);
+ glyphIndices.Length.Should().BeGreaterThanOrEqualTo(1000);
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_MixedCase_ReturnsDistinctGlyphs()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("Aa");
+ var itemProps = (ItemProps)spans![0].element;
+ var result = GetGlyphsAndPlacements("Aa", font, itemProps);
+
+ result.Should().NotBeNull();
+ var glyphIndices = result!.Value.glyphIndices;
+
+ // 'A' and 'a' should have different glyph indices
+ glyphIndices[0].Should().NotBe(glyphIndices[1]);
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_Numbers_ReturnsValidGlyphs()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("0123456789");
+ var itemProps = (ItemProps)spans![0].element;
+ var result = GetGlyphsAndPlacements("0123456789", font, itemProps);
+
+ result.Should().NotBeNull();
+ var glyphIndices = result!.Value.glyphIndices;
+
+ // All digits should have valid glyph indices
+ glyphIndices.Take(10).Should().AllSatisfy(g => g.Should().BeGreaterThan((ushort)0));
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_Punctuation_ReturnsValidGlyphs()
+ {
+ var font = GetTestFont();
+ var text = ".,!?;:";
+ var spans = ItemizeHelper.Itemize(text);
+ var itemProps = (ItemProps)spans![0].element;
+ var result = GetGlyphsAndPlacements(text, font, itemProps);
+
+ result.Should().NotBeNull();
+ var glyphIndices = result!.Value.glyphIndices;
+
+ // All punctuation should have valid glyph indices
+ glyphIndices.Take(text.Length).Should().AllSatisfy(g => g.Should().BeGreaterThan((ushort)0));
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_GlyphOffsets_AreReasonable()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("Hello");
+ var itemProps = (ItemProps)spans![0].element;
+ var result = GetGlyphsAndPlacements("Hello", font, itemProps);
+
+ result.Should().NotBeNull();
+ var glyphOffsets = result!.Value.glyphOffsets;
+
+ // For simple Latin text, offsets should generally be zero or small
+ foreach (var offset in glyphOffsets.Take(5))
+ {
+ // du is horizontal offset, dv is vertical offset
+ Math.Abs(offset.du).Should().BeLessThan(1000);
+ Math.Abs(offset.dv).Should().BeLessThan(1000);
+ }
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_ConsistentResults_ForSameInput()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("Test");
+ var itemProps = (ItemProps)spans![0].element;
+
+ var result1 = GetGlyphsAndPlacements("Test", font, itemProps);
+ var result2 = GetGlyphsAndPlacements("Test", font, itemProps);
+
+ result1.Should().NotBeNull();
+ result2.Should().NotBeNull();
+
+ // Results should be identical for the same input
+ result1!.Value.glyphIndices.Should().BeEquivalentTo(result2!.Value.glyphIndices);
+ result1!.Value.glyphAdvances.Should().BeEquivalentTo(result2!.Value.glyphAdvances);
+ }
+
+ [Fact]
+ public void GetGlyphsAndTheirPlacements_DisplayMode_DiffersFromIdeal()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("Hello");
+ var itemProps = (ItemProps)spans![0].element;
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ ushort[] clusterMapIdeal, clusterMapDisplay;
+ ushort[] glyphIndicesIdeal, glyphIndicesDisplay;
+ int[] glyphAdvancesIdeal, glyphAdvancesDisplay;
+ GlyphOffset[] glyphOffsetsIdeal, glyphOffsetsDisplay;
+
+ unsafe
+ {
+ fixed (char* pText = "Hello")
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText, 5, font, blankGlyphIndex, false, false,
+ CultureInfo.InvariantCulture, null, null,
+ 12.0, 1.0, 1.0f,
+ System.Windows.Media.TextFormattingMode.Ideal,
+ itemProps,
+ out clusterMapIdeal, out glyphIndicesIdeal, out glyphAdvancesIdeal, out glyphOffsetsIdeal);
+
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText, 5, font, blankGlyphIndex, false, false,
+ CultureInfo.InvariantCulture, null, null,
+ 12.0, 1.0, 1.0f,
+ System.Windows.Media.TextFormattingMode.Display,
+ itemProps,
+ out clusterMapDisplay, out glyphIndicesDisplay, out glyphAdvancesDisplay, out glyphOffsetsDisplay);
+ }
+ }
+
+ // Glyph indices should be the same
+ glyphIndicesIdeal.Should().BeEquivalentTo(glyphIndicesDisplay);
+
+ // Advances may differ between Ideal and Display modes due to rounding
+ // Just verify both return valid data
+ glyphAdvancesIdeal.Should().NotBeEmpty();
+ glyphAdvancesDisplay.Should().NotBeEmpty();
+ }
+
+ [Fact]
+ public void GetBlankGlyphIndex_ReturnsValidIndex()
+ {
+ var font = GetTestFont();
+ var blankIndex = GetBlankGlyphIndex(font);
+
+ // Blank glyph index should be valid (could be 0 for .notdef or space glyph index)
+ // Just verify it doesn't throw and returns a value
+ blankIndex.Should().BeGreaterThanOrEqualTo((ushort)0);
+ }
+
+ [Fact]
+ public void TextAnalyzer_GetGlyphsAndTheirPlacements_WithFeatures_Succeeds()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("fi"); // potential ligature
+ var itemProps = (ItemProps)spans![0].element;
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+
+ // Create a simple feature set (liga = ligatures)
+ var features = new DWriteFontFeature[][]
+ {
+ new DWriteFontFeature[]
+ {
+ new DWriteFontFeature(DWriteFontFeatureTag.StandardLigatures, 1)
+ }
+ };
+ var featureRanges = new uint[] { 2 }; // applies to 2 characters
+
+ ushort[] clusterMap;
+ ushort[] glyphIndices;
+ int[] glyphAdvances;
+ GlyphOffset[] glyphOffsets;
+
+ unsafe
+ {
+ fixed (char* pText = "fi")
+ {
+ analyzer.GetGlyphsAndTheirPlacements(
+ pText, 2, font, blankGlyphIndex, false, false,
+ CultureInfo.InvariantCulture, features, featureRanges,
+ 12.0, 1.0, 1.0f,
+ System.Windows.Media.TextFormattingMode.Ideal,
+ itemProps,
+ out clusterMap, out glyphIndices, out glyphAdvances, out glyphOffsets);
+ }
+ }
+
+ // Should succeed regardless of whether ligature is applied
+ clusterMap.Should().HaveCount(2);
+ glyphIndices.Should().NotBeEmpty();
+ }
+
+ #endregion
+
+ #region Phase 4: GetGlyphPlacements Direct Tests
+
+ ///
+ /// Helper to call GetGlyphs and then GetGlyphPlacements separately.
+ /// This tests the lower-level API that GetGlyphsAndTheirPlacements combines.
+ ///
+ private static unsafe (int[] glyphAdvances, GlyphOffset[] glyphOffsets)?
+ GetGlyphPlacementsDirect(
+ string text,
+ Font font,
+ ItemProps itemProps,
+ double fontSize = 12.0,
+ bool isSideways = false,
+ bool isRightToLeft = false)
+ {
+ var analyzer = GetTextAnalyzer();
+ ushort blankGlyphIndex = GetBlankGlyphIndex(font);
+ uint textLength = (uint)text.Length;
+ uint maxGlyphCount = textLength * 3 + 16; // Standard DWrite buffer size formula
+
+ // Allocate buffers for GetGlyphs output
+ ushort[] clusterMap = new ushort[textLength];
+ ushort[] textProps = new ushort[textLength];
+ ushort[] glyphIndices = new ushort[maxGlyphCount];
+ uint[] glyphProps = new uint[maxGlyphCount];
+ int[] canGlyphAlone = new int[textLength];
+ uint actualGlyphCount = 0;
+
+ fixed (char* pText = text)
+ fixed (ushort* pClusterMap = clusterMap)
+ fixed (ushort* pTextProps = textProps)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ fixed (uint* pGlyphProps = glyphProps)
+ fixed (int* pCanGlyphAlone = canGlyphAlone)
+ {
+ // Step 1: Call GetGlyphs to get glyph indices
+ analyzer.GetGlyphs(
+ pText,
+ textLength,
+ font,
+ blankGlyphIndex,
+ isSideways,
+ isRightToLeft,
+ CultureInfo.InvariantCulture,
+ null, // features
+ null, // featureRangeLengths
+ maxGlyphCount,
+ System.Windows.Media.TextFormattingMode.Ideal,
+ itemProps,
+ pClusterMap,
+ pTextProps,
+ pGlyphIndices,
+ pGlyphProps,
+ pCanGlyphAlone,
+ out actualGlyphCount
+ );
+
+ // Step 2: Call GetGlyphPlacements with the glyph data
+ int[] glyphAdvances = new int[actualGlyphCount];
+ GlyphOffset[] glyphOffsets;
+
+ fixed (int* pGlyphAdvances = glyphAdvances)
+ {
+ analyzer.GetGlyphPlacements(
+ pText,
+ pClusterMap,
+ pTextProps,
+ textLength,
+ pGlyphIndices,
+ pGlyphProps,
+ actualGlyphCount,
+ font,
+ fontSize,
+ 1.0, // scalingFactor
+ isSideways,
+ isRightToLeft,
+ CultureInfo.InvariantCulture,
+ null, // features
+ null, // featureRangeLengths
+ System.Windows.Media.TextFormattingMode.Ideal,
+ itemProps,
+ 1.0f, // pixelsPerDip
+ pGlyphAdvances,
+ out glyphOffsets
+ );
+ }
+
+ return (glyphAdvances, glyphOffsets);
+ }
+ }
+
+ [Fact]
+ public void GetGlyphPlacements_SimpleText_ReturnsAdvances()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("Hello");
+ var itemProps = (ItemProps)spans![0].element;
+
+ var result = GetGlyphPlacementsDirect("Hello", font, itemProps);
+
+ result.Should().NotBeNull();
+ var (glyphAdvances, glyphOffsets) = result!.Value;
+
+ // Should have advances for each glyph
+ glyphAdvances.Should().NotBeEmpty();
+ glyphAdvances.Length.Should().BeGreaterThanOrEqualTo(5);
+
+ // All advances should be positive
+ glyphAdvances.Should().AllSatisfy(a => a.Should().BeGreaterThan(0));
+
+ // Offsets should exist
+ glyphOffsets.Should().NotBeEmpty();
+ }
+
+ [Fact]
+ public void GetGlyphPlacements_DifferentFontSizes_ScalesAdvances()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("A");
+ var itemProps = (ItemProps)spans![0].element;
+
+ var result12 = GetGlyphPlacementsDirect("A", font, itemProps, fontSize: 12.0);
+ var result24 = GetGlyphPlacementsDirect("A", font, itemProps, fontSize: 24.0);
+
+ result12.Should().NotBeNull();
+ result24.Should().NotBeNull();
+
+ var advance12 = result12!.Value.glyphAdvances[0];
+ var advance24 = result24!.Value.glyphAdvances[0];
+
+ // Larger font = larger advances
+ advance24.Should().BeGreaterThan(advance12);
+ ((double)advance24 / advance12).Should().BeApproximately(2.0, 0.1);
+ }
+
+ [Fact]
+ public void GetGlyphPlacements_MultipleCharacters_ReturnsCorrectCount()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("ABCDE");
+ var itemProps = (ItemProps)spans![0].element;
+
+ var result = GetGlyphPlacementsDirect("ABCDE", font, itemProps);
+
+ result.Should().NotBeNull();
+ var (glyphAdvances, glyphOffsets) = result!.Value;
+
+ // For simple Latin, should have 5 glyphs
+ glyphAdvances.Length.Should().Be(5);
+ glyphOffsets.Length.Should().Be(5);
+ }
+
+ [Fact]
+ public void GetGlyphPlacements_Offsets_AreReasonable()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("Test");
+ var itemProps = (ItemProps)spans![0].element;
+
+ var result = GetGlyphPlacementsDirect("Test", font, itemProps);
+
+ result.Should().NotBeNull();
+ var glyphOffsets = result!.Value.glyphOffsets;
+
+ // For simple Latin text, offsets should be small/zero
+ foreach (var offset in glyphOffsets)
+ {
+ Math.Abs(offset.du).Should().BeLessThan(1000);
+ Math.Abs(offset.dv).Should().BeLessThan(1000);
+ }
+ }
+
+ [Fact]
+ public void GetGlyphPlacements_RightToLeft_Succeeds()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("مرحبا");
+ Assert.SkipUnless(spans != null && spans.Count > 0, "Itemization failed");
+
+ var itemProps = (ItemProps)spans[0].element;
+
+ var result = GetGlyphPlacementsDirect("مرحبا", font, itemProps, isRightToLeft: true);
+
+ result.Should().NotBeNull();
+ result!.Value.glyphAdvances.Should().NotBeEmpty();
+ }
+
+ [Fact]
+ public void GetGlyphPlacements_ConsistentWithGetGlyphsAndTheirPlacements()
+ {
+ var font = GetTestFont();
+ var spans = ItemizeHelper.Itemize("Hello");
+ var itemProps = (ItemProps)spans![0].element;
+
+ // Get results from both methods
+ var directResult = GetGlyphPlacementsDirect("Hello", font, itemProps);
+ var combinedResult = GetGlyphsAndPlacements("Hello", font, itemProps);
+
+ directResult.Should().NotBeNull();
+ combinedResult.Should().NotBeNull();
+
+ // Advances should match
+ directResult!.Value.glyphAdvances.Should().BeEquivalentTo(combinedResult!.Value.glyphAdvances);
+ }
+
+ #endregion
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TrueTypeSubsetterTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TrueTypeSubsetterTests.cs
new file mode 100644
index 00000000000..bbf64c98105
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TrueTypeSubsetterTests.cs
@@ -0,0 +1,341 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers.Binary;
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for class.
+///
+public class TrueTypeSubsetterTests
+{
+ // TrueType font structure constants
+ private const uint TrueTypeSignature = 0x00010000;
+ private const int OffsetTableSize = 12; // sfntVersion(4) + numTables(2) + searchRange(2) + entrySelector(2) + rangeShift(2)
+ private const int TableRecordSize = 16; // tag(4) + checksum(4) + offset(4) + length(4)
+
+ // Required TrueType tables for a valid subset
+ private static readonly string[] s_requiredTables = ["head", "hhea", "hmtx", "maxp", "cmap", "loca", "glyf"];
+
+ private byte[] LoadArialFontData()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial font not found");
+ return File.ReadAllBytes(TestHelpers.ArialPath);
+ }
+
+ ///
+ /// Gets glyph indices for the given code points using the specified font face.
+ ///
+ private static unsafe ushort[] GetGlyphIndices(FontFace fontFace, uint[] codePoints)
+ {
+ ushort[] glyphIndices = new ushort[codePoints.Length];
+ fixed (uint* pCodePoints = codePoints)
+ fixed (ushort* pGlyphIndices = glyphIndices)
+ {
+ fontFace.GetArrayOfGlyphIndices(pCodePoints, (uint)codePoints.Length, pGlyphIndices);
+ }
+ return glyphIndices;
+ }
+
+ ///
+ /// Reads a 4-character table tag from a byte span.
+ ///
+ private static string ReadTableTag(ReadOnlySpan data)
+ {
+ return new string([(char)data[0], (char)data[1], (char)data[2], (char)data[3]]);
+ }
+
+ ///
+ /// Finds a table in a TrueType font and returns its offset and length.
+ /// Returns (0, 0) if not found.
+ ///
+ private static (uint offset, uint length) FindTable(byte[] fontData, string tableTag)
+ {
+ ushort numTables = BinaryPrimitives.ReadUInt16BigEndian(fontData.AsSpan(4));
+
+ for (int i = 0; i < numTables; i++)
+ {
+ int recordOffset = OffsetTableSize + (i * TableRecordSize);
+ string tag = ReadTableTag(fontData.AsSpan(recordOffset, 4));
+ if (tag == tableTag)
+ {
+ uint offset = BinaryPrimitives.ReadUInt32BigEndian(fontData.AsSpan(recordOffset + 8, 4));
+ uint length = BinaryPrimitives.ReadUInt32BigEndian(fontData.AsSpan(recordOffset + 12, 4));
+ return (offset, length);
+ }
+ }
+
+ return (0, 0);
+ }
+
+ ///
+ /// Reads numGlyphs from the maxp table.
+ /// maxp table structure: version (4 bytes), numGlyphs (2 bytes), ...
+ ///
+ private static ushort ReadMaxpNumGlyphs(byte[] fontData)
+ {
+ var (offset, length) = FindTable(fontData, "maxp");
+ if (offset == 0 || length < 6)
+ return 0;
+
+ return BinaryPrimitives.ReadUInt16BigEndian(fontData.AsSpan((int)offset + 4, 2));
+ }
+
+ ///
+ /// Parses the table directory from a TrueType font and returns table names.
+ ///
+ private static HashSet GetTableNames(byte[] fontData)
+ {
+ var tables = new HashSet();
+ ushort numTables = BinaryPrimitives.ReadUInt16BigEndian(fontData.AsSpan(4));
+
+ for (int i = 0; i < numTables; i++)
+ {
+ int recordOffset = OffsetTableSize + (i * TableRecordSize);
+ string tag = ReadTableTag(fontData.AsSpan(recordOffset, 4));
+ tables.Add(tag);
+ }
+
+ return tables;
+ }
+
+ [Fact]
+ public void ComputeSubset_WithValidFont_ShouldReturnValidTrueTypeStructure()
+ {
+ var fontData = LoadArialFontData();
+
+ var arialUri = new Uri(TestHelpers.ArialPath);
+ ushort[] glyphArray = [0, 1, 2, 3]; // Basic glyph indices
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, arialUri, 0, glyphArray);
+
+ result.Should().NotBeNull();
+ result.Should().NotBeEmpty();
+
+ // Verify TrueType signature (sfntVersion = 0x00010000)
+ uint signature = BinaryPrimitives.ReadUInt32BigEndian(result.AsSpan(0, 4));
+ signature.Should().Be(TrueTypeSignature, "Subset should have valid TrueType signature");
+
+ // Verify numTables is reasonable (at least the required tables)
+ ushort numTables = BinaryPrimitives.ReadUInt16BigEndian(result.AsSpan(4, 2));
+ numTables.Should().BeGreaterThanOrEqualTo((ushort)s_requiredTables.Length,
+ $"Subset should contain at least {s_requiredTables.Length} tables");
+
+ // Verify offset table fields are consistent
+ ushort searchRange = BinaryPrimitives.ReadUInt16BigEndian(result.AsSpan(6, 2));
+ ushort rangeShift = BinaryPrimitives.ReadUInt16BigEndian(result.AsSpan(10, 2));
+
+ // searchRange = (maximum power of 2 <= numTables) * 16
+ int maxPow2 = 1;
+ while (maxPow2 * 2 <= numTables) maxPow2 *= 2;
+ searchRange.Should().Be((ushort)(maxPow2 * 16), "searchRange should be correctly calculated");
+
+ // rangeShift = numTables * 16 - searchRange
+ rangeShift.Should().Be((ushort)(numTables * 16 - searchRange), "rangeShift should be correctly calculated");
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_ShouldContainRequiredTables()
+ {
+ var fontData = LoadArialFontData();
+
+ var arialUri = new Uri(TestHelpers.ArialPath);
+ ushort[] glyphArray = [0, 1, 2, 3];
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, arialUri, 0, glyphArray);
+
+ var subsetTables = GetTableNames(result);
+
+ // Verify all required tables are present
+ foreach (var requiredTable in s_requiredTables)
+ {
+ subsetTables.Should().Contain(requiredTable,
+ $"Subset should contain required table '{requiredTable}'");
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_TableOffsetsAndLengths_ShouldBeValid()
+ {
+ var fontData = LoadArialFontData();
+
+ var arialUri = new Uri(TestHelpers.ArialPath);
+ ushort[] glyphArray = [0, 1, 2, 3];
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, arialUri, 0, glyphArray);
+
+ ushort numTables = BinaryPrimitives.ReadUInt16BigEndian(result.AsSpan(4, 2));
+
+ for (int i = 0; i < numTables; i++)
+ {
+ int recordOffset = OffsetTableSize + (i * TableRecordSize);
+
+ // Read table record fields
+ string tag = ReadTableTag(result.AsSpan(recordOffset, 4));
+ uint tableOffset = BinaryPrimitives.ReadUInt32BigEndian(result.AsSpan(recordOffset + 8, 4));
+ uint tableLength = BinaryPrimitives.ReadUInt32BigEndian(result.AsSpan(recordOffset + 12, 4));
+
+ // Verify offset and length are within bounds
+ tableOffset.Should().BeLessThan((uint)result.Length,
+ $"Table '{tag}' offset should be within file bounds");
+ (tableOffset + tableLength).Should().BeLessThanOrEqualTo((uint)result.Length,
+ $"Table '{tag}' should not extend beyond file end");
+ tableLength.Should().BeGreaterThan(0,
+ $"Table '{tag}' should have non-zero length");
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_WithNullGlyphArray_ShouldThrow()
+ {
+ var fontData = LoadArialFontData();
+
+ var arialUri = new Uri(TestHelpers.ArialPath);
+
+ Assert.Throws(() =>
+ {
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, arialUri, 0, null!);
+ }
+ }
+ });
+ }
+
+ [Fact]
+ public void ComputeSubset_WithEmptyGlyphArray_ShouldThrow()
+ {
+ var fontData = LoadArialFontData();
+
+ var arialUri = new Uri(TestHelpers.ArialPath);
+ ushort[] glyphArray = [];
+
+ Assert.Throws(() =>
+ {
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, arialUri, 0, glyphArray);
+ }
+ }
+ });
+ }
+
+ [Fact]
+ public void ComputeSubset_SubsetShouldBeSmallerThanOriginal()
+ {
+ var fontData = LoadArialFontData();
+
+ var arialUri = new Uri(TestHelpers.ArialPath);
+
+ // Get actual glyph indices for characters 'A', 'B', 'C' plus .notdef (0)
+ var factory = DWriteFactory.Instance;
+ var fontFace = factory.CreateFontFace(new Uri(TestHelpers.ArialPath), 0);
+ ushort[] glyphIndices;
+ try
+ {
+ uint[] codePoints = ['A', 'B', 'C'];
+ var charGlyphs = GetGlyphIndices(fontFace, codePoints);
+ glyphIndices = [0, charGlyphs[0], charGlyphs[1], charGlyphs[2]]; // .notdef + A, B, C
+ }
+ finally
+ {
+ fontFace.Release();
+ }
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, arialUri, 0, glyphIndices);
+
+ result.Should().NotBeNull();
+
+ // Subset with few glyphs should be smaller than original
+ // Note: The subsetter keeps the full glyph table structure (maxp.numGlyphs unchanged)
+ // but only embeds outlines for the requested glyphs, reducing overall file size
+ result.Length.Should().BeLessThan(fontData.Length,
+ "Subset should be smaller than original font");
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_OutputShouldBeValidTrueType()
+ {
+ var fontData = LoadArialFontData();
+
+ var arialUri = new Uri(TestHelpers.ArialPath);
+ ushort[] glyphArray = [0, 1, 2, 3];
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, arialUri, 0, glyphArray);
+
+ result.Should().NotBeNull();
+ result.Length.Should().BeGreaterThan(OffsetTableSize + TableRecordSize,
+ "Output should be large enough to contain header and at least one table record");
+
+ // Verify TrueType signature using BinaryPrimitives
+ uint signature = BinaryPrimitives.ReadUInt32BigEndian(result.AsSpan(0, 4));
+
+ // Valid TrueType signature is 0x00010000
+ bool isValidTrueType = signature is 0x00010000 or 0x74727565 or 0x4F54544F; // 0x00010000, 'true', 'OTTO'
+
+ isValidTrueType.Should().BeTrue($"Output should have valid TrueType signature, got 0x{signature:X8}");
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_WithDifferentGlyphCounts_ShouldProduceDifferentSizes()
+ {
+ var fontData = LoadArialFontData();
+
+ var arialUri = new Uri(TestHelpers.ArialPath);
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ // Small subset
+ ushort[] smallGlyphArray = [0, 1, 2];
+ var smallResult = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, arialUri, 0, smallGlyphArray);
+
+ // Larger subset
+ ushort[] largeGlyphArray = Enumerable.Range(0, 100).Select(i => (ushort)i).ToArray();
+ var largeResult = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, arialUri, 0, largeGlyphArray);
+
+ smallResult.Should().NotBeNull();
+ largeResult.Should().NotBeNull();
+
+ // More glyphs should generally result in larger subset
+ largeResult.Length.Should().BeGreaterThanOrEqualTo(smallResult.Length);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TtfDeltaCoverageTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TtfDeltaCoverageTests.cs
new file mode 100644
index 00000000000..b3282e4adf0
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TtfDeltaCoverageTests.cs
@@ -0,0 +1,808 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests designed to improve code coverage for TtfDelta font subsetting.
+/// These tests exercise different font types and glyph ranges to trigger
+/// various code paths in the TtfDelta subsetter.
+///
+public class TtfDeltaCoverageTests
+{
+ // Font paths for different test scenarios
+ private static readonly string s_simSunPath = Path.Combine(TestHelpers.FontsDirectory, "simsun.ttc");
+ private static readonly string s_msGothicPath = Path.Combine(TestHelpers.FontsDirectory, "msgothic.ttc");
+ private static readonly string s_segoeUIPath = Path.Combine(TestHelpers.FontsDirectory, "segoeui.ttf");
+ private static readonly string s_segoeUIEmojiPath = Path.Combine(TestHelpers.FontsDirectory, "seguiemj.ttf");
+ private static readonly string s_malgunPath = Path.Combine(TestHelpers.FontsDirectory, "malgun.ttf");
+ private static readonly string s_gabriolaPath = Path.Combine(TestHelpers.FontsDirectory, "Gabriola.ttf");
+ private static readonly string s_timesPath = Path.Combine(TestHelpers.FontsDirectory, "times.ttf");
+ private static readonly string s_courierPath = Path.Combine(TestHelpers.FontsDirectory, "cour.ttf");
+ private static readonly string s_tahomaPath = Path.Combine(TestHelpers.FontsDirectory, "tahoma.ttf");
+ private static readonly string s_verdanaPath = Path.Combine(TestHelpers.FontsDirectory, "verdana.ttf");
+ private static readonly string s_georgiaPath = Path.Combine(TestHelpers.FontsDirectory, "georgia.ttf");
+ private static readonly string s_consolasPath = Path.Combine(TestHelpers.FontsDirectory, "consola.ttf");
+ private static readonly string s_calibriPath = Path.Combine(TestHelpers.FontsDirectory, "calibri.ttf");
+ private static readonly string s_comicSansPath = Path.Combine(TestHelpers.FontsDirectory, "comic.ttf");
+ private static readonly string s_impactPath = Path.Combine(TestHelpers.FontsDirectory, "impact.ttf");
+ private static readonly string s_symbolPath = Path.Combine(TestHelpers.FontsDirectory, "symbol.ttf");
+ private static readonly string s_wingdingsPath = Path.Combine(TestHelpers.FontsDirectory, "wingding.ttf");
+ private static readonly string s_webdingsPath = Path.Combine(TestHelpers.FontsDirectory, "webdings.ttf");
+ private static readonly string s_segoeSymbolPath = Path.Combine(TestHelpers.FontsDirectory, "seguisym.ttf");
+
+ #region TTC (TrueType Collection) Tests - Exercises directory offset handling
+
+ ///
+ /// Reads the TTC header to get the offset for a specific font index.
+ /// TTC format: 'ttcf' tag (4 bytes), version (4 bytes), numFonts (4 bytes), offset[numFonts] (4 bytes each)
+ ///
+ private static int GetTTCFontOffset(byte[] fontData, int fontIndex)
+ {
+ // Check for 'ttcf' signature
+ if (fontData.Length < 12)
+ return 0;
+
+ uint tag = (uint)((fontData[0] << 24) | (fontData[1] << 16) | (fontData[2] << 8) | fontData[3]);
+ if (tag != 0x74746366) // 'ttcf'
+ return 0;
+
+ uint numFonts = (uint)((fontData[8] << 24) | (fontData[9] << 16) | (fontData[10] << 8) | fontData[11]);
+ if (fontIndex >= numFonts)
+ return 0;
+
+ // Offset table starts at byte 12
+ int offsetPos = 12 + (fontIndex * 4);
+ if (offsetPos + 4 > fontData.Length)
+ return 0;
+
+ return (int)((fontData[offsetPos] << 24) | (fontData[offsetPos + 1] << 16) |
+ (fontData[offsetPos + 2] << 8) | fontData[offsetPos + 3]);
+ }
+
+ [Fact]
+ public void ComputeSubset_CambriaTTC_FirstFont_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.CambriaPath), "Cambria TTC not found");
+ var fontData = File.ReadAllBytes(TestHelpers.CambriaPath);
+ var uri = new Uri(TestHelpers.CambriaPath);
+
+ // Get the proper offset for the first font in the TTC
+ int fontOffset = GetTTCFontOffset(fontData, 0);
+ ushort[] glyphArray = Enumerable.Range(0, 50).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, fontOffset, glyphArray);
+ result.Should().NotBeNull();
+ result.Length.Should().BeGreaterThan(0);
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_SimSunTTC_WithCJKGlyphs_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_simSunPath), "SimSun TTC not found");
+ var fontData = File.ReadAllBytes(s_simSunPath);
+ var uri = new Uri(s_simSunPath);
+
+ // Get the proper offset for the first font in the TTC
+ int fontOffset = GetTTCFontOffset(fontData, 0);
+ // CJK fonts have many glyphs - request a range that exercises CMAP handling
+ ushort[] glyphArray = Enumerable.Range(0, 500).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, fontOffset, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_MsGothicTTC_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_msGothicPath), "MS Gothic TTC not found");
+ var fontData = File.ReadAllBytes(s_msGothicPath);
+ var uri = new Uri(s_msGothicPath);
+
+ // Get the proper offset for the first font in the TTC
+ int fontOffset = GetTTCFontOffset(fontData, 0);
+ ushort[] glyphArray = Enumerable.Range(0, 200).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, fontOffset, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_CambriaTTC_SecondFont_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.CambriaPath), "Cambria TTC not found");
+ var fontData = File.ReadAllBytes(TestHelpers.CambriaPath);
+ var uri = new Uri(TestHelpers.CambriaPath);
+
+ // Get the proper offset for the second font in the TTC (Cambria Math)
+ int fontOffset = GetTTCFontOffset(fontData, 1);
+ Assert.SkipUnless(fontOffset > 0, "Second font not found in TTC");
+ ushort[] glyphArray = Enumerable.Range(0, 100).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, fontOffset, glyphArray);
+ result.Should().NotBeNull();
+ result.Length.Should().BeGreaterThan(0);
+ }
+ }
+ }
+
+ #endregion
+
+ #region High Glyph Index Tests - Exercises extended glyph handling
+
+ [Fact]
+ public void ComputeSubset_WithHighGlyphIndices_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ // Request high glyph indices - exercises different code paths
+ ushort[] glyphArray = [0, 100, 500, 1000, 1500, 2000];
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_WithSparseGlyphIndices_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ // Sparse glyph indices - exercises CMAP lookup
+ ushort[] glyphArray = [0, 10, 50, 100, 200, 500, 1000, 2000, 3000];
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Large Glyph Count Tests - Exercises table compression and optimization
+
+ [Fact]
+ public void ComputeSubset_WithManyGlyphs_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ // Request many glyphs to exercise table processing
+ ushort[] glyphArray = Enumerable.Range(0, 1000).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_WithVeryManyGlyphs_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ // Even more glyphs - may trigger ERR_WOULD_GROW path
+ ushort[] glyphArray = Enumerable.Range(0, 2500).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_CJKFont_WithManyGlyphs_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_malgunPath), "Malgun Gothic not found");
+ var fontData = File.ReadAllBytes(s_malgunPath);
+ var uri = new Uri(s_malgunPath);
+
+ // Korean font with many glyphs
+ ushort[] glyphArray = Enumerable.Range(0, 2000).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Different Font Type Tests - Exercises various table handling code
+
+ [Fact]
+ public void ComputeSubset_SegoeUI_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_segoeUIPath), "Segoe UI not found");
+ var fontData = File.ReadAllBytes(s_segoeUIPath);
+ var uri = new Uri(s_segoeUIPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 300).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_Gabriola_OpenTypeFeatures_ShouldWork()
+ {
+ // Gabriola has many OpenType features (stylistic sets, swashes)
+ Assert.SkipUnless(File.Exists(s_gabriolaPath), "Gabriola not found");
+ var fontData = File.ReadAllBytes(s_gabriolaPath);
+ var uri = new Uri(s_gabriolaPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 500).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_TimesNewRoman_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_timesPath), "Times New Roman not found");
+ var fontData = File.ReadAllBytes(s_timesPath);
+ var uri = new Uri(s_timesPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 200).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_CourierNew_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_courierPath), "Courier New not found");
+ var fontData = File.ReadAllBytes(s_courierPath);
+ var uri = new Uri(s_courierPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 200).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_Tahoma_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_tahomaPath), "Tahoma not found");
+ var fontData = File.ReadAllBytes(s_tahomaPath);
+ var uri = new Uri(s_tahomaPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 300).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_Verdana_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_verdanaPath), "Verdana not found");
+ var fontData = File.ReadAllBytes(s_verdanaPath);
+ var uri = new Uri(s_verdanaPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 300).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_Georgia_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_georgiaPath), "Georgia not found");
+ var fontData = File.ReadAllBytes(s_georgiaPath);
+ var uri = new Uri(s_georgiaPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 300).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_Consolas_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_consolasPath), "Consolas not found");
+ var fontData = File.ReadAllBytes(s_consolasPath);
+ var uri = new Uri(s_consolasPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 300).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_Calibri_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_calibriPath), "Calibri not found");
+ var fontData = File.ReadAllBytes(s_calibriPath);
+ var uri = new Uri(s_calibriPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 500).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_ComicSans_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_comicSansPath), "Comic Sans not found");
+ var fontData = File.ReadAllBytes(s_comicSansPath);
+ var uri = new Uri(s_comicSansPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 200).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_Impact_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_impactPath), "Impact not found");
+ var fontData = File.ReadAllBytes(s_impactPath);
+ var uri = new Uri(s_impactPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 200).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Symbol Font Tests - Exercises symbol CMAP encoding
+
+ [Fact]
+ public void ComputeSubset_Symbol_ShouldWork()
+ {
+ // Symbol font uses a different CMAP encoding
+ Assert.SkipUnless(File.Exists(s_symbolPath), "Symbol not found");
+ var fontData = File.ReadAllBytes(s_symbolPath);
+ var uri = new Uri(s_symbolPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 100).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_Wingdings_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_wingdingsPath), "Wingdings not found");
+ var fontData = File.ReadAllBytes(s_wingdingsPath);
+ var uri = new Uri(s_wingdingsPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 150).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_Webdings_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_webdingsPath), "Webdings not found");
+ var fontData = File.ReadAllBytes(s_webdingsPath);
+ var uri = new Uri(s_webdingsPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 150).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_SegoeSymbol_ShouldWork()
+ {
+ // Segoe UI Symbol has extended Unicode ranges
+ Assert.SkipUnless(File.Exists(s_segoeSymbolPath), "Segoe UI Symbol not found");
+ var fontData = File.ReadAllBytes(s_segoeSymbolPath);
+ var uri = new Uri(s_segoeSymbolPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 500).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Emoji Font Tests - May exercise Format 12 CMAP
+
+ [Fact]
+ public void ComputeSubset_SegoeUIEmoji_ShouldWork()
+ {
+ // Emoji font may have Format 12 CMAP for supplementary plane
+ Assert.SkipUnless(File.Exists(s_segoeUIEmojiPath), "Segoe UI Emoji not found");
+ var fontData = File.ReadAllBytes(s_segoeUIEmojiPath);
+ var uri = new Uri(s_segoeUIEmojiPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 500).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_SegoeUIEmoji_WithManyGlyphs_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(s_segoeUIEmojiPath), "Segoe UI Emoji not found");
+ var fontData = File.ReadAllBytes(s_segoeUIEmojiPath);
+ var uri = new Uri(s_segoeUIEmojiPath);
+
+ // Emoji font has thousands of glyphs
+ ushort[] glyphArray = Enumerable.Range(0, 2000).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Edge Case Glyph Patterns
+
+ [Fact]
+ public void ComputeSubset_SingleGlyph_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ // Single glyph - minimum subset
+ ushort[] glyphArray = [0];
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_ConsecutiveGlyphs_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ // Consecutive glyphs - exercises sequential optimization
+ ushort[] glyphArray = Enumerable.Range(100, 200).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_RandomGlyphs_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ // Random-ish pattern
+ ushort[] glyphArray = [0, 5, 17, 42, 88, 123, 256, 512, 777, 1024, 1111, 1500];
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_BoundaryGlyphs_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ // Boundary values
+ ushort[] glyphArray = [0, 1, 255, 256, 65534];
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_AllLowGlyphs_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ // All glyphs in low range (0-255) - exercises different table formats
+ ushort[] glyphArray = Enumerable.Range(0, 256).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Glyph Range Pattern Tests
+
+ [Fact]
+ public void ComputeSubset_LatinExtended_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ // Latin Extended range (glyphs for accented characters)
+ ushort[] glyphArray = Enumerable.Range(0, 400).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_GreekAndCyrillic_ShouldWork()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ // Greek and Cyrillic ranges
+ ushort[] glyphArray = Enumerable.Range(300, 400).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+ result.Should().NotBeNull();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Output Validation Tests
+
+ [Fact]
+ public void ComputeSubset_OutputHasValidStructure()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ var fontData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var uri = new Uri(TestHelpers.ArialPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 100).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pFontData = fontData)
+ {
+ var result = TrueTypeSubsetter.ComputeSubset(pFontData, fontData.Length, uri, 0, glyphArray);
+
+ result.Should().NotBeNull();
+ result.Length.Should().BeGreaterThan(12); // Minimum TrueType header size
+
+ // Verify offset table structure
+ // numTables is at offset 4-5
+ ushort numTables = (ushort)((result[4] << 8) | result[5]);
+ numTables.Should().BeGreaterThan(0).And.BeLessThan(100);
+ }
+ }
+ }
+
+ [Fact]
+ public void ComputeSubset_DifferentFonts_ProduceDifferentOutput()
+ {
+ Assert.SkipUnless(File.Exists(TestHelpers.ArialPath), "Arial not found");
+ Assert.SkipUnless(File.Exists(s_timesPath), "Times not found");
+
+ var arialData = File.ReadAllBytes(TestHelpers.ArialPath);
+ var timesData = File.ReadAllBytes(s_timesPath);
+ var arialUri = new Uri(TestHelpers.ArialPath);
+ var timesUri = new Uri(s_timesPath);
+
+ ushort[] glyphArray = Enumerable.Range(0, 50).Select(i => (ushort)i).ToArray();
+
+ unsafe
+ {
+ fixed (byte* pArial = arialData)
+ fixed (byte* pTimes = timesData)
+ {
+ var arialResult = TrueTypeSubsetter.ComputeSubset(pArial, arialData.Length, arialUri, 0, glyphArray);
+ var timesResult = TrueTypeSubsetter.ComputeSubset(pTimes, timesData.Length, timesUri, 0, glyphArray);
+
+ arialResult.Should().NotBeNull();
+ timesResult.Should().NotBeNull();
+
+ // Results should be different
+ arialResult.SequenceEqual(timesResult).Should().BeFalse();
+ }
+ }
+ }
+
+ #endregion
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TtfDeltaTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TtfDeltaTests.cs
new file mode 100644
index 00000000000..d3eebb8c30b
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/DirectWriteForwarder.Tests/TtfDeltaTests.cs
@@ -0,0 +1,176 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Concurrent;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace MS.Internal.Text.TextInterface.Tests;
+
+///
+/// Tests for TtfDelta font subsetting internals.
+/// These tests exercise the MS.Internal.TtfDelta namespace which contains
+/// TrueType font subsetting functionality used by WPF.
+///
+public class TtfDeltaTests
+{
+ ///
+ /// Gets the GlobalInit type from the TtfDelta namespace.
+ ///
+ private static Type GetGlobalInitType()
+ {
+ var asm = typeof(MS.Internal.TrueTypeSubsetter).Assembly;
+ return asm.GetTypes().First(t => t.Name == "GlobalInit" && t.Namespace == "MS.Internal.TtfDelta");
+ }
+
+ ///
+ /// Gets the ControlTableInit type from the TtfDelta namespace.
+ ///
+ private static Type GetControlTableInitType()
+ {
+ var asm = typeof(MS.Internal.TrueTypeSubsetter).Assembly;
+ return asm.GetTypes().First(t => t.Name == "ControlTableInit" && t.Namespace == "MS.Internal.TtfDelta");
+ }
+
+ [Fact]
+ public void GlobalInit_Init_CanBeInvoked()
+ {
+ // Arrange
+ var globalInitType = GetGlobalInitType();
+ var initMethod = globalInitType.GetMethod("Init", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
+
+ // Act & Assert
+ initMethod.Should().NotBeNull("GlobalInit.Init should be accessible");
+
+ // Invoke should not throw
+ var exception = Record.Exception(() => initMethod!.Invoke(null, null));
+ exception.Should().BeNull("GlobalInit.Init() should complete without throwing");
+ }
+
+ [Fact]
+ public void GlobalInit_Init_CanBeCalledMultipleTimes()
+ {
+ // The Init method uses a lock and _isInitialized flag, so multiple calls should be safe
+ var globalInitType = GetGlobalInitType();
+ var initMethod = globalInitType.GetMethod("Init", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)!;
+
+ // Call multiple times - should not throw
+ for (int i = 0; i < 5; i++)
+ {
+ var exception = Record.Exception(() => initMethod.Invoke(null, null));
+ exception.Should().BeNull($"GlobalInit.Init() call {i + 1} should not throw");
+ }
+ }
+
+ [Fact]
+ public void ControlTableInit_Init_CanBeInvoked()
+ {
+ // Arrange
+ var controlTableInitType = GetControlTableInitType();
+ var initMethod = controlTableInitType.GetMethod("Init", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
+
+ // Act & Assert
+ initMethod.Should().NotBeNull("ControlTableInit.Init should be accessible");
+
+ // Invoke should not throw
+ var exception = Record.Exception(() => initMethod!.Invoke(null, null));
+ exception.Should().BeNull("ControlTableInit.Init() should complete without throwing");
+ }
+
+ [Fact]
+ public void ControlTableInit_Init_CanBeCalledMultipleTimes()
+ {
+ // The Init method uses a lock and _isInitialized flag, so multiple calls should be safe
+ var controlTableInitType = GetControlTableInitType();
+ var initMethod = controlTableInitType.GetMethod("Init", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)!;
+
+ // Call multiple times - should not throw
+ for (int i = 0; i < 5; i++)
+ {
+ var exception = Record.Exception(() => initMethod.Invoke(null, null));
+ exception.Should().BeNull($"ControlTableInit.Init() call {i + 1} should not throw");
+ }
+ }
+
+ [Fact]
+ public void TtfDelta_Namespace_ContainsExpectedTypes()
+ {
+ // Verify that the expected TtfDelta types are present and accessible
+ var asm = typeof(MS.Internal.TrueTypeSubsetter).Assembly;
+ var ttfDeltaTypes = asm.GetTypes()
+ .Where(t => t.Namespace == "MS.Internal.TtfDelta")
+ .ToList();
+
+ ttfDeltaTypes.Should().NotBeEmpty("TtfDelta namespace should contain types");
+
+ // Verify key types exist
+ var typeNames = ttfDeltaTypes.Select(t => t.Name).ToHashSet();
+ typeNames.Should().Contain("GlobalInit");
+ typeNames.Should().Contain("ControlTableInit");
+ typeNames.Should().Contain("TTFACC_FILEBUFFERINFO");
+ typeNames.Should().Contain("CONST_TTFACC_FILEBUFFERINFO");
+ }
+
+ [Fact]
+ public void TtfDelta_StructTypes_AreValueTypes()
+ {
+ // Verify that struct types in TtfDelta are correctly defined as value types
+ var asm = typeof(MS.Internal.TrueTypeSubsetter).Assembly;
+ var structTypes = new[] { "TTFACC_FILEBUFFERINFO", "CONST_TTFACC_FILEBUFFERINFO", "DIRECTORY", "HEAD", "MAXP" };
+
+ foreach (var typeName in structTypes)
+ {
+ var type = asm.GetTypes().FirstOrDefault(t => t.Name == typeName && t.Namespace == "MS.Internal.TtfDelta");
+ type?.IsValueType.Should().BeTrue($"{typeName} should be a value type (struct)");
+ }
+ }
+
+ [Fact]
+ public async Task TtfDelta_InitMethods_AreThreadSafe()
+ {
+ // Both GlobalInit.Init and ControlTableInit.Init use Monitor.Enter for thread safety
+ // Verify this by invoking from multiple threads simultaneously
+ var globalInitType = GetGlobalInitType();
+ var controlTableInitType = GetControlTableInitType();
+ var globalInit = globalInitType.GetMethod("Init", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)!;
+ var controlTableInit = controlTableInitType.GetMethod("Init", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)!;
+
+ var tasks = new List();
+ var exceptions = new ConcurrentBag();
+
+ // Run 10 parallel tasks that call both Init methods
+ for (int i = 0; i < 10; i++)
+ {
+ tasks.Add(Task.Run(() =>
+ {
+ try
+ {
+ globalInit.Invoke(null, null);
+ controlTableInit.Invoke(null, null);
+ }
+ catch (Exception ex)
+ {
+ exceptions.Add(ex);
+ }
+ }, TestContext.Current.CancellationToken));
+ }
+
+ await Task.WhenAll(tasks);
+
+ exceptions.Should().BeEmpty("Init methods should be thread-safe and not throw exceptions");
+ }
+
+ [Fact]
+ public void TrueTypeSubsetter_Assembly_HasInternalsVisibleToTestProject()
+ {
+ // Verify that the InternalsVisibleTo attribute is set correctly
+ // by checking that we can access internal types
+ var globalInitType = GetGlobalInitType();
+
+ // GlobalInit is a "private ref class" in C++/CLI, which maps to internal in C#
+ // We should be able to access it due to InternalsVisibleTo
+ globalInitType.Should().NotBeNull("GlobalInit should be accessible to test project");
+ globalInitType.IsNotPublic.Should().BeTrue("GlobalInit should be internal (not public)");
+ }
+}
+