diff --git a/src/EPPlus.Export.Pdf/ExcelPdf.cs b/src/EPPlus.Export.Pdf/ExcelPdf.cs index 11ae3f555..e53878908 100644 --- a/src/EPPlus.Export.Pdf/ExcelPdf.cs +++ b/src/EPPlus.Export.Pdf/ExcelPdf.cs @@ -24,6 +24,7 @@ Date Author Change using EPPlus.Export.Pdf.PdfSettings; using OfficeOpenXml.Style; using EPPlus.Fonts.OpenType; +using OfficeOpenXml.Interfaces.Fonts; namespace EPPlus.Export.Pdf { diff --git a/src/EPPlus.Export.Pdf/PdfLayout/PdfCellContentLayout.cs b/src/EPPlus.Export.Pdf/PdfLayout/PdfCellContentLayout.cs index b2213b74f..f90db2eeb 100644 --- a/src/EPPlus.Export.Pdf/PdfLayout/PdfCellContentLayout.cs +++ b/src/EPPlus.Export.Pdf/PdfLayout/PdfCellContentLayout.cs @@ -18,6 +18,7 @@ Date Author Change using EPPlus.Graphics.Math; using OfficeOpenXml; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using OfficeOpenXml.Style; using System.Collections.Generic; using System.Linq; diff --git a/src/EPPlus.Export.Pdf/PdfLayout/PdfCellData.cs b/src/EPPlus.Export.Pdf/PdfLayout/PdfCellData.cs index 96cbd3957..b2a4fd248 100644 --- a/src/EPPlus.Export.Pdf/PdfLayout/PdfCellData.cs +++ b/src/EPPlus.Export.Pdf/PdfLayout/PdfCellData.cs @@ -13,6 +13,7 @@ Date Author Change using EPPlus.Export.Pdf.Pdfhelpers; using EPPlus.Fonts.OpenType; using EPPlus.Graphics; +using OfficeOpenXml.Interfaces.Fonts; using OfficeOpenXml.Style; using System.Collections.Generic; using System.Drawing; diff --git a/src/EPPlus.Export.Pdf/PdfObjects/PdfContentStream.cs b/src/EPPlus.Export.Pdf/PdfObjects/PdfContentStream.cs index fc7cd0665..f39f965be 100644 --- a/src/EPPlus.Export.Pdf/PdfObjects/PdfContentStream.cs +++ b/src/EPPlus.Export.Pdf/PdfObjects/PdfContentStream.cs @@ -18,6 +18,7 @@ Date Author Change using EPPlus.Graphics; using EPPlus.Graphics.Math; using OfficeOpenXml; +using OfficeOpenXml.Interfaces.Fonts; using OfficeOpenXml.Style; using System; using System.Collections.Generic; diff --git a/src/EPPlus.Export.Pdf/PdfResources/PdfFontResource.cs b/src/EPPlus.Export.Pdf/PdfResources/PdfFontResource.cs index 9d163192f..47c998945 100644 --- a/src/EPPlus.Export.Pdf/PdfResources/PdfFontResource.cs +++ b/src/EPPlus.Export.Pdf/PdfResources/PdfFontResource.cs @@ -17,6 +17,7 @@ Date Author Change using System; using System.Collections.Generic; using EPPlus.Graphics; +using OfficeOpenXml.Interfaces.Fonts; namespace EPPlus.Export.Pdf.PdfResources { @@ -34,7 +35,7 @@ public PdfFontResource(string fontName, FontSubFamily subFamily, int labelNumber : base("F", labelNumber) { this.fontName = fontName; - fontData = OpenTypeFonts.GetFontData(pageSettings.FontDirectories, fontName, subFamily, pageSettings.SearchSystemDirectories); + fontData = OpenTypeFonts.LoadFont(fontName, subFamily, pageSettings.FontDirectories, pageSettings.SearchSystemDirectories); } //Get the Font Descriptor object to write in PDF. diff --git a/src/EPPlus.Export.Pdf/Pdfhelpers/PdfTextData.cs b/src/EPPlus.Export.Pdf/Pdfhelpers/PdfTextData.cs index 84fba3170..cbf89df53 100644 --- a/src/EPPlus.Export.Pdf/Pdfhelpers/PdfTextData.cs +++ b/src/EPPlus.Export.Pdf/Pdfhelpers/PdfTextData.cs @@ -14,6 +14,7 @@ Date Author Change using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Tables.Cmap; using EPPlus.Fonts.OpenType.Tables.Kern; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Linq; @@ -23,7 +24,7 @@ internal static class PdfTextData { internal static OpenTypeFont GetFontData(PdfPageSettings pageSettings, string fontName, FontSubFamily subFamily) { - return OpenTypeFonts.GetFontData(pageSettings.FontDirectories, fontName, subFamily, pageSettings.SearchSystemDirectories); + return OpenTypeFonts.LoadFont(fontName, subFamily, pageSettings.FontDirectories, pageSettings.SearchSystemDirectories); } internal static double MeasureFontHeight(OpenTypeFont font, double fontSize) diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/ExtractCharWidthsBenchmark.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/ExtractCharWidthsBenchmark.cs index 2536f8378..734d92727 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/ExtractCharWidthsBenchmark.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/ExtractCharWidthsBenchmark.cs @@ -1,7 +1,7 @@ using BenchmarkDotNet.Attributes; using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.TextShaping; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; [MemoryDiagnoser] [SimpleJob(warmupCount: 1, iterationCount: 3)] @@ -17,7 +17,7 @@ public class ExtractCharWidthsBenchmark public void Setup() { var fontFolders = new List { /* your font paths */ }; - var font = OpenTypeFonts.GetFontData(fontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri"); _shaper = new TextShaper(font); _options = ShapingOptions.Default; diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheBenchmarks.cs index 9f699d4fb..b51dbe68a 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheBenchmarks.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Attributes; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Linq; @@ -30,14 +31,14 @@ public void Setup() // Pre-load font into cache OpenTypeFonts.ClearFontCache(); - OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Regular); + OpenTypeFonts.LoadFont("Roboto"); } [Benchmark] public OpenTypeFont Load_FromCache_SingleThread() { // This should be extremely fast - just cache lookup - return OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Regular); + return OpenTypeFonts.LoadFont("Roboto"); } [Benchmark] @@ -46,10 +47,10 @@ public OpenTypeFont[] Load_FromCache_MultipleFonts() // Simulates loading multiple font styles (like for a document) return new[] { - OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Regular), - OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Bold), - OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Italic), - OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.BoldItalic) + OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular), + OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Bold), + OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Italic), + OpenTypeFonts.LoadFont("Roboto", FontSubFamily.BoldItalic) }; } } diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheClearingBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheClearingBenchmarks.cs index 7259df995..29622a3ca 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheClearingBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheClearingBenchmarks.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Attributes; using EPPlus.Fonts.OpenType; +using OfficeOpenXml.Interfaces.Fonts; /// /// Benchmarks for repeated cache clearing scenarios @@ -28,10 +29,10 @@ public OpenTypeFont Load_Clear_Load_Pattern() { // Simulates pattern where cache is cleared between operations OpenTypeFonts.ClearFontCache(); - var font1 = OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Regular); + var font1 = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); OpenTypeFonts.ClearFontCache(); - var font2 = OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Regular); + var font2 = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); return font2; } @@ -40,8 +41,8 @@ public OpenTypeFont Load_Clear_Load_Pattern() public OpenTypeFont Load_Reuse_Pattern() { // Simulates pattern where cache is NOT cleared (optimal) - var font1 = OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Regular); - var font2 = OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Regular); + var font1 = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); + var font2 = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); return font2; } diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/FontLoadingBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/FontLoadingBenchmarks.cs index 433208d04..703d995c9 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/FontLoadingBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/FontLoadingBenchmarks.cs @@ -12,6 +12,7 @@ Date Author Change *************************************************************************************************/ using BenchmarkDotNet.Attributes; using EPPlus.Fonts.OpenType; +using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; using System.IO; @@ -40,35 +41,35 @@ public void Setup() public OpenTypeFont Load_Roboto_Regular_ColdCache() { OpenTypeFonts.ClearFontCache(); // Clear INNE i benchmark - return OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Regular); + return OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); } [Benchmark] public OpenTypeFont Load_Roboto_Regular_WarmCache() { // Load UTAN att cleara - använder cache från GlobalSetup eller warmup - return OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Regular); + return OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); } [Benchmark] public OpenTypeFont Load_Roboto_Bold_ColdCache() { OpenTypeFonts.ClearFontCache(); - return OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Bold); + return OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Bold); } [Benchmark] public OpenTypeFont Load_Roboto_Italic_ColdCache() { OpenTypeFonts.ClearFontCache(); - return OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Italic); + return OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Italic); } [Benchmark] public OpenTypeFont Load_Roboto_BoldItalic_ColdCache() { OpenTypeFonts.ClearFontCache(); - return OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.BoldItalic); + return OpenTypeFonts.LoadFont("Roboto", FontSubFamily.BoldItalic); } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs index d1f194085..c6e64408d 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs @@ -15,6 +15,7 @@ Date Author Change using EPPlus.Fonts.OpenType.Integration; using EPPlus.Fonts.OpenType.TextShaping; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.IO; @@ -61,12 +62,7 @@ public void Setup() } Console.WriteLine("\nLoading Roboto Regular..."); - var font = OpenTypeFonts.GetFontData( - _fontFolders, - FontFamily, - FontSubFamily.Regular, - searchSystemDirectories: false - ); + var font = OpenTypeFonts.LoadFont(FontFamily, FontSubFamily.Regular); Console.WriteLine(string.Format("Loaded: {0} {1} ({2} glyphs)", font.FullName, font.SubFamily, font.GlyfTable.Glyphs.Count)); diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/SubsettingBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/SubsettingBenchmarks.cs index 53c50acc5..985233598 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/SubsettingBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/SubsettingBenchmarks.cs @@ -12,6 +12,7 @@ Date Author Change *************************************************************************************************/ using BenchmarkDotNet.Attributes; using EPPlus.Fonts.OpenType; +using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; using System.IO; @@ -35,7 +36,7 @@ public void Setup() } _fontFolders = new List { fontsPath }; - _roboto = OpenTypeFonts.GetFontData(_fontFolders, "Roboto", FontSubFamily.Regular); + _roboto = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); } [Benchmark] diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/TextLayoutEngineBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/TextLayoutEngineBenchmarks.cs index ca4fa4927..cfe1147c5 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/TextLayoutEngineBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/TextLayoutEngineBenchmarks.cs @@ -15,6 +15,7 @@ Date Author Change using EPPlus.Fonts.OpenType.Integration; using EPPlus.Fonts.OpenType.TextShaping; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; namespace EPPlus.Fonts.Benchmarks @@ -59,7 +60,7 @@ public void Setup() var fontFolders = new List { fontsPath }; // Setup new layout engine - var font = OpenTypeFonts.GetFontData(fontFolders, FontFamily, FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont(FontFamily, FontSubFamily.Regular); var shaper = new TextShaper(font); _layoutEngine = new TextLayoutEngine(shaper); @@ -155,7 +156,7 @@ public List New_Wrap_10Paragraphs_Sequential() [Benchmark] public double[] OnlyExtractWidths() { - var font = OpenTypeFonts.GetFontData(null, FontFamily, FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont(FontFamily, FontSubFamily.Regular); var shaper = new TextShaper(font); return shaper.ExtractCharWidths(LoremIpsum20Para, FontSize, ShapingOptions.Default); } diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/TextShapingBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/TextShapingBenchmarks.cs index 8f44c3b83..87ce151d4 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/TextShapingBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/TextShapingBenchmarks.cs @@ -1,6 +1,6 @@ using BenchmarkDotNet.Attributes; using EPPlus.Fonts.OpenType.TextShaping; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; namespace EPPlus.Fonts.OpenType.Benchmarks { @@ -22,7 +22,7 @@ public void Setup() } var fontFolders = new List { fontsPath }; - _roboto = OpenTypeFonts.GetFontData(fontFolders, "Roboto", FontSubFamily.Regular); + _roboto = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); _shaper = new TextShaper(_roboto); } diff --git a/src/EPPlus.Fonts.OpenType.Tests/EPPlus.Fonts.OpenType.Tests.csproj b/src/EPPlus.Fonts.OpenType.Tests/EPPlus.Fonts.OpenType.Tests.csproj index 1813c8ce9..a69a0ca61 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/EPPlus.Fonts.OpenType.Tests.csproj +++ b/src/EPPlus.Fonts.OpenType.Tests/EPPlus.Fonts.OpenType.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 latest @@ -38,6 +38,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/CustomFontProviderTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/CustomFontProviderTests.cs new file mode 100644 index 000000000..b7fd0bc97 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/CustomFontProviderTests.cs @@ -0,0 +1,346 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 02/24/2026 EPPlus Software AB CustomFontProvider unit tests + *************************************************************************************************/ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml.Interfaces.Fonts; +using System; +using System.Linq; + +namespace EPPlus.Fonts.OpenType.Tests.FallbackFonts +{ + [TestClass] + public class CustomFontProviderTests : FontTestBase + { + public override TestContext? TestContext { get; set; } + + private OpenTypeFont _robotoFont; + private OpenTypeFont _notoEmojiFont; + private OpenTypeFont _notoMathFont; + + // U+1F600 = 😀 (Grinning Face) - present in NotoEmoji, absent in Roboto + private const uint EmojiCodePoint = 0x1F600; + // U+0041 = 'A' - present in Roboto (and most text fonts) + private const uint LatinA = 0x0041; + // U+2A0C = ⨌ (Quadruple Integral) - present in Noto Sans Math, absent in Roboto and NotoEmoji + private const uint IntegralCodePoint = 0x2A0C; + // U+F0000 = Private Use Area - unlikely to be in any standard font + private const uint PrivateUseCodePoint = 0xF0000; + + [TestInitialize] + public void TestSetup() + { + _robotoFont = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); + _notoEmojiFont = OpenTypeFonts.LoadFont("Noto Emoji", FontSubFamily.Regular); + _notoMathFont = OpenTypeFonts.LoadFont("Noto Sans Math", FontSubFamily.Regular); + } + + #region Constructor Tests + + [TestMethod] + public void Constructor_WithValidFont_ShouldSetPrimaryFont() + { + // Act + var provider = new CustomFontProvider(_robotoFont); + + // Assert + Assert.AreEqual(_robotoFont, provider.PrimaryFont); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void Constructor_WithNull_ShouldThrowArgumentNullException() + { + // Act + new CustomFontProvider(null); + } + + #endregion + + #region AddFallback Tests + + [TestMethod] + public void AddFallback_WithValidFont_ShouldBeIncludedInGetAllFonts() + { + // Arrange + var provider = new CustomFontProvider(_robotoFont); + + // Act + provider.AddFallback(_notoEmojiFont); + var allFonts = provider.GetAllFonts().ToList(); + + // Assert + Assert.AreEqual(2, allFonts.Count, "Should have primary + 1 fallback"); + Assert.AreEqual(_robotoFont, allFonts[0], "First should be primary font"); + Assert.AreEqual(_notoEmojiFont, allFonts[1], "Second should be fallback font"); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AddFallback_WithNull_ShouldThrowArgumentNullException() + { + // Arrange + var provider = new CustomFontProvider(_robotoFont); + + // Act + provider.AddFallback(null); + } + + [TestMethod] + public void AddFallback_MultipleFonts_ShouldPreserveOrder() + { + // Arrange + var provider = new CustomFontProvider(_robotoFont); + + // Act + provider.AddFallback(_notoEmojiFont); + provider.AddFallback(_robotoFont); // Same font again, to test ordering + var allFonts = provider.GetAllFonts().ToList(); + + // Assert + Assert.AreEqual(3, allFonts.Count, "Should have primary + 2 fallbacks"); + Assert.AreEqual(_robotoFont, allFonts[0], "First should be primary"); + Assert.AreEqual(_notoEmojiFont, allFonts[1], "Second should be first added fallback"); + Assert.AreEqual(_robotoFont, allFonts[2], "Third should be second added fallback"); + } + + #endregion + + #region TryGetGlyphFont - Primary Font Tests + + [TestMethod] + public void TryGetGlyphFont_LatinChar_ShouldUsePrimaryFont() + { + // Arrange + var provider = new CustomFontProvider(_robotoFont); + provider.AddFallback(_notoEmojiFont); + + // Act + bool found = provider.TryGetGlyphFont(LatinA, out var font, out var glyphId); + + // Assert + Assert.IsTrue(found, "Should find glyph for 'A'"); + Assert.AreEqual(_robotoFont, font, "Should use primary font for Latin chars"); + Assert.AreNotEqual((ushort)0, glyphId, "GlyphId should not be .notdef"); + } + + [TestMethod] + public void TryGetGlyphFont_CharInBothFonts_ShouldPreferPrimaryFont() + { + // Arrange - both Roboto and NotoEmoji might have basic chars, + // but primary should always be preferred + var provider = new CustomFontProvider(_robotoFont); + provider.AddFallback(_notoEmojiFont); + + // Act + bool found = provider.TryGetGlyphFont(LatinA, out var font, out var glyphId); + + // Assert + Assert.IsTrue(found); + Assert.AreEqual(_robotoFont, font, "Primary font should always be preferred when it has the glyph"); + } + + #endregion + + #region TryGetGlyphFont - Fallback Tests + + [TestMethod] + public void TryGetGlyphFont_EmojiNotInPrimary_ShouldUseFallbackFont() + { + // Arrange + var provider = new CustomFontProvider(_robotoFont); + provider.AddFallback(_notoEmojiFont); + + // Verify preconditions: Roboto should NOT have the emoji glyph + bool robotoHasEmoji = _robotoFont.CmapTable.TryGetGlyphId(EmojiCodePoint, out _); + Assert.IsFalse(robotoHasEmoji, "Precondition: Roboto should not contain emoji U+1F600"); + + // Verify preconditions: NotoEmoji SHOULD have the emoji glyph + bool notoHasEmoji = _notoEmojiFont.CmapTable.TryGetGlyphId(EmojiCodePoint, out _); + Assert.IsTrue(notoHasEmoji, "Precondition: NotoEmoji should contain emoji U+1F600"); + + // Act + bool found = provider.TryGetGlyphFont(EmojiCodePoint, out var font, out var glyphId); + + // Assert + Assert.IsTrue(found, "Should find emoji glyph in fallback font"); + Assert.AreEqual(_notoEmojiFont, font, "Should return NotoEmoji as the fallback font"); + Assert.AreNotEqual((ushort)0, glyphId, "GlyphId should not be .notdef"); + } + + [TestMethod] + public void TryGetGlyphFont_EmojiWithMultipleFallbacks_ShouldUseFirstMatchingFallback() + { + // Arrange - add Roboto (no emoji) first, then NotoEmoji (has emoji) + var provider = new CustomFontProvider(_robotoFont); + provider.AddFallback(_robotoFont); // First fallback: also no emoji + provider.AddFallback(_notoEmojiFont); // Second fallback: has emoji + + // Act + bool found = provider.TryGetGlyphFont(EmojiCodePoint, out var font, out var glyphId); + + // Assert + Assert.IsTrue(found, "Should find emoji in second fallback"); + Assert.AreEqual(_notoEmojiFont, font, "Should return NotoEmoji (second fallback), not Roboto (first fallback)"); + Assert.AreNotEqual((ushort)0, glyphId); + } + + [TestMethod] + public void TryGetGlyphFont_MathSymbol_ShouldUseThirdFallback() + { + // Arrange - three-level fallback chain: Roboto → NotoEmoji → NotoSansMath + var provider = new CustomFontProvider(_robotoFont); + provider.AddFallback(_notoEmojiFont); + provider.AddFallback(_notoMathFont); + + // Verify preconditions + bool robotoHasMath = _robotoFont.CmapTable.TryGetGlyphId(IntegralCodePoint, out _); + Assert.IsFalse(robotoHasMath, "Precondition: Roboto should not contain ∫ (U+222B)"); + + bool emojiHasMath = _notoEmojiFont.CmapTable.TryGetGlyphId(IntegralCodePoint, out _); + Assert.IsFalse(emojiHasMath, "Precondition: NotoEmoji should not contain ∫ (U+222B)"); + + bool mathHasMath = _notoMathFont.CmapTable.TryGetGlyphId(IntegralCodePoint, out _); + Assert.IsTrue(mathHasMath, "Precondition: Noto Sans Math should contain ∫ (U+222B)"); + + // Act + bool found = provider.TryGetGlyphFont(IntegralCodePoint, out var font, out var glyphId); + + // Assert + Assert.IsTrue(found, "Should find ∫ in third fallback (Noto Sans Math)"); + Assert.AreEqual(_notoMathFont, font, "Should return Noto Sans Math as the resolving font"); + Assert.AreNotEqual((ushort)0, glyphId, "GlyphId should not be .notdef"); + } + + [TestMethod] + public void TryGetGlyphFont_ThreeFallbacks_EachFontResolvesItsOwnDomain() + { + // Arrange - full chain: Roboto → NotoEmoji → NotoSansMath + var provider = new CustomFontProvider(_robotoFont); + provider.AddFallback(_notoEmojiFont); + provider.AddFallback(_notoMathFont); + + // Act & Assert - Latin 'A' should resolve from primary (Roboto) + bool foundLatin = provider.TryGetGlyphFont(LatinA, out var latinFont, out var latinGlyphId); + Assert.IsTrue(foundLatin); + Assert.AreEqual(_robotoFont, latinFont, "Latin 'A' should come from Roboto (primary)"); + Assert.AreNotEqual((ushort)0, latinGlyphId); + + // Act & Assert - Emoji should resolve from first fallback (NotoEmoji) + bool foundEmoji = provider.TryGetGlyphFont(EmojiCodePoint, out var emojiFont, out var emojiGlyphId); + Assert.IsTrue(foundEmoji); + Assert.AreEqual(_notoEmojiFont, emojiFont, "Emoji should come from NotoEmoji (1st fallback)"); + Assert.AreNotEqual((ushort)0, emojiGlyphId); + + // Act & Assert - Math symbol should resolve from second fallback (NotoSansMath) + bool foundMath = provider.TryGetGlyphFont(IntegralCodePoint, out var mathFont, out var mathGlyphId); + Assert.IsTrue(foundMath); + Assert.AreEqual(_notoMathFont, mathFont, "∫ should come from Noto Sans Math (2nd fallback)"); + Assert.AreNotEqual((ushort)0, mathGlyphId); + + // Act & Assert - Unknown code point should fail gracefully + bool foundUnknown = provider.TryGetGlyphFont(PrivateUseCodePoint, out var unknownFont, out var unknownGlyphId); + Assert.IsFalse(foundUnknown); + Assert.AreEqual(_robotoFont, unknownFont, "Not found should return primary font"); + Assert.AreEqual((ushort)0, unknownGlyphId); + } + + #endregion + + #region TryGetGlyphFont - Not Found Tests + + [TestMethod] + public void TryGetGlyphFont_CharNotInAnyFont_ShouldReturnFalseWithPrimaryFont() + { + // Arrange + var provider = new CustomFontProvider(_robotoFont); + provider.AddFallback(_notoEmojiFont); + + // Act + bool found = provider.TryGetGlyphFont(PrivateUseCodePoint, out var font, out var glyphId); + + // Assert + Assert.IsFalse(found, "Should not find glyph in Private Use Area"); + Assert.AreEqual(_robotoFont, font, "Should return primary font even when not found"); + Assert.AreEqual((ushort)0, glyphId, "GlyphId should be .notdef (0)"); + } + + [TestMethod] + public void TryGetGlyphFont_NoFallbacks_CharNotInPrimary_ShouldReturnFalse() + { + // Arrange - no fallbacks added at all + var provider = new CustomFontProvider(_robotoFont); + + // Act + bool found = provider.TryGetGlyphFont(EmojiCodePoint, out var font, out var glyphId); + + // Assert + Assert.IsFalse(found, "Should not find emoji when no fallbacks are configured"); + Assert.AreEqual(_robotoFont, font, "Should still return primary font"); + Assert.AreEqual((ushort)0, glyphId, "Should return .notdef"); + } + + #endregion + + #region GetAllFonts Tests + + [TestMethod] + public void GetAllFonts_NoFallbacks_ShouldReturnOnlyPrimary() + { + // Arrange + var provider = new CustomFontProvider(_robotoFont); + + // Act + var allFonts = provider.GetAllFonts().ToList(); + + // Assert + Assert.AreEqual(1, allFonts.Count, "Should only contain primary font"); + Assert.AreEqual(_robotoFont, allFonts[0]); + } + + [TestMethod] + public void GetAllFonts_WithFallbacks_ShouldReturnPrimaryFirstThenFallbacks() + { + // Arrange + var provider = new CustomFontProvider(_robotoFont); + provider.AddFallback(_notoEmojiFont); + + // Act + var allFonts = provider.GetAllFonts().ToList(); + + // Assert + Assert.AreEqual(2, allFonts.Count); + Assert.AreEqual(_robotoFont, allFonts[0], "Primary font should be first"); + Assert.AreEqual(_notoEmojiFont, allFonts[1], "Fallback should come after primary"); + } + + [TestMethod] + public void GetAllFonts_CalledMultipleTimes_ShouldReturnConsistentResults() + { + // Arrange + var provider = new CustomFontProvider(_robotoFont); + provider.AddFallback(_notoEmojiFont); + + // Act + var firstCall = provider.GetAllFonts().ToList(); + var secondCall = provider.GetAllFonts().ToList(); + + // Assert + Assert.AreEqual(firstCall.Count, secondCall.Count); + for (int i = 0; i < firstCall.Count; i++) + { + Assert.AreEqual(firstCall[i], secondCall[i], $"Font at index {i} should be the same across calls"); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/DefaultPrimaryFontTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/DefaultPrimaryFontTests.cs new file mode 100644 index 000000000..e9a3b0029 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/DefaultPrimaryFontTests.cs @@ -0,0 +1,83 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 02/27/2026 EPPlus Software AB Initial implementation + *************************************************************************************************/ +using EPPlus.Fonts.OpenType.FontResolver; +using OfficeOpenXml.Interfaces.Fonts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.Tests.FallbackFonts +{ + [TestClass] + public class DefaultPrimaryFontTests : FontTestBase + { + public override TestContext? TestContext { get; set; } + + private static readonly string UnknownFont = "ThisFontDoesNotExist_XYZ"; + + private static DefaultFontResolver CreateIsolatedResolver() + { + // No directories, no system fonts — guarantees Archivo Narrow fallback + return new DefaultFontResolver(fontDirectories: null, searchSystemDirectories: false); + } + + [TestMethod] + public void ResolveFont_UnknownFont_Regular_ShouldFallbackToArchivoNarrow() + { + var resolver = CreateIsolatedResolver(); + var bytes = resolver.ResolveFont(UnknownFont, FontSubFamily.Regular); + + Assert.IsNotNull(bytes, "Should fall back to Archivo Narrow, not return null"); + var font = OpenTypeFonts.GetFromBytes(bytes); + Assert.AreEqual("Archivo Narrow", font.NameTable.GetFamilyName()); + Assert.AreEqual(FontSubFamily.Regular, font.NameTable.GetSubfamilyEnum()); + } + + [TestMethod] + public void ResolveFont_UnknownFont_Bold_ShouldFallbackToArchivoNarrowBold() + { + var resolver = CreateIsolatedResolver(); + var bytes = resolver.ResolveFont(UnknownFont, FontSubFamily.Bold); + + Assert.IsNotNull(bytes, "Should fall back to Archivo Narrow Bold, not return null"); + var font = OpenTypeFonts.GetFromBytes(bytes); + Assert.AreEqual("Archivo Narrow", font.NameTable.GetFamilyName()); + Assert.AreEqual(FontSubFamily.Bold, font.NameTable.GetSubfamilyEnum()); + } + + [TestMethod] + public void ResolveFont_UnknownFont_Italic_ShouldFallbackToArchivoNarrowItalic() + { + var resolver = CreateIsolatedResolver(); + var bytes = resolver.ResolveFont(UnknownFont, FontSubFamily.Italic); + + Assert.IsNotNull(bytes, "Should fall back to Archivo Narrow Italic, not return null"); + var font = OpenTypeFonts.GetFromBytes(bytes); + Assert.AreEqual("Archivo Narrow", font.NameTable.GetFamilyName()); + Assert.AreEqual(FontSubFamily.Italic, font.NameTable.GetSubfamilyEnum()); + } + + [TestMethod] + public void ResolveFont_UnknownFont_BoldItalic_ShouldFallbackToArchivoNarrowBoldItalic() + { + var resolver = CreateIsolatedResolver(); + var bytes = resolver.ResolveFont(UnknownFont, FontSubFamily.BoldItalic); + + Assert.IsNotNull(bytes, "Should fall back to Archivo Narrow Bold Italic, not return null"); + var font = OpenTypeFonts.GetFromBytes(bytes); + Assert.AreEqual("Archivo Narrow", font.NameTable.GetFamilyName()); + Assert.AreEqual(FontSubFamily.BoldItalic, font.NameTable.GetSubfamilyEnum()); + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs index bf78eb66a..61c22b861 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs @@ -4,6 +4,7 @@ Font Provider Unit Tests *************************************************************************************************/ using EPPlus.Fonts.OpenType.TextShaping; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml.Interfaces.Fonts; using System.Linq; namespace EPPlus.Fonts.OpenType.Tests.FallbackFonts @@ -18,7 +19,7 @@ public class FontProviderTests : FontTestBase [TestInitialize] public void TestSetup() { - _robotoFont = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + _robotoFont = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); } [TestMethod] diff --git a/src/EPPlus.Fonts.OpenType.Tests/FontMeasurerPerformanceTest.cs b/src/EPPlus.Fonts.OpenType.Tests/FontMeasurerPerformanceTest.cs index 0bd23b175..acd5673d3 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/FontMeasurerPerformanceTest.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/FontMeasurerPerformanceTest.cs @@ -2,6 +2,7 @@ using EPPlus.Fonts.OpenType.Integration; using EPPlus.Fonts.OpenType.TextShaping; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Diagnostics; using System.Drawing; @@ -105,7 +106,7 @@ public void Wrap20Paragraphs100TimesMultipleTextFragments() public void QuickPeakMemoryTest() { var fontFolders = new List { /* your paths */ }; - var font = OpenTypeFonts.GetFontData(fontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layoutEngine = new TextLayoutEngine(shaper); @@ -145,7 +146,7 @@ public void QuickPeakMemoryTest() public void QuickPeakMemoryTest2() { var fontFolders = new List { /* your paths */ }; - var font = OpenTypeFonts.GetFontData(fontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layoutEngine = new TextLayoutEngine(shaper); diff --git a/src/EPPlus.Fonts.OpenType.Tests/FontSubsetManagerTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FontSubsetManagerTests.cs new file mode 100644 index 000000000..2bb0efe0d --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/FontSubsetManagerTests.cs @@ -0,0 +1,133 @@ +using EPPlus.Fonts.OpenType; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Linq; + +namespace EPPlus.Fonts.OpenType.Tests +{ + [TestClass] + public class FontSubsetManagerTests : FontTestBase + { + public override TestContext? TestContext { get; set; } + + // Helper: Load a real font for testing + private OpenTypeFont LoadTestFont() + { + // Adjust path to a font available in your test environment + return OpenTypeFonts.LoadFont("Roboto"); + } + + [TestMethod] + public void CreateSubsettedProvider_WithAsciiText_ReturnsSubsettedPrimaryFont() + { + // Arrange + var font = LoadTestFont(); + var manager = new FontSubsetManager(font); + + // Act + manager.AddText("Hello World"); + var provider = manager.CreateSubsettedProvider(); + + // Assert - The subset should be a different (smaller) font instance + var subsetFont = provider.PrimaryFont; + Assert.IsNotNull(subsetFont); + Assert.IsTrue(subsetFont.IsSubset, "Primary font should be subsetted"); + + // Verify the subset contains the glyphs we need + foreach (char c in "Hello World") + { + ushort glyphId; + Assert.IsTrue( + subsetFont.CmapTable.TryGetGlyphId(c, out glyphId), + $"Subset should contain glyph for '{c}'"); + Assert.AreNotEqual((ushort)0, glyphId, $"Glyph for '{c}' should not be .notdef"); + } + } + + [TestMethod] + public void CreateSubsettedProvider_WithEmoji_SubsetsFallbackFont() + { + // Arrange + var font = LoadTestFont(); + var provider = new DefaultFontProvider(font); + var manager = new FontSubsetManager(provider); + + // Act - Add text with emoji (U+1F600 = 😀, handled by Noto Emoji fallback) + manager.AddText("Hello 😀"); + var subsettedProvider = manager.CreateSubsettedProvider(); + + // Assert - Should have primary + at least one fallback + var allFonts = subsettedProvider.GetAllFonts().ToList(); + Assert.IsTrue(allFonts.Count >= 2, + "Should have primary font + emoji fallback font"); + + // The fallback font should also be subsetted + var fallbackFont = allFonts[1]; + Assert.IsTrue(fallbackFont.IsSubset, + "Fallback (emoji) font should be subsetted"); + + // The subsetted emoji font should be much smaller than the original + var serialized = fallbackFont.Serialize(); + Assert.IsTrue(serialized.Length < 100 * 1024, + $"Subsetted emoji font should be small, was {serialized.Length / 1024} KB"); + } + + [TestMethod] + public void CreateSubsettedProvider_WithMultipleAddTextCalls_CollectsAllCodePoints() + { + // Arrange + var font = LoadTestFont(); + var manager = new FontSubsetManager(font); + + // Act - Add text in multiple calls (simulates scanning multiple cells) + manager.AddText("ABC"); + manager.AddText("DEF"); + manager.AddText("ADF"); // Overlapping characters + var provider = manager.CreateSubsettedProvider(); + + // Assert - All characters from all calls should be present + var subsetFont = provider.PrimaryFont; + foreach (char c in "ABCDEF") + { + ushort glyphId; + Assert.IsTrue( + subsetFont.CmapTable.TryGetGlyphId(c, out glyphId), + $"Subset should contain glyph for '{c}'"); + } + } + + [TestMethod] + public void CreateSubsettedProvider_UnusedFallbackFontsAreExcluded() + { + // Arrange - DefaultFontProvider has Noto Emoji + Noto Math as fallbacks + var font = LoadTestFont(); + var provider = new DefaultFontProvider(font); + var manager = new FontSubsetManager(provider); + + // Act - Only ASCII text, no emoji or math symbols + manager.AddText("Plain text only"); + var subsettedProvider = manager.CreateSubsettedProvider(); + + // Assert - Should only have the primary font (no fallbacks needed) + var allFonts = subsettedProvider.GetAllFonts().ToList(); + Assert.AreEqual(1, allFonts.Count, + "Only primary font should be included when no fallback glyphs are used"); + } + + [TestMethod] + public void AddText_WithNullOrEmpty_DoesNotThrow() + { + // Arrange + var font = LoadTestFont(); + var manager = new FontSubsetManager(font); + + // Act & Assert - Should handle gracefully + manager.AddText(null); + manager.AddText(""); + manager.AddText("A"); // Then add real text + + var provider = manager.CreateSubsettedProvider(); + Assert.IsNotNull(provider.PrimaryFont); + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/FontTestBase.cs b/src/EPPlus.Fonts.OpenType.Tests/FontTestBase.cs index 2faf2b88b..1c5e0bd1d 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/FontTestBase.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/FontTestBase.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 12/21/2025 EPPlus Software AB Test base class *************************************************************************************************/ +using EPPlus.Fonts.OpenType.FontResolver; using EPPlus.Fonts.OpenType.Tests.Helpers; namespace EPPlus.Fonts.OpenType.Tests @@ -112,7 +113,13 @@ protected static void DeleteOutputFont(string fileName) [TestInitialize] public void ClearAllCaches() { - //OpenTypeFonts.ClearFontCache(); + OpenTypeFonts.ClearFontCache(); + ConfigureResolver(); + } + + protected virtual void ConfigureResolver() + { + OpenTypeFonts.Configure(x => x.SetFontResolver(new DefaultFontResolver(FontFolders, false))); } [ClassInitialize(InheritanceBehavior.BeforeEachDerivedClass)] @@ -120,5 +127,19 @@ public static void BaseClassInitialize(TestContext context) { FontDirectoriesTestHelper.ClassInitialize(context); } + + /// + /// Temporarily configures the font resolver for the duration of one test. + /// Restores the default test resolver automatically via [TestCleanup]. + /// + protected static void UseSystemFonts() + { + OpenTypeFonts.Configure(x => x.SetFontResolver(new DefaultFontResolver(null, true))); + } + + protected static void UseFontFolders(IEnumerable directories, bool searchSystemDirectories = false) + { + OpenTypeFonts.Configure(x => x.SetFontResolver(new DefaultFontResolver(directories, searchSystemDirectories))); + } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/Fonts/NotoSansMath-Regular.ttf b/src/EPPlus.Fonts.OpenType.Tests/Fonts/NotoSansMath-Regular.ttf new file mode 100644 index 000000000..7062ca131 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType.Tests/Fonts/NotoSansMath-Regular.ttf differ diff --git a/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontTestHelper.cs b/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontTestHelper.cs index aa84e0d5d..650b81516 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontTestHelper.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontTestHelper.cs @@ -1,5 +1,6 @@ using EPPlus.Fonts.OpenType.FontValidation; using EPPlus.Fonts.OpenType.Tables.Gsub.Data.Lookups; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.IO; @@ -85,7 +86,7 @@ public static byte[] SubsetAndSerialize( string text, List fontFolders) { - var font = OpenTypeFonts.GetFontData(fontFolders, fontName, FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(text); return subset.Serialize(); } @@ -98,7 +99,7 @@ public static byte[] SubsetAndSerialize( char[] chars, List fontFolders) { - var font = OpenTypeFonts.GetFontData(fontFolders, fontName, FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(chars); return subset.Serialize(); } @@ -115,13 +116,11 @@ public static OpenTypeFont RoundtripSubset( string text, List fontFolders) { - var font = OpenTypeFonts.GetFontData(fontFolders, fontName, FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(text); var bytes = subset.Serialize(); - var parsed = new OpenTypeFont( - bytes, - font.Format); + var parsed = new OpenTypeFont(bytes); AssertFontValid(parsed); @@ -136,13 +135,11 @@ public static OpenTypeFont RoundtripSubset( char[] chars, List fontFolders) { - var font = OpenTypeFonts.GetFontData(fontFolders, fontName, FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(chars); var bytes = subset.Serialize(); - var parsed = new OpenTypeFont( - bytes, - font.Format); + var parsed = new OpenTypeFont(bytes); AssertFontValid(parsed); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs index afcb93bdb..9e20a788a 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs @@ -4,6 +4,7 @@ using EPPlus.Fonts.OpenType.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Diagnostics; @@ -23,7 +24,7 @@ public class MeasurerComparisonTests : FontTestBase public void Compare_MeasureSimpleText_ShouldBeClose() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); // Old measurer var oldMeasurer = new FontMeasurerTrueType(11f, "Roboto", FontSubFamily.Regular); @@ -59,7 +60,7 @@ public void Compare_MeasureSimpleText_ShouldBeClose() public void Compare_MeasureTextWithKerning_ShouldBeClose() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var oldMeasurer = new FontMeasurerTrueType(11f, "Roboto", FontSubFamily.Regular); @@ -88,7 +89,7 @@ public void Compare_MeasureTextWithKerning_ShouldBeClose() public void Compare_MeasureMultiLineText_NewImplementationFixesBugs() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var oldMeasurer = new FontMeasurerTrueType(11f, "Roboto", FontSubFamily.Regular); oldMeasurer.MeasureWrappedTextCells = true; @@ -157,7 +158,7 @@ public void Compare_MeasureMultiLineText_NewImplementationFixesBugs() public void Compare_GetSingleLineSpacing_ShouldMatch() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var oldMeasurer = new FontMeasurerTrueType(11f, "Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); @@ -178,7 +179,7 @@ public void Compare_GetSingleLineSpacing_ShouldMatch() public void Compare_GetBaseLine_ShouldMatch() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var oldMeasurer = new FontMeasurerTrueType(11f, "Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); @@ -203,7 +204,7 @@ public void Compare_GetBaseLine_ShouldMatch() public void Compare_WrapSimpleText_ShouldGiveSameLines() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var oldMeasurer = new FontMeasurerTrueType(11f, "Roboto", FontSubFamily.Regular); @@ -246,7 +247,7 @@ public void Compare_WrapSimpleText_ShouldGiveSameLines() public void Compare_WrapTextWithPreExistingWidth_ShouldGiveSameLines() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var oldMeasurer = new FontMeasurerTrueType(11f, "Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); @@ -287,7 +288,7 @@ public void Compare_WrapTextWithPreExistingWidth_ShouldGiveSameLines() public void Compare_WrapRichText_ShouldGiveSimilarLines() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var oldMeasurer = new FontMeasurerTrueType(11f, "Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); @@ -343,7 +344,7 @@ public void Compare_WrapRichText_ShouldGiveSimilarLines() public void Compare_EmptyString_BothShouldReturnZero() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var oldMeasurer = new FontMeasurerTrueType(11f, "Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); @@ -371,7 +372,7 @@ public void Compare_EmptyString_BothShouldReturnZero() public void Compare_SingleCharacter_ShouldMatch() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var oldMeasurer = new FontMeasurerTrueType(11f, "Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 76c3fc3a1..cdad64a43 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -4,6 +4,7 @@ using EPPlus.Fonts.OpenType.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; using System.Diagnostics; using static System.Net.Mime.MediaTypeNames; @@ -21,7 +22,7 @@ public class TextLayoutEngineTests : FontTestBase public void WrapText_ShortText_NoWrapping() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper, FontFolders); @@ -37,7 +38,7 @@ public void WrapText_ShortText_NoWrapping() public void WrapText_LongText_WrapsAtSpaces() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -58,7 +59,7 @@ public void WrapText_LongText_WrapsAtSpaces() public void WrapText_WithLineBreaks_PreservesBreaks() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -75,7 +76,7 @@ public void WrapText_WithLineBreaks_PreservesBreaks() [TestMethod] public void WrapText_TestWhenOnExactWrapPlusSpaces2() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders); //'sit amet non' is EXACTLY 72 pixels (54 points) in excel at 100% size/display //So an added space should push 'non' over the edge to the next line var text = "sit amet non lacus."; @@ -98,7 +99,7 @@ public void WrapText_TestWhenOnExactWrapPlusSpaces2() [TestMethod] public void WrapText_TestWhenOnExactWrap() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders); //'sit amet non' is EXACTLY 72 pixels (54 points) in excel at 100% size/display var text = "nulla efficitur commodo sit amet non lacus. Proin viverra enim"; var comparison = new List() { "nulla", "efficitur", "commodo", "sit amet non", "lacus. Proin", "viverra enim" }; @@ -118,7 +119,7 @@ public void WrapText_TestWhenOnExactWrap() [TestMethod] public void WrapText_TestFragments() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders); var Lorem20Str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla pulvinar interdum imperdiet. Praesent ut auctor urna. Phasellus sollicitudin quam vitae est convallis, eu mattis lorem efficitur. Mauris nulla libero, tincidunt id ipsum non, lobortis tristique mauris. Donec ut enim sed enim fermentum molestie vel quis odio. Morbi a fermentum massa, sit amet ultrices est. Aenean ante mi, fermentum nec rhoncus et, vulputate vel sapien. Donec tempus, leo quis luctus rhoncus, augue odio pharetra libero, ac blandit urna turpis sed diam. Vivamus augue purus, eleifend et justo facilisis, imperdiet rhoncus sem. Quisque accumsan pellentesque elit, eget finibus massa accumsan in. Fusce eu accumsan enim. Cras pulvinar enim vel tellus lacinia, consectetur euismod tortor consectetur. Praesent tincidunt pretium eros, ac auctor magna luctus sed. Ut porta lectus quam, non ornare mauris lacinia sit amet. Nullam egestas dolor quis magna porttitor, ac iaculis nisi hendrerit. Proin at mollis lacus, in porttitor nunc. Aliquam erat volutpat. Sed vel egestas risus, at aliquam arcu. Vestibulum quis lobortis nulla. Etiam pellentesque auctor nulla, eget tincidunt felis rhoncus id. Sed metus ante, efficitur id dui eu, fermentum mollis odio. Phasellus ullamcorper iaculis augue vel consequat. Etiam fringilla euismod interdum. Ut molestie massa id fringilla lobortis. Vestibulum malesuada, ante vel mattis ultrices, sem ante molestie augue, non tristique dui mi non nibh. Maecenas dictum, sem eget convallis rhoncus, lacus enim porta neque, in posuere dui ex a sapien. Nam lacus nibh, posuere sed elit eget, condimentum facilisis ligula. Cras consectetur lacus ullamcorper velit aliquet bibendum eget vel nulla. Aenean varius ac erat quis ullamcorper. Donec laoreet arcu a lorem volutpat faucibus. Vivamus vehicula leo ut erat luctus scelerisque. Morbi posuere ex et magna egestas facilisis. Fusce scelerisque volutpat erat bibendum hendrerit. Nam blandit mi ut metus pulvinar, vel tempus lacus euismod. Quisque imperdiet sit amet sapien sed ultricies. Phasellus sodales, ipsum vitae tincidunt facilisis, nulla ligula faucibus felis, eget vehicula ante lacus eu lorem. Integer congue diam ac viverra tristique. Curabitur tristique dolor quis quam pretium, et scelerisque quam dictum. Maecenas vitae sodales ligula. Pellentesque maximus diam vel porta convallis. Ut aliquam eros quis porta pellentesque. Fusce in ex ut mi egestas cursus. Aliquam erat volutpat. Cras laoreet condimentum laoreet. Sed eget facilisis tellus. Morbi viverra odio sed odio placerat mollis. Duis turpis metus, dignissim varius urna quis, viverra dignissim dui. Vivamus viverra at nisi quis convallis. Suspendisse fringilla risus et ante sollicitudin, sed eleifend sem placerat. Proin pretium blandit arcu, eget rhoncus risus hendrerit at. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus vulputate efficitur maximus. Cras blandit nulla eu nisi auctor tempus. Sed pretium lacus ac magna vestibulum, aliquam faucibus orci luctus. Mauris enim lorem, varius ut ante quis, varius viverra lectus. Fusce blandit nibh vel feugiat efficitur. Donec maximus id justo ac mollis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla placerat lectus et purus dictum, id congue nisi euismod. Maecenas euismod fermentum diam, sit amet gravida magna suscipit a. Quisque consectetur arcu eu nunc sodales scelerisque. Nulla non tincidunt nulla. Pellentesque ut tortor vel enim convallis malesuada. Aliquam ultricies bibendum ultrices. Mauris rutrum ac nisl vel luctus. Donec quis nibh vitae orci ultricies gravida. Aliquam vitae velit porttitor lorem bibendum fringilla volutpat a eros. Curabitur at commodo tortor. Etiam ultricies, neque et iaculis euismod, diam ligula luctus mi, vitae lobortis felis lorem eu nulla. Sed a semper ex. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla mauris elit, pulvinar ac tortor et, luctus hendrerit nisl. In egestas auctor urna vitae laoreet. Praesent bibendum egestas convallis. Proin non suscipit tellus. Nullam at nibh in urna laoreet sodales non vel tellus. Donec in enim dui. Phasellus quis quam tincidunt, pellentesque lorem ac, scelerisque neque. Integer nec tempus urna. Donec elit massa, eleifend eu sapien sit amet, mollis pellentesque est. Nullam tristique tellus iaculis arcu consectetur pretium. Sed venenatis convallis scelerisque. Suspendisse varius urna sit amet purus accumsan, id ultricies erat efficitur. Cras non ipsum eget nulla efficitur commodo sit amet non lacus. Proin viverra enim sit amet enim tempus ullamcorper. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis ac massa interdum, gravida ex egestas, finibus purus. Nunc consectetur commodo lacus, ac convallis quam lobortis eu. Sed convallis tempor commodo. Nulla sed convallis mauris. Donec venenatis nisi est, ac ullamcorper mi pretium quis. Donec vitae eros at ipsum interdum scelerisque nec vitae nisi. Sed vestibulum erat ac bibendum dapibus. Morbi nec elit id quam tristique cursus id sed sem. Praesent non ante enim. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent non mauris dui. Aliquam rhoncus mattis ante sed venenatis. Vivamus vehicula sed sapien sed dictum. In aliquet, urna efficitur tincidunt lobortis, nibh justo tristique purus, sed volutpat risus magna et libero.Suspendisse lectus justo, varius eget arcu et, semper laoreet erat. Quisque eget lacus ornare, pellentesque erat sit amet, vulputate felis. Duis luctus, massa a pellentesque mollis, massa elit convallis mi, vel bibendum ex ex eu purus. Suspendisse vel fermentum urna, ac commodo enim. Mauris tincidunt cursus elit, a volutpat libero commodo et. Etiam dapibus libero venenatis tellus lobortis, vel lacinia elit faucibus. Maecenas semper sed quam quis finibus. Integer efficitur, libero imperdiet sollicitudin commodo, elit arcu vulputate est, eget finibus mi urna sit amet magna. Cras ullamcorper consequat ornare. Fusce convallis nunc vel risus cursus, at maximus ligula cursus. Pellentesque vulputate risus libero, eget cursus nibh sodales sed. Donec accumsan sem et massa semper, id dignissim velit vehicula.Cras cursus ipsum ac erat vehicula, nec iaculis purus dictum. Quisque lacinia elit vitae leo dictum, vel dignissim velit dapibus. Aenean sem nisi, faucibus interdum justo eu, euismod porttitor ex. Morbi et lectus lectus. Duis neque felis, suscipit at scelerisque eu, scelerisque id orci. Curabitur et placerat ipsum. Proin gravida sapien nisl, et varius ipsum mollis nec. Quisque dignissim consectetur feugiat. Aenean eros purus, laoreet interdum rutrum at, aliquet sit amet lectus. Donec gravida lorem ut tincidunt laoreet. Donec consequat viverra ligula, in accumsan mi bibendum scelerisque. Quisque ac risus justo. Morbi magna arcu, egestas nec luctus commodo, cursus eget nunc. Vivamus euismod lorem ex, et maximus felis hendrerit eget. Nullam ullamcorper euismod ligula, et iaculis ligula ultricies a. Fusce aliquam, enim vel fermentum ultrices, elit quam semper erat, vitae semper velit augue non magna.Quisque maximus semper arcu, id pellentesque est tempus a. Phasellus lacus elit, auctor sit amet lacinia a, dapibus vitae velit. Phasellus ut pharetra justo, ut ultricies erat. Sed molestie sapien vel interdum lobortis. Nulla facilisi. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla nec mauris quis nisi vulputate gravida quis nec velit.Nam et congue ipsum. Nulla vel elit non dolor mollis aliquet vel at magna. Pellentesque nec facilisis elit. In vulputate quis sem porta suscipit. Nullam sed ex ornare nibh suscipit mattis quis non lacus. Mauris vel ex urna. Vivamus ultricies sapien sit amet sapien vehicula gravida. Donec feugiat volutpat quam. Vestibulum auctor dictum nisl, id hendrerit metus ullamcorper sed. Nulla maximus lacus vel mollis maximus. Nulla laoreet placerat quam eu viverra. Etiam feugiat accumsan nisl a condimentum. Sed ultricies ante ante, ac auctor ligula gravida nec. Praesent a neque dignissim, sagittis felis sit amet, condimentum turpis. Fusce at leo vel est blandit malesuada. Pellentesque et neque non metus pellentesque imperdiet. Praesent pellentesque lacinia lorem, et tristique tellus efficitur id. Suspendisse aliquet ultricies justo vitae interdum. Cras tristique viverra quam, eget gravida mi fermentum imperdiet. Sed imperdiet vitae purus ut volutpat. Nulla lacinia elit in fermentum consectetur. Phasellus commodo ut nisl sit amet sagittis. Duis ac ornare orci. Vivamus vel enim posuere, pharetra ex vel, elementum est. Vestibulum commodo luctus metus eget maximus. Suspendisse a nulla a odio eleifend faucibus. Suspendisse semper lacus non porttitor aliquet. Cras ac scelerisque magna, et pulvinar justo. Integer cursus pulvinar fringilla. Mauris imperdiet nibh sit amet tempor laoreet. Morbi tincidunt tortor ex, sit amet maximus purus tristique quis. Quisque sed hendrerit velit. Mauris mattis nibh ut eros luctus, eget mattis massa auctor. Phasellus eu neque at augue gravida sagittis nec non tortor. Etiam porttitor sem sodales mi ullamcorper gravida. In in dictum orci. In vitae vestibulum quam. Cras augue eros, tincidunt ac elit posuere, sollicitudin efficitur lectus. Praesent quis sodales nisl. Proin sit amet molestie est. In commodo mauris vel mauris efficitur, nec mollis mauris sagittis. Cras ligula nibh, egestas sit amet eros in, lacinia tristique magna. Cras risus libero, lacinia eget libero vitae, maximus aliquet nibh. Mauris id sodales purus, vitae dictum lectus. Cras consectetur ligula velit, tempus pulvinar lacus porttitor vitae. Phasellus eget tellus ipsum. Donec interdum laoreet elit non vestibulum. Cras sed urna ullamcorper, aliquam erat eget, porta orci. Vestibulum eget congue nulla. Sed sem tortor, euismod at rutrum id, sagittis a nunc. Duis in nibh facilisis, dignissim purus ut, hendrerit magna. Sed semper ligula id massa elementum, non malesuada velit egestas. Nullam dictum, mi nec euismod sagittis, ligula leo ullamcorper dolor, quis faucibus odio metus eget magna. Ut gravida metus non metus bibendum bibendum. In sagittis eleifend aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nam mollis sagittis felis, in faucibus tortor pretium vel. Nam nec enim metus. Donec in augue arcu. Proin non lobortis purus, sit amet lacinia elit. Suspendisse quis eros condimentum, blandit justo sit amet, lobortis nisl. Suspendisse maximus massa sed urna tempor ornare. Nunc malesuada purus odio, eu luctus lectus auctor nec. Morbi auctor pellentesque auctor. Sed ullamcorper, ex vitae aliquam vulputate, est diam feugiat mi, id porttitor lectus orci ac leo. Donec sit amet velit pulvinar, venenatis turpis ut, interdum ligula. Interdum et malesuada fames ac ante ipsum primis in faucibus. Vestibulum eu lacus urna. Maecenas sem nulla, accumsan eu ultricies sed, tempor vel magna. Cras aliquet sollicitudin sapien ac pulvinar. Praesent ac sodales mi. Integer vitae mauris massa. Maecenas iaculis orci et faucibus interdum. Nunc nec maximus felis, sed finibus quam. Pellentesque felis massa, vestibulum in tellus vitae, congue tincidunt justo. Nunc vitae enim malesuada, bibendum ante nec, varius tellus. Praesent vitae nisi id quam auctor lacinia at non quam. Nam nec ligula sit amet felis auctor sagittis. Nunc in risus eu urna varius laoreet quis sit amet felis. Morbi varius tempor orci, eu vestibulum nunc vestibulum ac. Nunc vehicula velit eleifend consequat porta. Suspendisse maximus dapibus orci, in vulputate massa pretium ac. Quisque malesuada aliquet aliquet."; const string SavedComparisonString = "Lorem\r\nipsum dolor\r\nsit amet,\r\nconsectetur\r\nadipiscing\r\nelit. Nulla\r\npulvinar\r\ninterdum\r\nimperdiet.\r\nPraesent ut\r\nauctor urna.\r\nPhasellus\r\nsollicitudin\r\nquam vitae\r\nest\r\nconvallis,\r\neu mattis\r\nlorem\r\nefficitur.\r\nMauris nulla\r\nlibero,\r\ntincidunt id\r\nipsum non,\r\nlobortis\r\ntristique\r\nmauris.\r\nDonec ut\r\nenim sed\r\nenim\r\nfermentum\r\nmolestie vel\r\nquis odio.\r\nMorbi a\r\nfermentum\r\nmassa, sit\r\namet\r\nultrices est.\r\nAenean\r\nante mi,\r\nfermentum\r\nnec\r\nrhoncus et,\r\nvulputate\r\nvel sapien.\r\nDonec\r\ntempus, leo\r\nquis luctus\r\nrhoncus,\r\naugue odio\r\npharetra\r\nlibero, ac\r\nblandit urna\r\nturpis sed\r\ndiam.\r\nVivamus\r\naugue\r\npurus,\r\neleifend et\r\njusto\r\nfacilisis,\r\nimperdiet\r\nrhoncus\r\nsem.\r\nQuisque\r\naccumsan\r\npellentesqu\r\ne elit, eget\r\nfinibus\r\nmassa\r\naccumsan\r\nin. Fusce eu\r\naccumsan\r\nenim. Cras\r\npulvinar\r\nenim vel\r\ntellus\r\nlacinia,\r\nconsectetur\r\neuismod\r\ntortor\r\nconsectetur\r\n. Praesent\r\ntincidunt\r\npretium\r\neros, ac\r\nauctor\r\nmagna\r\nluctus sed.\r\nUt porta\r\nlectus\r\nquam, non\r\nornare\r\nmauris\r\nlacinia sit\r\namet.\r\nNullam\r\negestas\r\ndolor quis\r\nmagna\r\nporttitor, ac\r\niaculis nisi\r\nhendrerit.\r\nProin at\r\nmollis\r\nlacus, in\r\nporttitor\r\nnunc.\r\nAliquam\r\nerat\r\nvolutpat.\r\nSed vel\r\negestas\r\nrisus, at\r\naliquam\r\narcu.\r\nVestibulum\r\nquis\r\nlobortis\r\nnulla. Etiam\r\npellentesqu\r\ne auctor\r\nnulla, eget\r\ntincidunt\r\nfelis\r\nrhoncus id.\r\nSed metus\r\nante,\r\nefficitur id\r\ndui eu,\r\nfermentum\r\nmollis odio.\r\nPhasellus\r\nullamcorper\r\niaculis\r\naugue vel\r\nconsequat.\r\nEtiam\r\nfringilla\r\neuismod\r\ninterdum.\r\nUt molestie\r\nmassa id\r\nfringilla\r\nlobortis.\r\nVestibulum\r\nmalesuada,\r\nante vel\r\nmattis\r\nultrices,\r\nsem ante\r\nmolestie\r\naugue, non\r\ntristique dui\r\nmi non\r\nnibh.\r\nMaecenas\r\ndictum,\r\nsem eget\r\nconvallis\r\nrhoncus,\r\nlacus enim\r\nporta\r\nneque, in\r\nposuere dui\r\nex a sapien.\r\nNam lacus\r\nnibh,\r\nposuere sed\r\nelit eget,\r\ncondimentu\r\nm facilisis\r\nligula. Cras\r\nconsectetur\r\nlacus\r\nullamcorper\r\nvelit aliquet\r\nbibendum\r\neget vel\r\nnulla.\r\nAenean\r\nvarius ac\r\nerat quis\r\nullamcorper\r\n. Donec\r\nlaoreet arcu\r\na lorem\r\nvolutpat\r\nfaucibus.\r\nVivamus\r\nvehicula leo\r\nut erat\r\nluctus\r\nscelerisque.\r\nMorbi\r\nposuere ex\r\net magna\r\negestas\r\nfacilisis.\r\nFusce\r\nscelerisque\r\nvolutpat\r\nerat\r\nbibendum\r\nhendrerit.\r\nNam blandit\r\nmi ut metus\r\npulvinar, vel\r\ntempus\r\nlacus\r\neuismod.\r\nQuisque\r\nimperdiet\r\nsit amet\r\nsapien sed\r\nultricies.\r\nPhasellus\r\nsodales,\r\nipsum vitae\r\ntincidunt\r\nfacilisis,\r\nnulla ligula\r\nfaucibus\r\nfelis, eget\r\nvehicula\r\nante lacus\r\neu lorem.\r\nInteger\r\ncongue\r\ndiam ac\r\nviverra\r\ntristique.\r\nCurabitur\r\ntristique\r\ndolor quis\r\nquam\r\npretium, et\r\nscelerisque\r\nquam\r\ndictum.\r\nMaecenas\r\nvitae\r\nsodales\r\nligula.\r\nPellentesqu\r\ne maximus\r\ndiam vel\r\nporta\r\nconvallis. Ut\r\naliquam\r\neros quis\r\nporta\r\npellentesqu\r\ne. Fusce in\r\nex ut mi\r\negestas\r\ncursus.\r\nAliquam\r\nerat\r\nvolutpat.\r\nCras laoreet\r\ncondimentu\r\nm laoreet.\r\nSed eget\r\nfacilisis\r\ntellus.\r\nMorbi\r\nviverra odio\r\nsed odio\r\nplacerat\r\nmollis. Duis\r\nturpis\r\nmetus,\r\ndignissim\r\nvarius urna\r\nquis, viverra\r\ndignissim\r\ndui.\r\nVivamus\r\nviverra at\r\nnisi quis\r\nconvallis.\r\nSuspendiss\r\ne fringilla\r\nrisus et ante\r\nsollicitudin,\r\nsed eleifend\r\nsem\r\nplacerat.\r\nProin\r\npretium\r\nblandit\r\narcu, eget\r\nrhoncus\r\nrisus\r\nhendrerit at.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nPhasellus\r\nvulputate\r\nefficitur\r\nmaximus.\r\nCras blandit\r\nnulla eu nisi\r\nauctor\r\ntempus.\r\nSed pretium\r\nlacus ac\r\nmagna\r\nvestibulum,\r\naliquam\r\nfaucibus\r\norci luctus.\r\nMauris enim\r\nlorem,\r\nvarius ut\r\nante quis,\r\nvarius\r\nviverra\r\nlectus.\r\nFusce\r\nblandit nibh\r\nvel feugiat\r\nefficitur.\r\nDonec\r\nmaximus id\r\njusto ac\r\nmollis.\r\nVestibulum\r\nante ipsum\r\nprimis in\r\nfaucibus\r\norci luctus\r\net ultrices\r\nposuere\r\ncubilia\r\ncurae; Nulla\r\nplacerat\r\nlectus et\r\npurus\r\ndictum, id\r\ncongue nisi\r\neuismod.\r\nMaecenas\r\neuismod\r\nfermentum\r\ndiam, sit\r\namet\r\ngravida\r\nmagna\r\nsuscipit a.\r\nQuisque\r\nconsectetur\r\narcu eu\r\nnunc\r\nsodales\r\nscelerisque.\r\nNulla non\r\ntincidunt\r\nnulla.\r\nPellentesqu\r\ne ut tortor\r\nvel enim\r\nconvallis\r\nmalesuada.\r\nAliquam\r\nultricies\r\nbibendum\r\nultrices.\r\nMauris\r\nrutrum ac\r\nnisl vel\r\nluctus.\r\nDonec quis\r\nnibh vitae\r\norci ultricies\r\ngravida.\r\nAliquam\r\nvitae velit\r\nporttitor\r\nlorem\r\nbibendum\r\nfringilla\r\nvolutpat a\r\neros.\r\nCurabitur at\r\ncommodo\r\ntortor. Etiam\r\nultricies,\r\nneque et\r\niaculis\r\neuismod,\r\ndiam ligula\r\nluctus mi,\r\nvitae\r\nlobortis felis\r\nlorem eu\r\nnulla. Sed a\r\nsemper ex.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nNulla\r\nmauris elit,\r\npulvinar ac\r\ntortor et,\r\nluctus\r\nhendrerit\r\nnisl. In\r\negestas\r\nauctor urna\r\nvitae\r\nlaoreet.\r\nPraesent\r\nbibendum\r\negestas\r\nconvallis.\r\nProin non\r\nsuscipit\r\ntellus.\r\nNullam at\r\nnibh in urna\r\nlaoreet\r\nsodales non\r\nvel tellus.\r\nDonec in\r\nenim dui.\r\nPhasellus\r\nquis quam\r\ntincidunt,\r\npellentesqu\r\ne lorem ac,\r\nscelerisque\r\nneque.\r\nInteger nec\r\ntempus\r\nurna. Donec\r\nelit massa,\r\neleifend eu\r\nsapien sit\r\namet,\r\nmollis\r\npellentesqu\r\ne est.\r\nNullam\r\ntristique\r\ntellus\r\niaculis arcu\r\nconsectetur\r\npretium.\r\nSed\r\nvenenatis\r\nconvallis\r\nscelerisque.\r\nSuspendiss\r\ne varius\r\nurna sit\r\namet purus\r\naccumsan,\r\nid ultricies\r\nerat\r\nefficitur.\r\nCras non\r\nipsum eget\r\nnulla\r\nefficitur\r\ncommodo\r\nsit amet non\r\nlacus. Proin\r\nviverra enim\r\nsit amet\r\nenim\r\ntempus\r\nullamcorper\r\n. Class\r\naptent taciti\r\nsociosqu ad\r\nlitora\r\ntorquent per\r\nconubia\r\nnostra, per\r\ninceptos\r\nhimenaeos.\r\nDuis ac\r\nmassa\r\ninterdum,\r\ngravida ex\r\negestas,\r\nfinibus\r\npurus. Nunc\r\nconsectetur\r\ncommodo\r\nlacus, ac\r\nconvallis\r\nquam\r\nlobortis eu.\r\nSed\r\nconvallis\r\ntempor\r\ncommodo.\r\nNulla sed\r\nconvallis\r\nmauris.\r\nDonec\r\nvenenatis\r\nnisi est, ac\r\nullamcorper\r\nmi pretium\r\nquis. Donec\r\nvitae eros at\r\nipsum\r\ninterdum\r\nscelerisque\r\nnec vitae\r\nnisi. Sed\r\nvestibulum\r\nerat ac\r\nbibendum\r\ndapibus.\r\nMorbi nec\r\nelit id quam\r\ntristique\r\ncursus id\r\nsed sem.\r\nPraesent\r\nnon ante\r\nenim.\r\nPellentesqu\r\ne habitant\r\nmorbi\r\ntristique\r\nsenectus et\r\nnetus et\r\nmalesuada\r\nfames ac\r\nturpis\r\negestas.\r\nPraesent\r\nnon mauris\r\ndui.\r\nAliquam\r\nrhoncus\r\nmattis ante\r\nsed\r\nvenenatis.\r\nVivamus\r\nvehicula\r\nsed sapien\r\nsed dictum.\r\nIn aliquet,\r\nurna\r\nefficitur\r\ntincidunt\r\nlobortis,\r\nnibh justo\r\ntristique\r\npurus, sed\r\nvolutpat\r\nrisus magna\r\net\r\nlibero.Susp\r\nendisse\r\nlectus justo,\r\nvarius eget\r\narcu et,\r\nsemper\r\nlaoreet erat.\r\nQuisque\r\neget lacus\r\nornare,\r\npellentesqu\r\ne erat sit\r\namet,\r\nvulputate\r\nfelis. Duis\r\nluctus,\r\nmassa a\r\npellentesqu\r\ne mollis,\r\nmassa elit\r\nconvallis\r\nmi, vel\r\nbibendum\r\nex ex eu\r\npurus.\r\nSuspendiss\r\ne vel\r\nfermentum\r\nurna, ac\r\ncommodo\r\nenim.\r\nMauris\r\ntincidunt\r\ncursus elit,\r\na volutpat\r\nlibero\r\ncommodo\r\net. Etiam\r\ndapibus\r\nlibero\r\nvenenatis\r\ntellus\r\nlobortis, vel\r\nlacinia elit\r\nfaucibus.\r\nMaecenas\r\nsemper sed\r\nquam quis\r\nfinibus.\r\nInteger\r\nefficitur,\r\nlibero\r\nimperdiet\r\nsollicitudin\r\ncommodo,\r\nelit arcu\r\nvulputate\r\nest, eget\r\nfinibus mi\r\nurna sit\r\namet\r\nmagna.\r\nCras\r\nullamcorper\r\nconsequat\r\nornare.\r\nFusce\r\nconvallis\r\nnunc vel\r\nrisus\r\ncursus, at\r\nmaximus\r\nligula\r\ncursus.\r\nPellentesqu\r\ne vulputate\r\nrisus libero,\r\neget cursus\r\nnibh\r\nsodales\r\nsed. Donec\r\naccumsan\r\nsem et\r\nmassa\r\nsemper, id\r\ndignissim\r\nvelit\r\nvehicula.Cr\r\nas cursus\r\nipsum ac\r\nerat\r\nvehicula,\r\nnec iaculis\r\npurus\r\ndictum.\r\nQuisque\r\nlacinia elit\r\nvitae leo\r\ndictum, vel\r\ndignissim\r\nvelit\r\ndapibus.\r\nAenean sem\r\nnisi,\r\nfaucibus\r\ninterdum\r\njusto eu,\r\neuismod\r\nporttitor ex.\r\nMorbi et\r\nlectus\r\nlectus. Duis\r\nneque felis,\r\nsuscipit at\r\nscelerisque\r\neu,\r\nscelerisque\r\nid orci.\r\nCurabitur et\r\nplacerat\r\nipsum.\r\nProin\r\ngravida\r\nsapien nisl,\r\net varius\r\nipsum\r\nmollis nec.\r\nQuisque\r\ndignissim\r\nconsectetur\r\nfeugiat.\r\nAenean\r\neros purus,\r\nlaoreet\r\ninterdum\r\nrutrum at,\r\naliquet sit\r\namet\r\nlectus.\r\nDonec\r\ngravida\r\nlorem ut\r\ntincidunt\r\nlaoreet.\r\nDonec\r\nconsequat\r\nviverra\r\nligula, in\r\naccumsan\r\nmi\r\nbibendum\r\nscelerisque.\r\nQuisque ac\r\nrisus justo.\r\nMorbi\r\nmagna\r\narcu,\r\negestas nec\r\nluctus\r\ncommodo,\r\ncursus eget\r\nnunc.\r\nVivamus\r\neuismod\r\nlorem ex, et\r\nmaximus\r\nfelis\r\nhendrerit\r\neget.\r\nNullam\r\nullamcorper\r\neuismod\r\nligula, et\r\niaculis\r\nligula\r\nultricies a.\r\nFusce\r\naliquam,\r\nenim vel\r\nfermentum\r\nultrices, elit\r\nquam\r\nsemper\r\nerat, vitae\r\nsemper velit\r\naugue non\r\nmagna.Quis\r\nque\r\nmaximus\r\nsemper\r\narcu, id\r\npellentesqu\r\ne est\r\ntempus a.\r\nPhasellus\r\nlacus elit,\r\nauctor sit\r\namet lacinia\r\na, dapibus\r\nvitae velit.\r\nPhasellus ut\r\npharetra\r\njusto, ut\r\nultricies\r\nerat. Sed\r\nmolestie\r\nsapien vel\r\ninterdum\r\nlobortis.\r\nNulla\r\nfacilisi.\r\nVestibulum\r\nante ipsum\r\nprimis in\r\nfaucibus\r\norci luctus\r\net ultrices\r\nposuere\r\ncubilia\r\ncurae; Nulla\r\nnec mauris\r\nquis nisi\r\nvulputate\r\ngravida quis\r\nnec\r\nvelit.Nam et\r\ncongue\r\nipsum.\r\nNulla vel elit\r\nnon dolor\r\nmollis\r\naliquet vel\r\nat magna.\r\nPellentesqu\r\ne nec\r\nfacilisis elit.\r\nIn vulputate\r\nquis sem\r\nporta\r\nsuscipit.\r\nNullam sed\r\nex ornare\r\nnibh\r\nsuscipit\r\nmattis quis\r\nnon lacus.\r\nMauris vel\r\nex urna.\r\nVivamus\r\nultricies\r\nsapien sit\r\namet sapien\r\nvehicula\r\ngravida.\r\nDonec\r\nfeugiat\r\nvolutpat\r\nquam.\r\nVestibulum\r\nauctor\r\ndictum nisl,\r\nid hendrerit\r\nmetus\r\nullamcorper\r\nsed. Nulla\r\nmaximus\r\nlacus vel\r\nmollis\r\nmaximus.\r\nNulla\r\nlaoreet\r\nplacerat\r\nquam eu\r\nviverra.\r\nEtiam\r\nfeugiat\r\naccumsan\r\nnisl a\r\ncondimentu\r\nm. Sed\r\nultricies\r\nante ante,\r\nac auctor\r\nligula\r\ngravida nec.\r\nPraesent a\r\nneque\r\ndignissim,\r\nsagittis felis\r\nsit amet,\r\ncondimentu\r\nm turpis.\r\nFusce at leo\r\nvel est\r\nblandit\r\nmalesuada.\r\nPellentesqu\r\ne et neque\r\nnon metus\r\npellentesqu\r\ne imperdiet.\r\nPraesent\r\npellentesqu\r\ne lacinia\r\nlorem, et\r\ntristique\r\ntellus\r\nefficitur id.\r\nSuspendiss\r\ne aliquet\r\nultricies\r\njusto vitae\r\ninterdum.\r\nCras\r\ntristique\r\nviverra\r\nquam, eget\r\ngravida mi\r\nfermentum\r\nimperdiet.\r\nSed\r\nimperdiet\r\nvitae purus\r\nut volutpat.\r\nNulla\r\nlacinia elit\r\nin\r\nfermentum\r\nconsectetur\r\n. Phasellus\r\ncommodo\r\nut nisl sit\r\namet\r\nsagittis.\r\nDuis ac\r\nornare orci.\r\nVivamus vel\r\nenim\r\nposuere,\r\npharetra ex\r\nvel,\r\nelementum\r\nest.\r\nVestibulum\r\ncommodo\r\nluctus\r\nmetus eget\r\nmaximus.\r\nSuspendiss\r\ne a nulla a\r\nodio\r\neleifend\r\nfaucibus.\r\nSuspendiss\r\ne semper\r\nlacus non\r\nporttitor\r\naliquet.\r\nCras ac\r\nscelerisque\r\nmagna, et\r\npulvinar\r\njusto.\r\nInteger\r\ncursus\r\npulvinar\r\nfringilla.\r\nMauris\r\nimperdiet\r\nnibh sit\r\namet\r\ntempor\r\nlaoreet.\r\nMorbi\r\ntincidunt\r\ntortor ex, sit\r\namet\r\nmaximus\r\npurus\r\ntristique\r\nquis.\r\nQuisque\r\nsed\r\nhendrerit\r\nvelit. Mauris\r\nmattis nibh\r\nut eros\r\nluctus, eget\r\nmattis\r\nmassa\r\nauctor.\r\nPhasellus\r\neu neque at\r\naugue\r\ngravida\r\nsagittis nec\r\nnon tortor.\r\nEtiam\r\nporttitor\r\nsem\r\nsodales mi\r\nullamcorper\r\ngravida. In\r\nin dictum\r\norci. In vitae\r\nvestibulum\r\nquam. Cras\r\naugue eros,\r\ntincidunt ac\r\nelit posuere,\r\nsollicitudin\r\nefficitur\r\nlectus.\r\nPraesent\r\nquis\r\nsodales\r\nnisl. Proin\r\nsit amet\r\nmolestie\r\nest. In\r\ncommodo\r\nmauris vel\r\nmauris\r\nefficitur,\r\nnec mollis\r\nmauris\r\nsagittis.\r\nCras ligula\r\nnibh,\r\negestas sit\r\namet eros\r\nin, lacinia\r\ntristique\r\nmagna.\r\nCras risus\r\nlibero,\r\nlacinia eget\r\nlibero vitae,\r\nmaximus\r\naliquet\r\nnibh. Mauris\r\nid sodales\r\npurus, vitae\r\ndictum\r\nlectus. Cras\r\nconsectetur\r\nligula velit,\r\ntempus\r\npulvinar\r\nlacus\r\nporttitor\r\nvitae.\r\nPhasellus\r\neget tellus\r\nipsum.\r\nDonec\r\ninterdum\r\nlaoreet elit\r\nnon\r\nvestibulum.\r\nCras sed\r\nurna\r\nullamcorper\r\n, aliquam\r\nerat eget,\r\nporta orci.\r\nVestibulum\r\neget congue\r\nnulla. Sed\r\nsem tortor,\r\neuismod at\r\nrutrum id,\r\nsagittis a\r\nnunc. Duis\r\nin nibh\r\nfacilisis,\r\ndignissim\r\npurus ut,\r\nhendrerit\r\nmagna. Sed\r\nsemper\r\nligula id\r\nmassa\r\nelementum,\r\nnon\r\nmalesuada\r\nvelit\r\negestas.\r\nNullam\r\ndictum, mi\r\nnec\r\neuismod\r\nsagittis,\r\nligula leo\r\nullamcorper\r\ndolor, quis\r\nfaucibus\r\nodio metus\r\neget magna.\r\nUt gravida\r\nmetus non\r\nmetus\r\nbibendum\r\nbibendum.\r\nIn sagittis\r\neleifend\r\naliquet.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nNam mollis\r\nsagittis\r\nfelis, in\r\nfaucibus\r\ntortor\r\npretium vel.\r\nNam nec\r\nenim\r\nmetus.\r\nDonec in\r\naugue arcu.\r\nProin non\r\nlobortis\r\npurus, sit\r\namet lacinia\r\nelit.\r\nSuspendiss\r\ne quis eros\r\ncondimentu\r\nm, blandit\r\njusto sit\r\namet,\r\nlobortis nisl.\r\nSuspendiss\r\ne maximus\r\nmassa sed\r\nurna tempor\r\nornare.\r\nNunc\r\nmalesuada\r\npurus odio,\r\neu luctus\r\nlectus\r\nauctor nec.\r\nMorbi\r\nauctor\r\npellentesqu\r\ne auctor.\r\nSed\r\nullamcorper\r\n, ex vitae\r\naliquam\r\nvulputate,\r\nest diam\r\nfeugiat mi,\r\nid porttitor\r\nlectus orci\r\nac leo.\r\nDonec sit\r\namet velit\r\npulvinar,\r\nvenenatis\r\nturpis ut,\r\ninterdum\r\nligula.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nVestibulum\r\neu lacus\r\nurna.\r\nMaecenas\r\nsem nulla,\r\naccumsan\r\neu ultricies\r\nsed, tempor\r\nvel magna.\r\nCras aliquet\r\nsollicitudin\r\nsapien ac\r\npulvinar.\r\nPraesent ac\r\nsodales mi.\r\nInteger vitae\r\nmauris\r\nmassa.\r\nMaecenas\r\niaculis orci\r\net faucibus\r\ninterdum.\r\nNunc nec\r\nmaximus\r\nfelis, sed\r\nfinibus\r\nquam.\r\nPellentesqu\r\ne felis\r\nmassa,\r\nvestibulum\r\nin tellus\r\nvitae,\r\ncongue\r\ntincidunt\r\njusto. Nunc\r\nvitae enim\r\nmalesuada,\r\nbibendum\r\nante nec,\r\nvarius\r\ntellus.\r\nPraesent\r\nvitae nisi id\r\nquam\r\nauctor\r\nlacinia at\r\nnon quam.\r\nNam nec\r\nligula sit\r\namet felis\r\nauctor\r\nsagittis.\r\nNunc in\r\nrisus eu\r\nurna varius\r\nlaoreet quis\r\nsit amet\r\nfelis. Morbi\r\nvarius\r\ntempor orci,\r\neu\r\nvestibulum\r\nnunc\r\nvestibulum\r\nac. Nunc\r\nvehicula\r\nvelit\r\neleifend\r\nconsequat\r\nporta.\r\nSuspendiss\r\ne maximus\r\ndapibus\r\norci, in\r\nvulputate\r\nmassa\r\npretium ac.\r\nQuisque\r\nmalesuada\r\naliquet\r\naliquet."; @@ -161,7 +162,7 @@ public void WrapText_TestFragments() public void WrapText_WithPreExistingWidth_AccountsForIt() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -181,7 +182,7 @@ public void WrapText_WithPreExistingWidth_AccountsForIt() public void WrapText_EmptyString_ReturnsEmptyLine() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -197,7 +198,7 @@ public void WrapText_EmptyString_ReturnsEmptyLine() public void WrapText_WithKerning_MeasuresCorrectly() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -220,7 +221,7 @@ public void WrapText_WithKerning_MeasuresCorrectly() public void WrapRichText_SingleFragment_BehavesLikeSingleFont() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -245,7 +246,7 @@ public void WrapRichText_SingleFragment_BehavesLikeSingleFont() public void WrapRichText_MultipleFragments_ConcatenatesCorrectly() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -275,7 +276,7 @@ public void WrapRichText_MultipleFragments_ConcatenatesCorrectly() public void WrapRichText_DifferentFonts_WrapsCorrectly() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -342,7 +343,7 @@ public void WrapLongRichTextWord() Style = MeasurementFontStyles.Regular }; - var font = OpenTypeFonts.GetFontData(FontFolders, mFont.FontFamily, FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont(mFont.FontFamily, FontSubFamily.Regular, FontFolders, true); var longWord = "pellentesquer"; @@ -413,7 +414,7 @@ public void WrapRichTextDifficultCase() public void WrapRichText_WordSpanningFragments_MeasuresCorrectly() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -444,7 +445,7 @@ public void WrapRichText_WordSpanningFragments_MeasuresCorrectly() public void WrapRichText_WithLineBreaks_PreservesBreaks() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -475,7 +476,7 @@ public void WrapRichText_WithLineBreaks_PreservesBreaks() public void WrapRichText_EmptyFragments_HandlesGracefully() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -510,7 +511,7 @@ public void WrapRichText_EmptyFragments_HandlesGracefully() public void WrapRichText_NullFragmentList_ReturnsEmptyLine() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -530,7 +531,7 @@ public void WrapRichText_NullFragmentList_ReturnsEmptyLine() public void WrapRichText_SameFontMultipleTimes_UsesCache() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Calibri", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -568,7 +569,7 @@ public void WrapRichText_SameFontMultipleTimes_UsesCache() [TestMethod] public void WrapText_Continous_Long_Word() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders); var longWord = "pellentesquer"; diff --git a/src/EPPlus.Fonts.OpenType.Tests/Reading/GposReadingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Reading/GposReadingTests.cs index 0a2b86bdf..abe02a36c 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Reading/GposReadingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Reading/GposReadingTests.cs @@ -1,4 +1,5 @@ -using EPPlus.Fonts.OpenType.Scanner; +using EPPlus.Fonts.OpenType.FontResolver; +using EPPlus.Fonts.OpenType.Scanner; using EPPlus.Fonts.OpenType.Tables.Cmap; using EPPlus.Fonts.OpenType.Tables.Gpos; using EPPlus.Fonts.OpenType.Tables.Gpos.Data.Lookups.LookupType1; @@ -6,6 +7,7 @@ using EPPlus.Fonts.OpenType.Tables.Gpos.Data.Lookups.LookupType4; using EPPlus.Fonts.OpenType.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml.Interfaces.Fonts; using System.Diagnostics; using System.Linq; @@ -16,19 +18,21 @@ public class GposReadingTests : FontTestBase { public override TestContext? TestContext { get; set; } + protected override void ConfigureResolver() + { + OpenTypeFonts.Configure(x => x.SetFontResolver(new DefaultFontResolver(FontFolders, true))); + } + [TestMethod] public void ReadGposTable_Roboto() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); - // Assert GPOS exists Assert.IsNotNull(font.GposTable, "Roboto should have GPOS table"); var gpos = font.GposTable; - // Verify basic structure Assert.AreEqual((ushort)1, gpos.MajorVersion, "GPOS major version should be 1"); Assert.IsNotNull(gpos.ScriptList, "GPOS should have ScriptList"); Assert.IsNotNull(gpos.FeatureList, "GPOS should have FeatureList"); @@ -38,17 +42,14 @@ public void ReadGposTable_Roboto() [TestMethod] public void ReadGposTable_HasKernFeature() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; Assert.IsNotNull(gpos, "GPOS table should exist"); - // Act - Find 'kern' feature var kernFeature = gpos.FeatureList.FeatureRecords .FirstOrDefault(f => f.FeatureTag.Value == "kern"); - // Assert Assert.IsNotNull(kernFeature, "GPOS should have 'kern' feature"); Assert.IsNotNull(kernFeature.FeatureTable, "kern feature should have FeatureTable"); Assert.IsTrue(kernFeature.FeatureTable.LookupListIndices.Length > 0, @@ -58,17 +59,14 @@ public void ReadGposTable_HasKernFeature() [TestMethod] public void ReadGposTable_HasPairPosLookup() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; Assert.IsNotNull(gpos, "GPOS table should exist"); - // Act - Find Type 2 (PairPos) lookup var pairPosLookup = gpos.LookupList.Lookups .FirstOrDefault(l => l.LookupType == 2); - // Assert Assert.IsNotNull(pairPosLookup, "GPOS should have Type 2 (PairPos) lookup"); Assert.IsTrue(pairPosLookup.SubTables.Count > 0, "PairPos lookup should have subtables"); @@ -77,18 +75,15 @@ public void ReadGposTable_HasPairPosLookup() [TestMethod] public void ReadGposTable_PairPosFormat1() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; var pairPosLookup = gpos.LookupList.Lookups.FirstOrDefault(l => l.LookupType == 2); Assert.IsNotNull(pairPosLookup, "Need PairPos lookup for test"); - // Act - Get first PairPos subtable var subtable = pairPosLookup.SubTables[0] as PairPosSubTableFormat1; - // Assert Assert.IsNotNull(subtable, "First subtable should be PairPosSubTableFormat1"); Assert.AreEqual((ushort)1, subtable.SubtableFormat, "Format should be 1"); Assert.IsNotNull(subtable.Coverage, "Should have Coverage table"); @@ -96,14 +91,10 @@ public void ReadGposTable_PairPosFormat1() Assert.IsTrue(subtable.PairSets.Count > 0, "Should have at least one PairSet"); } - - - [TestMethod] public void ReadGposTable_FindKerningPair_ActualPairs() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); if (!font.CmapTable.TryGetGlyphId('A', out ushort aGlyph)) { @@ -118,11 +109,8 @@ public void ReadGposTable_FindKerningPair_ActualPairs() var subtable = pairPosLookup.SubTables[0] as PairPosSubTableFormat1; Assert.IsNotNull(subtable, "Need PairPosSubTableFormat1"); - // Act - Test with pairs we KNOW exist from debug output - // A(37) + glyph 35 = kerning -61 bool found = subtable.TryGetPairAdjustment(aGlyph, 35, out var value1, out var value2); - // Assert Assert.IsTrue(found, $"Should find kerning pair for A({aGlyph}) + glyph 35"); Assert.IsNotNull(value1, "Value1 should exist"); Assert.AreEqual(-61, value1.XAdvance, "Should have XAdvance = -61"); @@ -137,9 +125,8 @@ public void SerializeCmapTable() var ffi = FontScannerV2.FindBestMatch(FontFolder, "Roboto", FontSubFamily.Regular); var originalBytes = ffi.GetTableBytes("cmap"); - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); - // Check IdDelta[3] var subtable = font.CmapTable.SubTables[0] as CmapSubtable4; Debug.WriteLine($"IdDelta[3] = {subtable.IdDelta[3]}"); @@ -155,8 +142,7 @@ public void SerializeCmapTable() [TestMethod] public void ReadGposTable_MultipleKerningPairs() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; var pairPosLookup = gpos.LookupList.Lookups.FirstOrDefault(l => l.LookupType == 2); @@ -164,9 +150,6 @@ public void ReadGposTable_MultipleKerningPairs() Assert.IsNotNull(subtable, "Need PairPosSubTableFormat1"); - // Test known pairs from debug output - // NOTE: All glyph IDs adjusted for current Roboto-Regular.ttf version - // 'A' = glyph 37, 'V' = glyph 58 var testPairs = new[] { (37, 35, -61), // A + glyph 35 @@ -178,7 +161,6 @@ public void ReadGposTable_MultipleKerningPairs() int foundCount = 0; - // Act - Check each pair foreach (var (first, second, expectedKern) in testPairs) { if (subtable.TryGetPairAdjustment((ushort)first, (ushort)second, out var val1, out var val2)) @@ -194,24 +176,20 @@ public void ReadGposTable_MultipleKerningPairs() } } - // Assert Assert.AreEqual(5, foundCount, "Should find all 5 test pairs"); } [TestMethod] public void ReadGposTable_HasSinglePosLookup() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; Assert.IsNotNull(gpos, "GPOS table should exist"); - // Act - Find Type 1 (SinglePos) lookup var singlePosLookup = gpos.LookupList.Lookups .FirstOrDefault(l => l.LookupType == 1); - // Assert if (singlePosLookup != null) { Assert.IsTrue(singlePosLookup.SubTables.Count > 0, @@ -227,8 +205,7 @@ public void ReadGposTable_HasSinglePosLookup() [TestMethod] public void ReadGposTable_SinglePosFormat1() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; var singlePosLookup = gpos.LookupList.Lookups.FirstOrDefault(l => l.LookupType == 1); @@ -239,7 +216,6 @@ public void ReadGposTable_SinglePosFormat1() return; } - // Act - Get first SinglePos subtable var subtable = singlePosLookup.SubTables.FirstOrDefault() as SinglePosSubTableFormat1; if (subtable == null) @@ -248,7 +224,6 @@ public void ReadGposTable_SinglePosFormat1() return; } - // Assert Assert.AreEqual((ushort)1, subtable.SubtableFormat, "Format should be 1"); Assert.IsNotNull(subtable.Coverage, "Should have Coverage table"); Assert.IsNotNull(subtable.Value, "Should have ValueRecord"); @@ -264,8 +239,7 @@ public void ReadGposTable_SinglePosFormat1() [TestMethod] public void ReadGposTable_SinglePosFormat2() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; var singlePosLookup = gpos.LookupList.Lookups.FirstOrDefault(l => l.LookupType == 1); @@ -276,7 +250,6 @@ public void ReadGposTable_SinglePosFormat2() return; } - // Act - Find Format 2 subtable var subtable = singlePosLookup.SubTables .OfType() .FirstOrDefault(); @@ -287,7 +260,6 @@ public void ReadGposTable_SinglePosFormat2() return; } - // Assert Assert.AreEqual((ushort)2, subtable.SubtableFormat, "Format should be 2"); Assert.IsNotNull(subtable.Coverage, "Should have Coverage table"); Assert.IsNotNull(subtable.Values, "Should have ValueRecords array"); @@ -303,8 +275,7 @@ public void ReadGposTable_SinglePosFormat2() [TestMethod] public void ReadGposTable_SinglePosTryGetAdjustment() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; var singlePosLookup = gpos.LookupList.Lookups.FirstOrDefault(l => l.LookupType == 1); @@ -315,7 +286,6 @@ public void ReadGposTable_SinglePosTryGetAdjustment() return; } - // Act - Test TryGetAdjustment on first subtable var subtableFormat1 = singlePosLookup.SubTables.OfType().FirstOrDefault(); var subtableFormat2 = singlePosLookup.SubTables.OfType().FirstOrDefault(); @@ -323,7 +293,6 @@ public void ReadGposTable_SinglePosTryGetAdjustment() if (subtableFormat1 != null) { - // Try a few common glyph IDs for (ushort glyphId = 1; glyphId < 100; glyphId++) { if (subtableFormat1.TryGetAdjustment(glyphId, out var value)) @@ -337,7 +306,6 @@ public void ReadGposTable_SinglePosTryGetAdjustment() if (subtableFormat2 != null && !foundAdjustment) { - // Try a few common glyph IDs for (ushort glyphId = 1; glyphId < 100; glyphId++) { if (subtableFormat2.TryGetAdjustment(glyphId, out var value)) @@ -358,17 +326,14 @@ public void ReadGposTable_SinglePosTryGetAdjustment() [TestMethod] public void ReadGposTable_HasMarkToBaseLookup() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; Assert.IsNotNull(gpos, "GPOS table should exist"); - // Act - Find Type 4 (MarkToBase) lookup var markToBaseLookup = gpos.LookupList.Lookups .FirstOrDefault(l => l.LookupType == 4); - // Assert if (markToBaseLookup != null) { Assert.IsTrue(markToBaseLookup.SubTables.Count > 0, @@ -384,8 +349,7 @@ public void ReadGposTable_HasMarkToBaseLookup() [TestMethod] public void ReadGposTable_MarkToBaseFormat1() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; var markToBaseLookup = gpos.LookupList.Lookups.FirstOrDefault(l => l.LookupType == 4); @@ -396,7 +360,6 @@ public void ReadGposTable_MarkToBaseFormat1() return; } - // Act - Get first MarkToBase subtable var subtable = markToBaseLookup.SubTables.FirstOrDefault() as MarkToBaseSubTableFormat1; if (subtable == null) @@ -405,7 +368,6 @@ public void ReadGposTable_MarkToBaseFormat1() return; } - // Assert Assert.AreEqual((ushort)1, subtable.SubtableFormat, "Format should be 1"); Assert.IsNotNull(subtable.MarkCoverage, "Should have MarkCoverage table"); Assert.IsNotNull(subtable.BaseCoverage, "Should have BaseCoverage table"); @@ -422,8 +384,7 @@ public void ReadGposTable_MarkToBaseFormat1() [TestMethod] public void ReadGposTable_MarkToBaseTryGetAttachment() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; var markToBaseLookup = gpos.LookupList.Lookups.FirstOrDefault(l => l.LookupType == 4); @@ -442,13 +403,8 @@ public void ReadGposTable_MarkToBaseTryGetAttachment() return; } - // Act - Try to find an attachment - // We need to know which glyphs are marks and bases - // Let's try some common combinations - bool foundAttachment = false; - // Try first few marks with first few bases for (ushort markGlyph = 1; markGlyph < 100 && !foundAttachment; markGlyph++) { if (subtable.MarkCoverage.IsCovered(markGlyph)) @@ -481,8 +437,7 @@ public void ReadGposTable_MarkToBaseTryGetAttachment() [TestMethod] public void ReadGposTable_MarkToBaseWithAccentedCharacters() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); var gpos = font.GposTable; var markToBaseLookup = gpos.LookupList.Lookups.FirstOrDefault(l => l.LookupType == 4); @@ -501,15 +456,10 @@ public void ReadGposTable_MarkToBaseWithAccentedCharacters() return; } - // Act - Try common accented character combinations - // e + combining acute (́) = é - // Get glyph IDs for these characters - if (font.CmapTable.TryGetGlyphId('e', out ushort eGlyph)) { Debug.WriteLine($"'e' = glyph {eGlyph}"); - // Combining acute accent Unicode: U+0301 if (font.CmapTable.TryGetGlyphId('\u0301', out ushort acuteGlyph)) { Debug.WriteLine($"Combining acute = glyph {acuteGlyph}"); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs index 66fc6d5a9..d76a14984 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs @@ -10,8 +10,10 @@ Date Author Change ************************************************************************************************* 12/22/2025 EPPlus Software AB TTF reading tests *************************************************************************************************/ +using EPPlus.Fonts.OpenType.FontResolver; using EPPlus.Fonts.OpenType.Tests.Helpers; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.Diagnostics; namespace EPPlus.Fonts.OpenType.Tests.Reading @@ -21,10 +23,15 @@ public sealed class TtfReadingTests : FontTestBase { public override TestContext? TestContext { get; set; } + protected override void ConfigureResolver() + { + OpenTypeFonts.Configure(x => x.SetFontResolver(new DefaultFontResolver(FontFolders, true))); + } + [TestMethod] public void ReadRobotoRegularTtf() { - OpenTypeFont? font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false); + OpenTypeFont? font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); Assert.IsNotNull(font); var cmap = font.CmapTable; Assert.AreEqual("Roboto", font.FullName); @@ -37,7 +44,7 @@ public void ReadRobotoBoldTtf() { Stopwatch sw = Stopwatch.StartNew(); sw.Start(); - OpenTypeFont? font = OpenTypeFonts.GetFontDataOpen(FontFolders, "Roboto", FontSubFamily.Bold, false); + OpenTypeFont? font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Bold); sw.Stop(); var ms = sw.ElapsedMilliseconds; Assert.IsNotNull(font); @@ -48,7 +55,7 @@ public void ReadRobotoBoldTtf() [TestMethod] public void ReadSourceSans3Otf() { - OpenTypeFont? font = OpenTypeFonts.GetFontDataOpen(FontFolders, "Source Sans 3", FontSubFamily.Regular, false); + OpenTypeFont? font = OpenTypeFonts.LoadFont("Source Sans 3", FontSubFamily.Regular); Assert.IsNotNull(font); Assert.AreEqual("Source Sans 3", font.FullName); Assert.AreEqual("Regular", font.SubFamily); @@ -88,7 +95,7 @@ string GetFsString(ushort fsId) [TestMethod] public void ReadTccGothic() { - OpenTypeFont? font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Bold, true); + OpenTypeFont? font = OpenTypeFonts.LoadFont("BIZ UDGothic", FontSubFamily.Bold); Assert.IsNotNull(font); Assert.AreEqual("BIZ UDGothic Bold", font.FullName); @@ -98,7 +105,7 @@ public void ReadTccGothic() [TestMethod] public void ReadGsubTable() { - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "Roboto", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var gSub = font.GsubTable; Assert.IsNotNull(gSub); } @@ -106,11 +113,11 @@ public void ReadGsubTable() [TestMethod] public void ReadSixFonts() { - OpenTypeFont? gothic = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Bold, true); - OpenTypeFont? calibri = OpenTypeFonts.GetFontDataOpen(FontFolders, "Calibri", FontSubFamily.Italic, true); - OpenTypeFont? aptos = OpenTypeFonts.GetFontDataOpen(FontFolders, "Aptos Narrow", FontSubFamily.Bold, true); - OpenTypeFont? timesNewRoman = OpenTypeFonts.GetFontDataOpen(FontFolders, "Times New Roman", FontSubFamily.Regular, true); - OpenTypeFont? SS3 = OpenTypeFonts.GetFontDataOpen(FontFolders, "Source Sans 3", FontSubFamily.Bold, false); + OpenTypeFont? gothic = OpenTypeFonts.LoadFont("BIZ UDGothic", FontSubFamily.Bold, FontFolders, true, true); + OpenTypeFont? calibri = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Italic, FontFolders, true, true); + OpenTypeFont? aptos = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Bold, FontFolders, true, true); + OpenTypeFont? timesNewRoman = OpenTypeFonts.LoadFont("Times New Roman", FontSubFamily.Regular, FontFolders, true, true); + OpenTypeFont? SS3 = OpenTypeFonts.LoadFont("Source Sans 3", FontSubFamily.Bold, FontFolders, true, true); Assert.IsNotNull(gothic); Assert.AreEqual("BIZ UDGothic Bold", gothic.FullName); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Reading/VerticalTextTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Reading/VerticalTextTests.cs index 3a195d3be..04aef4022 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Reading/VerticalTextTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Reading/VerticalTextTests.cs @@ -15,7 +15,7 @@ public class VerticalTextTests : FontTestBase [TestMethod] public void TestVerticalMetrics_Vhea() { - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic"); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); Assert.IsNotNull(font); var vhea = font.VheaTable; Assert.IsNotNull(vhea, "vhea table should be present in a CJK font."); @@ -30,7 +30,7 @@ public void TestVerticalMetrics_Vhea() [TestMethod] public void TestVerticalMetrics_Vmtx() { - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic"); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); Assert.IsNotNull(font); var vhea = font.VheaTable; diff --git a/src/EPPlus.Fonts.OpenType.Tests/Regression/RegressionTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Regression/RegressionTests.cs index 4b05238d2..b490c6589 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Regression/RegressionTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Regression/RegressionTests.cs @@ -13,6 +13,7 @@ Date Author Change using EPPlus.Fonts.OpenType.FontCache; using EPPlus.Fonts.OpenType.Tests.Helpers; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.IO; @@ -45,14 +46,13 @@ public void Bug_20251222_CircularLigatureDependency_Roboto() // Discovery and Rewrite phases // DATE: 2025-12-22 - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var subset = font.CreateSubset("ffi"); SaveFont("regression_ffi_circular.ttf", subset); - // Should create valid font with ffi ligature var bytes = subset.Serialize(); - var parsed = new OpenTypeFont(bytes, font.Format); + var parsed = new OpenTypeFont(bytes); Assert.IsNotNull(parsed.GsubTable); @@ -73,16 +73,14 @@ public void Bug_20251222_AbcSubset_TooManyGlyphs() // exist in the subset // DATE: 2025-12-22 - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var subset = font.CreateSubset(new[] { 'a', 'b', 'c' }); SaveFont("regression_abc_glyphs.ttf", subset); - // Should have ~10 glyphs (abc + space + .notdef + variants), NOT 28 Assert.IsTrue(subset.MaxpTable.numGlyphs >= 5 && subset.MaxpTable.numGlyphs <= 15, $"Expected 5-15 glyphs for abc, got {subset.MaxpTable.numGlyphs}"); - // Should have NO ligatures int ligCount = FontTestHelper.CountLigatures(subset); Assert.AreEqual(0, ligCount, "abc should have no ligatures"); } @@ -95,16 +93,14 @@ public void Bug_20251222_Fiffig_LostLigatures_AfterAbcFix() // FIX: Corrected logic to only check base components (GID < 400) // DATE: 2025-12-22 - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var subset = font.CreateSubset("fiffig"); SaveFont("regression_fiffig_ligatures.ttf", subset); - // Should have exactly 3 ligatures: fi, ff, ffi int ligCount = FontTestHelper.CountLigatures(subset); Assert.AreEqual(3, ligCount, "fiffig should have fi, ff, ffi ligatures"); - // Verify font is valid FontTestHelper.AssertFontValid(subset); } @@ -116,12 +112,11 @@ public void Bug_20251222_FeaturePointsToInvalidLookup() // FIX: FeatureListTable.Rewrite now uses lookupMap to remap indices // DATE: 2025-12-22 - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var subset = font.CreateSubset("fiffig"); SaveFont("regression_feature_lookup.ttf", subset); - // Verify all features point to valid lookups if (subset.GsubTable?.FeatureList?.FeatureRecords != null) { int lookupCount = subset.GsubTable.LookupList?.Lookups?.Count ?? 0; @@ -149,16 +144,14 @@ public void Bug_20251222_LigatureComponentRewrite_WrongDictionary() // FIX: Properly lookup each component in OldToNewGlyphId dictionary // DATE: 2025-12-22 - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var subset = font.CreateSubset("fi"); SaveFont("regression_ligature_components.ttf", subset); - // Serialize and re-parse to verify components are correct var bytes = subset.Serialize(); - var parsed = new OpenTypeFont(bytes, font.Format); + var parsed = new OpenTypeFont(bytes); - // Should have fi ligature with correctly remapped components int ligCount = FontTestHelper.CountLigatures(parsed); Assert.IsTrue(ligCount >= 1, "Should have fi ligature"); @@ -174,12 +167,11 @@ public void Bug_20251222_ValidationCrash_NullRawData() // FIX: Check if RawData is null before accessing, skip checksum validation for in-memory fonts // DATE: 2025-12-22 - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var subset = font.CreateSubset("f"); SaveFont("regression_validation_rawdata.ttf", subset); - // Should not crash during validation FontTestHelper.AssertFontValid(subset); Assert.IsNotNull(subset); @@ -194,12 +186,11 @@ public void Bug_20251222_ValidationCrash_FileLengthZero() // FIX: Calculate fileLength from TableRecords if FileLength property is <= 0 // DATE: 2025-12-22 - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var subset = font.CreateSubset("fl"); SaveFont("regression_validation_filelength.ttf", subset); - // Should not fail with file length error FontTestHelper.AssertFontValid(subset); Assert.IsNotNull(subset); @@ -214,18 +205,16 @@ public void Bug_20251222_MissingCoverageInitialization() // FIX: Initialize Coverage.SubstFormat = 1 and newSubTable.SubstFormat = 1 // DATE: 2025-12-22 - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var subset = font.CreateSubset("office"); SaveFont("regression_coverage_init.ttf", subset); - // Should serialize successfully var bytes = subset.Serialize(); Assert.IsNotNull(bytes); Assert.IsTrue(bytes.Length > 0); - // Should parse successfully - var parsed = new OpenTypeFont(bytes, font.Format); + var parsed = new OpenTypeFont(bytes); Assert.IsNotNull(parsed); FontTestHelper.AssertFontValid(parsed); @@ -239,7 +228,7 @@ public void Bug_20251222_EmptySubset_ShouldThrow() // FIX: Added validation to throw ArgumentException if usedChars is empty // DATE: 2025-12-22 - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); Assert.ThrowsException(() => font.CreateSubset("")); Assert.ThrowsException(() => font.CreateSubset((char[])null)); @@ -257,16 +246,14 @@ public void Bug_20251222_CompoundLigatureComponents() // RESULT: Simplified component lists (ffi = [f, f, i] instead of [f, i, fi]) // DATE: 2025-12-22 - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var subset = font.CreateSubset("fiffig"); SaveFont("regression_compound_components.ttf", subset); - // Should have 3 ligatures: ff, fi, ffi int ligCount = FontTestHelper.CountLigatures(subset); Assert.AreEqual(3, ligCount); - // Verify in FontDrop or similar tool that ligatures render correctly FontTestHelper.AssertFontValid(subset); } @@ -283,7 +270,8 @@ public void GetFromCacheBoldItalicShouldWork() var ttTextMeasurer = new FontMeasurerTrueType(); ttTextMeasurer.SetFont(boldItalic); - var cachedFont = OpenTypeFontCache.GetFromCache("Aptos Narrow", FontSubFamily.BoldItalic); + var cacheKey = OpenTypeFonts.BuildCacheKey("Aptos Narrow", FontSubFamily.BoldItalic, Enumerable.Empty(), searchSystemDirectories: true); + var cachedFont = OpenTypeFontCache.GetFromCache(cacheKey); Assert.IsNotNull(cachedFont); } } diff --git a/src/EPPlus.Fonts.OpenType.Tests/Serialization/CmapSerializationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Serialization/CmapSerializationTests.cs index 30608f5ee..b84b0c87d 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Serialization/CmapSerializationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Serialization/CmapSerializationTests.cs @@ -1,5 +1,6 @@ using EPPlus.Fonts.OpenType.Scanner; using EPPlus.Fonts.OpenType.Tests.Helpers; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Linq; @@ -18,7 +19,7 @@ public void SerializeCmapTable() var ffi = FontScannerV2.FindBestMatch(FontFolder, "Roboto", FontSubFamily.Regular); var originalBytes = ffi.GetTableBytes("cmap"); - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto"); var cmapBytes = font.CmapTable.Serialize(font); Assert.AreEqual(originalBytes.Length, cmapBytes?.Length); @@ -28,7 +29,7 @@ public void SerializeCmapTable() [TestMethod] public void SerializeCmapTable_Format12() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Noto Emoji", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Noto Emoji"); // Ta unique chars from the originalfonts cmap (format 4 + 12) var allCodePoints = new HashSet(); @@ -41,8 +42,8 @@ public void SerializeCmapTable_Format12() } // re-serialize - var bytes = font.CmapTable.Serialize(font); - var tempFont = new OpenTypeFont(bytes, font.Format); + var bytes = font.Serialize(); + var tempFont = new OpenTypeFont(bytes); // Check that ALL original chars are still there foreach (uint cp in allCodePoints) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Serialization/CoreTableSerializationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Serialization/CoreTableSerializationTests.cs index 675376699..e7ca71ce3 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Serialization/CoreTableSerializationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Serialization/CoreTableSerializationTests.cs @@ -1,5 +1,6 @@ using EPPlus.Fonts.OpenType.Scanner; using EPPlus.Fonts.OpenType.Tests.Helpers; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Linq; @@ -18,7 +19,7 @@ public void SerializeHeadTable() var ffi = FontScannerV2.FindBestMatch(FontFolder, "Roboto", FontSubFamily.Regular); var originalBytes = ffi.GetTableBytes("head"); - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var headBytes = font?.HeadTable.Serialize(font); Assert.AreEqual(originalBytes.Length, headBytes?.Length); @@ -31,7 +32,7 @@ public void SerializeMaxpTable() var ffi = FontScannerV2.FindBestMatch(FontFolder, "Roboto", FontSubFamily.Regular); var originalBytes = ffi.GetTableBytes("maxp"); - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var maxpBytes = font?.MaxpTable.Serialize(font); Assert.AreEqual(originalBytes.Length, maxpBytes?.Length); @@ -44,7 +45,7 @@ public void SerializeHheaTable() var ffi = FontScannerV2.FindBestMatch(FontFolder, "Roboto", FontSubFamily.Regular); var originalBytes = ffi.GetTableBytes("hhea"); - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var hheaBytes = font?.HheaTable.Serialize(font); Assert.AreEqual(originalBytes.Length, hheaBytes?.Length); @@ -57,7 +58,7 @@ public void SerializePostTable() var ffi = FontScannerV2.FindBestMatch(FontFolders, "Roboto", FontSubFamily.Regular); var originalBytes = ffi.GetTableBytes("post"); - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var postBytes = font?.PostTable.Serialize(font); Assert.AreEqual(originalBytes.Length, postBytes?.Length); @@ -70,7 +71,7 @@ public void SerializeNameTable() var ffi = FontScannerV2.FindBestMatch(FontFolder, "Roboto", FontSubFamily.Regular); var originalBytes = ffi.GetTableBytes("name"); - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var nameBytes = font?.NameTable.Serialize(font); Assert.AreEqual(originalBytes.Length, nameBytes?.Length); @@ -87,7 +88,7 @@ public void SerializeOs2Table(string fontName, FontSubFamily subFamily, int expe var ffi = FontScannerV2.FindBestMatch(FontFolder, fontName, subFamily); var originalBytes = ffi.GetTableBytes("OS/2"); - var font = OpenTypeFonts.GetFontData(FontFolders, fontName, subFamily, false, true); + var font = OpenTypeFonts.LoadFont(fontName, subFamily); var os2Bytes = font?.Os2Table.Serialize(font); Assert.AreEqual(expectedLength, os2Bytes?.Length); @@ -98,4 +99,4 @@ public void SerializeOs2Table(string fontName, FontSubFamily subFamily, int expe CollectionAssert.AreEqual(originalBytes, os2Bytes); } } -} +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/Serialization/GPosSerializationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Serialization/GPosSerializationTests.cs index 39d8ed285..8633c1ad5 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Serialization/GPosSerializationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Serialization/GPosSerializationTests.cs @@ -37,9 +37,6 @@ public class GposSerializationTests : FontTestBase #region Helper Classes for .NET 3.5 Compatibility - /// - /// Represents a glyph pair key for kerning lookups - /// private class GlyphPair { public ushort FirstGlyph { get; set; } @@ -64,9 +61,6 @@ public override int GetHashCode() } } - /// - /// Represents anchor point data for mark-to-base attachments - /// private class AnchorPointPair { public short MarkAnchorX { get; set; } @@ -75,9 +69,6 @@ private class AnchorPointPair public short BaseAnchorY { get; set; } } - /// - /// Represents a single position adjustment - /// private class SinglePosValue { public short XPlacement { get; set; } @@ -91,8 +82,7 @@ private class SinglePosValue [TestMethod] public void Diagnose_SerializedFontOffsets() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, ignoreCache: true); + var font = OpenTypeFonts.LoadFont("Roboto"); Debug.WriteLine("=== ORIGINAL TABLE RECORDS ==="); foreach (var kvp in font.TableRecords) @@ -101,23 +91,20 @@ public void Diagnose_SerializedFontOffsets() kvp.Key, kvp.Value.Offset, kvp.Value.Length)); } - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); Debug.WriteLine(string.Format("\n=== SERIALIZED FONT: {0} bytes ===", bytes.Length)); - // Read back table directory from serialized bytes using (var ms = new MemoryStream(bytes)) using (var reader = new FontsBinaryReader(ms)) { - // Skip sfnt header (12 bytes) reader.BaseStream.Position = 0; uint sfntVersion = reader.ReadUInt32BigEndian(); ushort numTables = reader.ReadUInt16BigEndian(); - reader.ReadUInt16BigEndian(); // searchRange - reader.ReadUInt16BigEndian(); // entrySelector - reader.ReadUInt16BigEndian(); // rangeShift + reader.ReadUInt16BigEndian(); + reader.ReadUInt16BigEndian(); + reader.ReadUInt16BigEndian(); Debug.WriteLine(string.Format("\nsfntVersion: 0x{0:X8}", sfntVersion)); Debug.WriteLine(string.Format("numTables: {0}", numTables)); @@ -131,14 +118,8 @@ public void Diagnose_SerializedFontOffsets() uint offset = reader.ReadUInt32BigEndian(); uint length = reader.ReadUInt32BigEndian(); - string status = ""; - if (offset + length > bytes.Length) - { - status = " *** INVALID: extends beyond file!"; - } - - Debug.WriteLine(string.Format("{0}: Offset={1}, Length={2}{3}", - tag, offset, length, status)); + string status = offset + length > bytes.Length ? " *** INVALID: extends beyond file!" : ""; + Debug.WriteLine(string.Format("{0}: Offset={1}, Length={2}{3}", tag, offset, length, status)); } } } @@ -148,39 +129,32 @@ public void Diagnose_SerializedFontOffsets() [TestMethod] public void SerializeGpos_StructurePreserved() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, ignoreCache: true); + var font = OpenTypeFonts.LoadFont("Roboto"); var originalGpos = font.GposTable; - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); - // Assert - Structure Assert.IsNotNull(reparsed.GposTable, "GPOS table should exist after roundtrip"); Assert.AreEqual(originalGpos.MajorVersion, reparsed.GposTable.MajorVersion); Assert.AreEqual(originalGpos.MinorVersion, reparsed.GposTable.MinorVersion); - // Script count Assert.AreEqual( originalGpos.ScriptList.ScriptRecords.Count, reparsed.GposTable.ScriptList.ScriptRecords.Count, "Script count should match"); - // Feature count Assert.AreEqual( originalGpos.FeatureList.FeatureRecords.Count, reparsed.GposTable.FeatureList.FeatureRecords.Count, "Feature count should match"); - // Lookup count Assert.AreEqual( originalGpos.LookupList.Lookups.Count, reparsed.GposTable.LookupList.Lookups.Count, "Lookup count should match"); - // Lookup types should match for (int i = 0; i < originalGpos.LookupList.Lookups.Count; i++) { Assert.AreEqual( @@ -193,29 +167,22 @@ public void SerializeGpos_StructurePreserved() [TestMethod] public void SerializeGpos_FeatureTagsPreserved() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var originalTags = new List(); foreach (var feature in font.GposTable.FeatureList.FeatureRecords) - { originalTags.Add(feature.FeatureTag.Value); - } originalTags.Sort(); - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); var reparsedTags = new List(); foreach (var feature in reparsed.GposTable.FeatureList.FeatureRecords) - { reparsedTags.Add(feature.FeatureTag.Value); - } reparsedTags.Sort(); - // Assert Assert.AreEqual(originalTags.Count, reparsedTags.Count, "Feature count should match"); for (int i = 0; i < originalTags.Count; i++) { @@ -229,29 +196,22 @@ public void SerializeGpos_FeatureTagsPreserved() [TestMethod] public void SerializeGpos_ScriptTagsPreserved() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, ignoreCache: true); + var font = OpenTypeFonts.LoadFont("Roboto"); var originalTags = new List(); foreach (var script in font.GposTable.ScriptList.ScriptRecords) - { originalTags.Add(script.ScriptTag.Value); - } originalTags.Sort(); - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); var reparsedTags = new List(); foreach (var script in reparsed.GposTable.ScriptList.ScriptRecords) - { reparsedTags.Add(script.ScriptTag.Value); - } reparsedTags.Sort(); - // Assert Assert.AreEqual(originalTags.Count, reparsedTags.Count, "Script count should match"); for (int i = 0; i < originalTags.Count; i++) { @@ -269,25 +229,19 @@ public void SerializeGpos_ScriptTagsPreserved() [TestMethod] public void SerializeGpos_PairPos_KerningValuesPreserved() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, ignoreCache: true); + var font = OpenTypeFonts.LoadFont("Roboto"); - // Collect original kerning pairs var originalKerning = CollectKerningPairs(font); Debug.WriteLine(string.Format("Original font has {0} kerning pairs", originalKerning.Count)); - Assert.IsTrue(originalKerning.Count > 0, "Font should have kerning pairs"); - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); - // Assert - All kerning values should be preserved var reparsedKerning = CollectKerningPairs(reparsed); - Assert.AreEqual(originalKerning.Count, reparsedKerning.Count, - "Kerning pair count should match"); + Assert.AreEqual(originalKerning.Count, reparsedKerning.Count, "Kerning pair count should match"); int verified = 0; foreach (var pair in originalKerning.Keys) @@ -311,17 +265,14 @@ public void SerializeGpos_PairPos_KerningValuesPreserved() [TestMethod] public void SerializeGpos_PairPos_SpecificPairsVerified() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, ignoreCache: true); + var font = OpenTypeFonts.LoadFont("Roboto"); - // Get glyph IDs for test characters ushort fGlyph, eGlyph, aGlyph, vGlyph; font.CmapTable.TryGetGlyphId('f', out fGlyph); font.CmapTable.TryGetGlyphId('e', out eGlyph); font.CmapTable.TryGetGlyphId('A', out aGlyph); font.CmapTable.TryGetGlyphId('V', out vGlyph); - // Get original values var origLookup = FindFirstLookupOfType(font.GposTable, 2); Assert.IsNotNull(origLookup, "Should have PairPos lookup"); @@ -332,21 +283,13 @@ public void SerializeGpos_PairPos_SpecificPairsVerified() bool hasFe = origSubtable.TryGetPairAdjustment(fGlyph, eGlyph, out feOrig1, out feOrig2); bool hasAv = origSubtable.TryGetPairAdjustment(aGlyph, vGlyph, out avOrig1, out avOrig2); - if (hasFe) - { - Debug.WriteLine(string.Format("Original: f-e = {0}", feOrig1.XAdvance)); - } - if (hasAv) - { - Debug.WriteLine(string.Format("Original: A-V = {0}", avOrig1.XAdvance)); - } + if (hasFe) Debug.WriteLine(string.Format("Original: f-e = {0}", feOrig1.XAdvance)); + if (hasAv) Debug.WriteLine(string.Format("Original: A-V = {0}", avOrig1.XAdvance)); - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); - // Assert var newLookup = FindFirstLookupOfType(reparsed.GposTable, 2); Assert.IsNotNull(newLookup, "Reparsed should have PairPos lookup"); @@ -356,8 +299,7 @@ public void SerializeGpos_PairPos_SpecificPairsVerified() if (hasFe) { ValueRecord feNew1, feNew2; - Assert.IsTrue(newSubtable.TryGetPairAdjustment(fGlyph, eGlyph, out feNew1, out feNew2), - "f-e pair should exist"); + Assert.IsTrue(newSubtable.TryGetPairAdjustment(fGlyph, eGlyph, out feNew1, out feNew2), "f-e pair should exist"); Assert.AreEqual(feOrig1.XAdvance, feNew1.XAdvance, "f-e kerning should match"); Debug.WriteLine(string.Format("f-e: {0} verified", feNew1.XAdvance)); } @@ -365,8 +307,7 @@ public void SerializeGpos_PairPos_SpecificPairsVerified() if (hasAv) { ValueRecord avNew1, avNew2; - Assert.IsTrue(newSubtable.TryGetPairAdjustment(aGlyph, vGlyph, out avNew1, out avNew2), - "A-V pair should exist"); + Assert.IsTrue(newSubtable.TryGetPairAdjustment(aGlyph, vGlyph, out avNew1, out avNew2), "A-V pair should exist"); Assert.AreEqual(avOrig1.XAdvance, avNew1.XAdvance, "A-V kerning should match"); Debug.WriteLine(string.Format("A-V: {0} verified", avNew1.XAdvance)); } @@ -375,25 +316,20 @@ public void SerializeGpos_PairPos_SpecificPairsVerified() [TestMethod] public void SerializeGpos_PairPos_ValueFormatPreserved() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, ignoreCache: true); + var font = OpenTypeFonts.LoadFont("Roboto"); var origLookup = FindFirstLookupOfType(font.GposTable, 2); var origSubtable = origLookup.SubTables[0] as PairPosSubTableFormat1; - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); var newLookup = FindFirstLookupOfType(reparsed.GposTable, 2); var newSubtable = newLookup.SubTables[0] as PairPosSubTableFormat1; - // Assert - Assert.AreEqual(origSubtable.ValueFormat1, newSubtable.ValueFormat1, - "ValueFormat1 should be preserved"); - Assert.AreEqual(origSubtable.ValueFormat2, newSubtable.ValueFormat2, - "ValueFormat2 should be preserved"); + Assert.AreEqual(origSubtable.ValueFormat1, newSubtable.ValueFormat1, "ValueFormat1 should be preserved"); + Assert.AreEqual(origSubtable.ValueFormat2, newSubtable.ValueFormat2, "ValueFormat2 should be preserved"); Debug.WriteLine(string.Format("ValueFormat1: 0x{0:X4}", newSubtable.ValueFormat1)); Debug.WriteLine(string.Format("ValueFormat2: 0x{0:X4}", newSubtable.ValueFormat2)); @@ -406,8 +342,7 @@ public void SerializeGpos_PairPos_ValueFormatPreserved() [TestMethod] public void SerializeGpos_SinglePos_ValuesPreserved() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, ignoreCache: true); + var font = OpenTypeFonts.LoadFont("Roboto"); var singlePosLookup = FindFirstLookupOfType(font.GposTable, 1); if (singlePosLookup == null) @@ -416,20 +351,16 @@ public void SerializeGpos_SinglePos_ValuesPreserved() return; } - // Collect original adjustments var originalAdjustments = CollectSinglePosAdjustments(font); Debug.WriteLine(string.Format("Original has {0} single adjustments", originalAdjustments.Count)); - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); - // Assert var reparsedAdjustments = CollectSinglePosAdjustments(reparsed); - Assert.AreEqual(originalAdjustments.Count, reparsedAdjustments.Count, - "SinglePos adjustment count should match"); + Assert.AreEqual(originalAdjustments.Count, reparsedAdjustments.Count, "SinglePos adjustment count should match"); foreach (ushort glyphId in originalAdjustments.Keys) { @@ -440,14 +371,10 @@ public void SerializeGpos_SinglePos_ValuesPreserved() var actual = reparsedAdjustments[glyphId]; - Assert.AreEqual(expected.XPlacement, actual.XPlacement, - string.Format("Glyph {0} XPlacement", glyphId)); - Assert.AreEqual(expected.YPlacement, actual.YPlacement, - string.Format("Glyph {0} YPlacement", glyphId)); - Assert.AreEqual(expected.XAdvance, actual.XAdvance, - string.Format("Glyph {0} XAdvance", glyphId)); - Assert.AreEqual(expected.YAdvance, actual.YAdvance, - string.Format("Glyph {0} YAdvance", glyphId)); + Assert.AreEqual(expected.XPlacement, actual.XPlacement, string.Format("Glyph {0} XPlacement", glyphId)); + Assert.AreEqual(expected.YPlacement, actual.YPlacement, string.Format("Glyph {0} YPlacement", glyphId)); + Assert.AreEqual(expected.XAdvance, actual.XAdvance, string.Format("Glyph {0} XAdvance", glyphId)); + Assert.AreEqual(expected.YAdvance, actual.YAdvance, string.Format("Glyph {0} YAdvance", glyphId)); } Debug.WriteLine(string.Format("Verified {0} single adjustments", originalAdjustments.Count)); @@ -460,8 +387,7 @@ public void SerializeGpos_SinglePos_ValuesPreserved() [TestMethod] public void SerializeGpos_MarkToBase_StructurePreserved() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, ignoreCache: true); + var font = OpenTypeFonts.LoadFont("Roboto"); var markToBaseLookup = FindFirstLookupOfType(font.GposTable, 4); if (markToBaseLookup == null) @@ -473,12 +399,10 @@ public void SerializeGpos_MarkToBase_StructurePreserved() var origSubtable = markToBaseLookup.SubTables[0] as MarkToBaseSubTableFormat1; Assert.IsNotNull(origSubtable, "Should have MarkToBase subtable"); - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); - // Assert var newLookup = FindFirstLookupOfType(reparsed.GposTable, 4); Assert.IsNotNull(newLookup, "MarkToBase lookup should exist"); @@ -490,16 +414,13 @@ public void SerializeGpos_MarkToBase_StructurePreserved() Assert.AreEqual(origSubtable.BaseArray.BaseCount, newSubtable.BaseArray.BaseCount, "BaseCount"); Debug.WriteLine(string.Format("MarkToBase preserved: {0} classes, {1} marks, {2} bases", - newSubtable.MarkClassCount, - newSubtable.MarkArray.MarkCount, - newSubtable.BaseArray.BaseCount)); + newSubtable.MarkClassCount, newSubtable.MarkArray.MarkCount, newSubtable.BaseArray.BaseCount)); } [TestMethod] public void SerializeGpos_MarkToBase_AnchorPointsPreserved() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, ignoreCache: true); + var font = OpenTypeFonts.LoadFont("Roboto"); var markToBaseLookup = FindFirstLookupOfType(font.GposTable, 4); if (markToBaseLookup == null) @@ -509,9 +430,8 @@ public void SerializeGpos_MarkToBase_AnchorPointsPreserved() } var origSubtable = markToBaseLookup.SubTables[0] as MarkToBaseSubTableFormat1; - - // Find valid mark-base pairs var originalAttachments = CollectMarkToBaseAttachments(origSubtable); + if (originalAttachments.Count == 0) { Assert.Inconclusive("No attachments found"); @@ -520,12 +440,10 @@ public void SerializeGpos_MarkToBase_AnchorPointsPreserved() Debug.WriteLine(string.Format("Found {0} mark-base attachments", originalAttachments.Count)); - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); - // Assert var newLookup = FindFirstLookupOfType(reparsed.GposTable, 4); var newSubtable = newLookup.SubTables[0] as MarkToBaseSubTableFormat1; @@ -539,10 +457,8 @@ public void SerializeGpos_MarkToBase_AnchorPointsPreserved() var expected = originalAttachments[pair]; AnchorTable newMarkAnchor, newBaseAnchor; - Assert.IsTrue(newSubtable.TryGetAttachment(pair.FirstGlyph, pair.SecondGlyph, - out newMarkAnchor, out newBaseAnchor), - string.Format("Missing attachment for mark {0}, base {1}", - pair.FirstGlyph, pair.SecondGlyph)); + Assert.IsTrue(newSubtable.TryGetAttachment(pair.FirstGlyph, pair.SecondGlyph, out newMarkAnchor, out newBaseAnchor), + string.Format("Missing attachment for mark {0}, base {1}", pair.FirstGlyph, pair.SecondGlyph)); Assert.AreEqual(expected.MarkAnchorX, newMarkAnchor.XCoordinate, string.Format("Mark anchor X for ({0}, {1})", pair.FirstGlyph, pair.SecondGlyph)); @@ -566,15 +482,12 @@ public void SerializeGpos_MarkToBase_AnchorPointsPreserved() [TestMethod] public void SerializeGpos_FeatureLookupIndices_AreValid() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, ignoreCache: true); + var font = OpenTypeFonts.LoadFont("Roboto"); - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); - // Assert int lookupCount = reparsed.GposTable.LookupList.Lookups.Count; foreach (var feature in reparsed.GposTable.FeatureList.FeatureRecords) @@ -593,7 +506,7 @@ public void SerializeGpos_FeatureLookupIndices_AreValid() [TestMethod] public void Diagnose_GposTableOffset() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); Debug.WriteLine("=== TABLE RECORDS ==="); foreach (var kvp in font.TableRecords) @@ -604,9 +517,7 @@ public void Diagnose_GposTableOffset() Debug.WriteLine(string.Format("\n=== READER STATE ===")); Debug.WriteLine(string.Format("Reader stream length: {0}", font._tblSettings.TableReaderFactory.FontBytesLength)); - Debug.WriteLine(string.Format("Reader stream position: {0}", font._tblSettings.TableReaderFactory.FontBytesLength)); - // Try to read GPOS Debug.WriteLine("\n=== LOADING GPOS ==="); var gpos = font.GposTable; Debug.WriteLine(string.Format("GPOS version: {0}.{1}", gpos.MajorVersion, gpos.MinorVersion)); @@ -615,20 +526,16 @@ public void Diagnose_GposTableOffset() [TestMethod] public void SerializeGpos_LangSysFeatureIndices_AreValid() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); - // Assert int featureCount = reparsed.GposTable.FeatureList.FeatureRecords.Count; foreach (var script in reparsed.GposTable.ScriptList.ScriptRecords) { - // Check DefaultLangSys if (script.ScriptTable.DefaultLangSys != null) { foreach (var idx in script.ScriptTable.DefaultLangSys.FeatureIndices) @@ -639,7 +546,6 @@ public void SerializeGpos_LangSysFeatureIndices_AreValid() } } - // Check other LangSys records foreach (var langSys in script.ScriptTable.LangSysRecords) { foreach (var idx in langSys.LangSysTable.FeatureIndices) @@ -661,15 +567,12 @@ public void SerializeGpos_LangSysFeatureIndices_AreValid() [TestMethod] public void SerializeGpos_CoverageGlyphIds_AreValid() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, ignoreCache: true); + var font = OpenTypeFonts.LoadFont("Roboto"); - // Act var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); - var reparsed = new OpenTypeFont(bytes, font.Format); + var reparsed = new OpenTypeFont(bytes); - // Assert ushort maxGlyph = reparsed.MaxpTable.numGlyphs; for (int lookupIdx = 0; lookupIdx < reparsed.GposTable.LookupList.Lookups.Count; lookupIdx++) @@ -708,9 +611,7 @@ private LookupTable FindFirstLookupOfType(GposTable gpos, int lookupType) foreach (var lookup in gpos.LookupList.Lookups) { if (lookup.LookupType == lookupType) - { return lookup; - } } return null; } @@ -729,7 +630,6 @@ private Dictionary CollectKerningPairs(OpenTypeFont font) var coveredGlyphs = subtable.Coverage.GetCoveredGlyphs(); - // PairSets är en List som är indexerad parallellt med Coverage for (int i = 0; i < coveredGlyphs.Length && i < subtable.PairSets.Count; i++) { ushort firstGlyph = coveredGlyphs[i]; @@ -836,33 +736,16 @@ private List GetCoveragesFromSubtable(object subtable) var coverages = new List(); var pp1 = subtable as PairPosSubTableFormat1; - if (pp1 != null) - { - coverages.Add(pp1.Coverage); - return coverages; - } + if (pp1 != null) { coverages.Add(pp1.Coverage); return coverages; } var sp1 = subtable as SinglePosSubTableFormat1; - if (sp1 != null) - { - coverages.Add(sp1.Coverage); - return coverages; - } + if (sp1 != null) { coverages.Add(sp1.Coverage); return coverages; } var sp2 = subtable as SinglePosSubTableFormat2; - if (sp2 != null) - { - coverages.Add(sp2.Coverage); - return coverages; - } + if (sp2 != null) { coverages.Add(sp2.Coverage); return coverages; } var mtb = subtable as MarkToBaseSubTableFormat1; - if (mtb != null) - { - coverages.Add(mtb.MarkCoverage); - coverages.Add(mtb.BaseCoverage); - return coverages; - } + if (mtb != null) { coverages.Add(mtb.MarkCoverage); coverages.Add(mtb.BaseCoverage); return coverages; } return coverages; } diff --git a/src/EPPlus.Fonts.OpenType.Tests/Serialization/GlyphTableSerializationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Serialization/GlyphTableSerializationTests.cs index ad1b54ac6..a97ca24ad 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Serialization/GlyphTableSerializationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Serialization/GlyphTableSerializationTests.cs @@ -1,5 +1,6 @@ using EPPlus.Fonts.OpenType.Scanner; using EPPlus.Fonts.OpenType.Tests.Helpers; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Linq; @@ -19,7 +20,7 @@ public void SerializeLocaTable() var ffi = FontScannerV2.FindBestMatch(FontFolder, "Roboto", FontSubFamily.Regular); var originalBytes = ffi.GetTableBytes("loca"); - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var locaBytes = font.LocaTable.Serialize(font); Assert.AreEqual(originalBytes.Length, locaBytes?.Length); @@ -32,7 +33,7 @@ public void SerializeHtmxTable() var ffi = FontScannerV2.FindBestMatch(FontFolder, "Roboto", FontSubFamily.Regular); var originalBytes = ffi.GetTableBytes("hmtx"); - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var hmtxBytes = font?.HmtxTable.Serialize(font); Assert.AreEqual(originalBytes.Length, hmtxBytes?.Length); @@ -46,10 +47,11 @@ public void SerializeHtmxTable() [DataRow("Mulish", FontSubFamily.Regular)] public void SerializeGlyfTable(string fontName, FontSubFamily subFamily) { + OpenTypeFonts.ClearFontCache(); var ffi = FontScannerV2.FindBestMatch(FontFolders, fontName, subFamily); var originalBytes = ffi.GetTableBytes("glyf"); - var font = OpenTypeFonts.GetFontData(FontFolders, fontName, subFamily, false, true); + var font = OpenTypeFonts.LoadFont(fontName, subFamily); var glyfBytes = font.GlyfTable.Serialize(font); Assert.AreEqual(originalBytes.Length, glyfBytes?.Length, diff --git a/src/EPPlus.Fonts.OpenType.Tests/Serialization/KernSerializationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Serialization/KernSerializationTests.cs index 4da664f3e..517675118 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Serialization/KernSerializationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Serialization/KernSerializationTests.cs @@ -1,5 +1,6 @@ using EPPlus.Fonts.OpenType.Scanner; using EPPlus.Fonts.OpenType.Tests.Helpers; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Linq; @@ -19,7 +20,7 @@ public void SerializeKernTable() var ffi = FontScannerV2.FindBestMatch(@"c:\windows\fonts", "Arial", FontSubFamily.Regular); var originalBytes = ffi.GetTableBytes("kern"); - var font = OpenTypeFonts.GetFontData(new List { @"c:\windows\fonts" }, "Arial", FontSubFamily.Regular, false); + var font = OpenTypeFonts.LoadFont("Arial"); var kernBytes = font?.KernTable.Serialize(font); Assert.AreEqual(originalBytes.Length, kernBytes?.Length); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs index 4770c5212..c86277435 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs @@ -28,19 +28,15 @@ public class BasicSubsettingTests : FontTestBase [TestMethod] public void Subset_Abc_RoundtripValidation() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false); + var font = OpenTypeFonts.LoadFont("Roboto"); var subsetFont = font.CreateSubset(new[] { 'a', 'b', 'c' }); - // Act var serializer = new OpenTypeFontSerializer(subsetFont); var bytes = serializer.Serialize(); - var parsedFont = new OpenTypeFont(bytes, font.Format); + var parsedFont = new OpenTypeFont(bytes); - // Save for inspection SaveFontForCurrentTest(parsedFont); - // Assert: Check table presence Assert.AreEqual(12, parsedFont.TableRecords.Count); Assert.IsTrue(parsedFont.TableRecords.ContainsKey("head")); Assert.IsTrue(parsedFont.TableRecords.ContainsKey("name")); @@ -55,14 +51,11 @@ public void Subset_Abc_RoundtripValidation() Assert.IsTrue(parsedFont.TableRecords.ContainsKey("GSUB")); Assert.IsTrue(parsedFont.TableRecords.ContainsKey("GPOS")); - // Validate all tables FontTestHelper.AssertFontValid(parsedFont); - // ✅ FIXED: abc should have NO ligatures int ligatureCount = FontTestHelper.CountLigatures(parsedFont); Assert.AreEqual(0, ligatureCount, "abc should have NO ligatures"); - // Verify glyph count (approximately) int expectedGlyphs = 3; // a, b, c expectedGlyphs += 1; // + space (U+0020) expectedGlyphs += 1; // + .notdef (GID 0) @@ -71,39 +64,33 @@ public void Subset_Abc_RoundtripValidation() Assert.AreEqual((ushort)expectedGlyphs, parsedFont.MaxpTable.numGlyphs); Assert.AreEqual((ushort)expectedGlyphs, parsedFont.HheaTable.numberOfHMetrics); - // Verify space exists in cmap Assert.IsTrue(parsedFont.CmapTable.ContainsChar(32)); } [TestMethod] public void Subset_Fiffig_WithFullValidation() { - // Arrange - var fontName = "Roboto"; - var font = OpenTypeFonts.GetFontData(FontFolders, fontName, FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("Roboto"); var subsetFont = font.CreateSubset("fiffig"); - // Act var serializer = new OpenTypeFontSerializer(subsetFont); var bytes = serializer.Serialize(); - var parsedFont = new OpenTypeFont(bytes, font.Format); + var parsedFont = new OpenTypeFont(bytes); - // Save for inspection - SaveFontForCurrentTest( parsedFont); + SaveFontForCurrentTest(parsedFont); - // Assert FontTestHelper.AssertFontValid(parsedFont, FontValidationSeverity.Warning); } [TestMethod] public void Subset_SingleChar_ShouldWork() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Mulish", FontSubFamily.Regular, false); + var font = OpenTypeFonts.LoadFont("Mulish"); var subsetFont = font.CreateSubset(new[] { 'a' }); var serializer = new OpenTypeFontSerializer(subsetFont); var bytes = serializer.Serialize(); - var parsedFont = new OpenTypeFont(bytes, font.Format); + var parsedFont = new OpenTypeFont(bytes); SaveFontForCurrentTest(parsedFont); @@ -113,7 +100,7 @@ public void Subset_SingleChar_ShouldWork() [TestMethod] public void Subset_MultipleChars_ShouldWork() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false); + var font = OpenTypeFonts.LoadFont("Roboto"); var subsetFont = font.CreateSubset(new[] { 'F', 'l', 'y', 'g', 'a', 'n', 'd', 'e', 'b', 'ä', 'c', 'k', 's', 'i', 'r', 'ö', 'h', 'w', 'p', 'å', 'm', 'j', 'u', 't', 'v', 'o', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' @@ -121,7 +108,7 @@ public void Subset_MultipleChars_ShouldWork() var serializer = new OpenTypeFontSerializer(subsetFont); var bytes = serializer.Serialize(); - var parsedFont = new OpenTypeFont(bytes, font.Format); + var parsedFont = new OpenTypeFont(bytes); SaveFontForCurrentTest(parsedFont); @@ -131,7 +118,6 @@ public void Subset_MultipleChars_ShouldWork() [TestMethod] public void Subset_RoundtripHelper_ShouldWork() { - // Using FontTestHelper.RoundtripSubset var parsedFont = FontTestHelper.RoundtripSubset("Roboto", "test", FontFolders); SaveFontForCurrentTest(parsedFont); @@ -143,9 +129,8 @@ public void Subset_RoundtripHelper_ShouldWork() [TestMethod] public void Check_Original_Roboto_Ligatures() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false); + var font = OpenTypeFonts.LoadFont("Roboto"); - // Get glyph IDs in ORIGINAL font.CmapTable.TryGetGlyphId('f', out ushort fGlyph); font.CmapTable.TryGetGlyphId('i', out ushort iGlyph); font.CmapTable.TryGetGlyphId('o', out ushort oGlyph); @@ -183,9 +168,8 @@ public void Check_Original_Roboto_Ligatures() [TestMethod] public void Subset_Ligatures_ShouldStillWork() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false); + var font = OpenTypeFonts.LoadFont("Roboto"); - // Check ORIGINAL first Debug.WriteLine("=== ORIGINAL ROBOTO ==="); if (font.GsubTable != null) { @@ -216,13 +200,12 @@ public void Subset_Ligatures_ShouldStillWork() Debug.WriteLine($"Feature[{i}]: '{feat.FeatureTag.Value}'"); } - // Create subset Debug.WriteLine("\n=== CREATING SUBSET ==="); var subsetFont = font.CreateSubset("fiffigoffice"); var serializer = new OpenTypeFontSerializer(subsetFont); var bytes = serializer.Serialize(); - var parsedFont = new OpenTypeFont(bytes, font.Format); + var parsedFont = new OpenTypeFont(bytes); SaveFontForCurrentTest(parsedFont); @@ -234,7 +217,6 @@ public void Subset_Ligatures_ShouldStillWork() Debug.WriteLine($"'f' = glyph {subsetF}"); Debug.WriteLine($"'i' = glyph {subsetI}"); - // Check GSUB Assert.IsNotNull(parsedFont.GsubTable, "GSUB should be present"); Debug.WriteLine($"\n=== GSUB FEATURES (DETAILED) ==="); @@ -253,7 +235,6 @@ public void Subset_Ligatures_ShouldStillWork() Assert.IsTrue(ligLookups.Count > 0, "Should have ligature lookups"); - // Check what ligatures exist foreach (var lookup in ligLookups) { foreach (var subtable in lookup.SubTables) @@ -293,7 +274,6 @@ public void Subset_Ligatures_ShouldStillWork() var scriptTable = scriptRecord.ScriptTable; - // Check DefaultLangSys if (scriptTable.DefaultLangSys != null) { var defLang = scriptTable.DefaultLangSys; @@ -301,7 +281,6 @@ public void Subset_Ligatures_ShouldStillWork() Debug.WriteLine($" RequiredFeatureIndex: {defLang.RequiredFeatureIndex}"); Debug.WriteLine($" FeatureIndices: [{string.Join(", ", defLang.FeatureIndices.Select(i => i.ToString()).ToArray())}]"); - // Show what features these indices point to foreach (var featIdx in defLang.FeatureIndices) { if (featIdx < parsedFont.GsubTable.FeatureList.FeatureRecords.Count) @@ -313,21 +292,16 @@ public void Subset_Ligatures_ShouldStillWork() } } } + FontTestHelper.AssertFontValid(parsedFont, FontValidationSeverity.Warning); } - - [TestMethod] public void Subset_WithGposKerning_ShouldPreservePositioning() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false); - - // Use characters that ACTUALLY have kerning in Roboto + var font = OpenTypeFonts.LoadFont("Roboto"); var chars = new[] { 'f', 'e', 'c', 'd', 'g', 'E', 'a', 'b', ' ' }; - // Check ORIGINAL font - test f-e pair bool foundF_Original = font.CmapTable.TryGetGlyphId('f', out ushort fGlyphOrig); bool foundE_Original = font.CmapTable.TryGetGlyphId('e', out ushort eGlyphOrig); @@ -337,7 +311,6 @@ public void Subset_WithGposKerning_ShouldPreservePositioning() Assert.IsTrue(foundF_Original && foundE_Original, "Should find f and e in original"); - // Verify kerning exists in original bool hasOriginalKerning = false; if (font.GposTable != null) { @@ -356,17 +329,15 @@ public void Subset_WithGposKerning_ShouldPreservePositioning() Assert.IsTrue(hasOriginalKerning, "f-e should have kerning in original font"); - // Create subset Debug.WriteLine($"\n=== CREATING SUBSET ==="); var subsetFont = font.CreateSubset(chars); var serializer = new OpenTypeFontSerializer(subsetFont); var bytes = serializer.Serialize(); - var parsedFont = new OpenTypeFont(bytes, font.Format); + var parsedFont = new OpenTypeFont(bytes); SaveFontForCurrentTest(parsedFont); - // Check SUBSET font bool foundF = parsedFont.CmapTable.TryGetGlyphId('f', out ushort fGlyph); bool foundE = parsedFont.CmapTable.TryGetGlyphId('e', out ushort eGlyph); @@ -379,7 +350,6 @@ public void Subset_WithGposKerning_ShouldPreservePositioning() var kernLookups = parsedFont.GposTable.LookupList.Lookups.Where(l => l.LookupType == 2).ToList(); Assert.IsTrue(kernLookups.Count > 0, "Should have at least one kerning lookup"); - // Verify f-e kerning is preserved bool hasKerning = false; foreach (var lookup in kernLookups) { @@ -402,29 +372,22 @@ public void Subset_WithGposKerning_ShouldPreservePositioning() [TestMethod] public void Subset_WithGposSingleAdjustment_ShouldPreserve() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false); - - // Include various characters that might have single adjustments + var font = OpenTypeFonts.LoadFont("Roboto"); var chars = new[] { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', - 'A', 'B', 'C', 'D', 'E', ' ' - }; + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'A', 'B', 'C', 'D', 'E', ' ' + }; var subsetFont = font.CreateSubset(chars); - // Act var serializer = new OpenTypeFontSerializer(subsetFont); var bytes = serializer.Serialize(); - var parsedFont = new OpenTypeFont(bytes, font.Format); + var parsedFont = new OpenTypeFont(bytes); - // Save for inspection SaveFontForCurrentTest(parsedFont); - // Assert FontTestHelper.AssertFontValid(parsedFont, FontValidationSeverity.Warning); - // Check if SinglePos lookups exist if (parsedFont.GposTable != null) { var singlePosLookups = parsedFont.GposTable.LookupList.Lookups @@ -432,43 +395,32 @@ public void Subset_WithGposSingleAdjustment_ShouldPreserve() .ToList(); if (singlePosLookups.Count > 0) - { System.Diagnostics.Debug.WriteLine($"✅ Subset has {singlePosLookups.Count} SinglePos lookup(s)"); - } else - { System.Diagnostics.Debug.WriteLine("ℹ️ No SinglePos lookups in subset (may not be present in Roboto)"); - } } } [TestMethod] public void Subset_WithGposMarkToBase_ShouldPreserveAccents() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false); - - // Include base characters and their accented versions + var font = OpenTypeFonts.LoadFont("Roboto"); var chars = new[] { - 'e', 'a', 'o', 'u', 'i', 'n', - 'é', 'à', 'ö', 'ü', 'ñ', ' ', - 'E', 'A', 'O', 'U' - }; + 'e', 'a', 'o', 'u', 'i', 'n', + 'é', 'à', 'ö', 'ü', 'ñ', ' ', + 'E', 'A', 'O', 'U' + }; var subsetFont = font.CreateSubset(chars); - // Act var serializer = new OpenTypeFontSerializer(subsetFont); var bytes = serializer.Serialize(); - var parsedFont = new OpenTypeFont(bytes, font.Format); + var parsedFont = new OpenTypeFont(bytes); - // Save for inspection SaveFontForCurrentTest(parsedFont); - // Assert FontTestHelper.AssertFontValid(parsedFont, FontValidationSeverity.Warning); - // Check if MarkToBase lookups exist if (parsedFont.GposTable != null) { var markToBaseLookups = parsedFont.GposTable.LookupList.Lookups @@ -479,7 +431,6 @@ public void Subset_WithGposMarkToBase_ShouldPreserveAccents() { System.Diagnostics.Debug.WriteLine($"✅ Subset has {markToBaseLookups.Count} MarkToBase lookup(s)"); - // Try to verify a specific attachment var subtable = markToBaseLookups[0].SubTables.FirstOrDefault() as MarkToBaseSubTableFormat1; if (subtable != null) { @@ -498,28 +449,18 @@ public void Subset_WithGposMarkToBase_ShouldPreserveAccents() [TestMethod] public void Subset_CompleteGposTest_AllThreeLookupTypes() { - // Arrange - Kitchen sink test with all GPOS features - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false); - - // Characters covering: - // - Kerning pairs (A-V, T-o, etc.) - // - Potential single adjustments - // - Accented characters for MarkToBase + var font = OpenTypeFonts.LoadFont("Roboto"); var text = "AVTO Wave Typography TEST café résumé ñoño 123"; var subsetFont = font.CreateSubset(text); - // Act var serializer = new OpenTypeFontSerializer(subsetFont); var bytes = serializer.Serialize(); - var parsedFont = new OpenTypeFont(bytes, font.Format); + var parsedFont = new OpenTypeFont(bytes); - // Save for inspection SaveFontForCurrentTest(parsedFont); - // Assert FontTestHelper.AssertFontValid(parsedFont, FontValidationSeverity.Warning); - // Comprehensive GPOS check if (parsedFont.GposTable != null) { System.Diagnostics.Debug.WriteLine("=== GPOS Subsetting Results ==="); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/CompositeGlyphSubsettingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/CompositeGlyphSubsettingTests.cs index f44dc4ccd..a5c55bf95 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/CompositeGlyphSubsettingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/CompositeGlyphSubsettingTests.cs @@ -11,6 +11,7 @@ Date Author Change 12/21/2025 EPPlus Software AB Composite glyph subsetting tests *************************************************************************************************/ using EPPlus.Fonts.OpenType.Tests.Helpers; +using OfficeOpenXml.Interfaces.Fonts; using System.Linq; namespace EPPlus.Fonts.OpenType.Tests.Subsetting @@ -23,7 +24,7 @@ public class CompositeGlyphSubsettingTests : FontTestBase [TestMethod] public void Subset_Roboto_With_ÅÄÖ_Should_Work() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); // Get the original å var ågId = font.CmapTable.MapCharToGlyph('å'); @@ -45,7 +46,7 @@ public class CompositeGlyphSubsettingTests : FontTestBase [TestMethod] public void Subset_Mulish_With_ÅÄÖ_Should_Work() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Mulish", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Mulish", FontSubFamily.Regular); var subset = font.CreateSubset("Testar åäö ÅÄÖ och även é û č ć đ ł".Distinct()); // Save for inspection (CI/CD safe) @@ -62,7 +63,7 @@ public class CompositeGlyphSubsettingTests : FontTestBase [TestMethod] public void Subset_BIZUDGothic_With_ÅÄÖ_Should_Work() { - var font = OpenTypeFonts.GetFontData(FontFolders, "BIZUDGothic", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("BIZUDGothic", FontSubFamily.Regular); var subset = font.CreateSubset("Testar åäö ÅÄÖ och även é û č ć đ ł".Distinct()); // Save for inspection (CI/CD safe) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/LigatureSubsettingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/LigatureSubsettingTests.cs index 9cbba3b16..567928524 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/LigatureSubsettingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/LigatureSubsettingTests.cs @@ -25,7 +25,7 @@ public class LigatureSubsettingTests : FontTestBase [TestMethod] public void Subset_Abc_ShouldHaveNoLigatures() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset(new[] { 'a', 'b', 'c' }); SaveFontForCurrentTest(subset); @@ -37,7 +37,7 @@ public void Subset_Abc_ShouldHaveNoLigatures() [TestMethod] public void Subset_Fiffig_ShouldHaveThreeLigatures() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset("fiffig"); SaveFontForCurrentTest(subset); @@ -49,7 +49,7 @@ public void Subset_Fiffig_ShouldHaveThreeLigatures() [TestMethod] public void Subset_Fi_ShouldHaveFiLigature() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset("fi"); SaveFontForCurrentTest(subset); @@ -61,7 +61,7 @@ public void Subset_Fi_ShouldHaveFiLigature() [TestMethod] public void Subset_Ff_ShouldHaveFfLigature() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset("ff"); SaveFontForCurrentTest(subset); @@ -77,7 +77,7 @@ public void Subset_Ff_ShouldHaveFfLigature() [DataRow("fl")] public void Subset_CommonLigatures_ShouldWork(string text) { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset(text); SaveFontForCurrentTest(subset, text); @@ -85,7 +85,6 @@ public void Subset_CommonLigatures_ShouldWork(string text) Assert.IsNotNull(subset); FontTestHelper.AssertFontValid(subset); - // Should have at least one ligature int ligCount = FontTestHelper.CountLigatures(subset); Assert.IsTrue(ligCount > 0, $"{text} should have ligatures"); } @@ -93,22 +92,20 @@ public void Subset_CommonLigatures_ShouldWork(string text) [TestMethod] public void Subset_OnlyF_ShouldNotCrash() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset("f"); SaveFontForCurrentTest(subset); - // Should not crash, may or may not have ligatures Assert.IsNotNull(subset); Assert.IsTrue(subset.MaxpTable.numGlyphs > 0); FontTestHelper.AssertFontValid(subset); - } [TestMethod] public void Subset_Office_ShouldHaveFfiLigature() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset("office"); SaveFontForCurrentTest(subset); @@ -120,7 +117,7 @@ public void Subset_Office_ShouldHaveFfiLigature() [TestMethod] public void Subset_HasLigatureLookupType() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset("fi"); bool hasLigatures = FontTestHelper.HasGsubLookupType(subset, 4); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/SubsettingEdgeCasesTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/SubsettingEdgeCasesTests.cs index fc60eca42..524744cf4 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/SubsettingEdgeCasesTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/SubsettingEdgeCasesTests.cs @@ -28,7 +28,7 @@ public class SubsettingEdgeCasesTests : FontTestBase [ExpectedException(typeof(ArgumentException))] public void Subset_EmptyString_ShouldThrow() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset(""); } @@ -36,14 +36,14 @@ public void Subset_EmptyString_ShouldThrow() [ExpectedException(typeof(ArgumentNullException))] public void Subset_NullArray_ShouldThrow() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset((char[])null); } [TestMethod] public void Subset_SingleChar_ShouldHaveMinimalGlyphs() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset("a"); SaveFont("edge_single_char.ttf", subset); @@ -56,7 +56,7 @@ public void Subset_SingleChar_ShouldHaveMinimalGlyphs() [TestMethod] public void Subset_LargeText_ShouldCompleteQuickly() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var allLatinChars = Enumerable.Range(32, 95).Select(i => (char)i).ToArray(); var sw = System.Diagnostics.Stopwatch.StartNew(); @@ -72,7 +72,7 @@ public void Subset_LargeText_ShouldCompleteQuickly() [TestMethod] public void Subset_DuplicateChars_ShouldDedup() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset1 = font.CreateSubset("aaa"); var subset2 = font.CreateSubset("a"); @@ -84,7 +84,7 @@ public void Subset_DuplicateChars_ShouldDedup() [TestMethod] public void Subset_AllGlyphs_ShouldBeSimilarSizeToOriginal() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); // Get ALL characters from cmap var allChars = new HashSet(); @@ -115,7 +115,7 @@ public void Subset_AllGlyphs_ShouldBeSimilarSizeToOriginal() public void Subset_PreservesRobotoKerning() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var builder = new SubsetFontBuilder(); // Act diff --git a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/VerticalSubsettingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/VerticalSubsettingTests.cs index 967d6f31e..95efe9815 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/VerticalSubsettingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/VerticalSubsettingTests.cs @@ -32,13 +32,13 @@ public static void ClassInitialize(TestContext ctx) public void Subset_CjkFont_SubsetContainsVheaTable() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); Assert.IsNotNull(font.VheaTable, "BIZ UDGothic should have a vhea table"); // Act var subset = font.CreateSubset("日本語"); var bytes = subset.Serialize(); - var parsed = new OpenTypeFont(bytes, font.Format); + var parsed = new OpenTypeFont(bytes); SaveFontForCurrentTest(parsed); @@ -50,13 +50,13 @@ public void Subset_CjkFont_SubsetContainsVheaTable() public void Subset_CjkFont_SubsetContainsVmtxTable() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); Assert.IsNotNull(font.VmtxTable, "BIZ UDGothic should have a vmtx table"); // Act var subset = font.CreateSubset("日本語"); var bytes = subset.Serialize(); - var parsed = new OpenTypeFont(bytes, font.Format); + var parsed = new OpenTypeFont(bytes); SaveFontForCurrentTest(parsed); @@ -68,13 +68,13 @@ public void Subset_CjkFont_SubsetContainsVmtxTable() public void Subset_FontWithoutVmtx_SubsetDoesNotContainVmtxTable() { // Arrange - Roboto has no vmtx/vhea - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "Roboto", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("Roboto"); Assert.IsNull(font.VmtxTable, "Roboto should not have a vmtx table"); // Act var subset = font.CreateSubset("ABC"); var bytes = subset.Serialize(); - var parsed = new OpenTypeFont(bytes, font.Format); + var parsed = new OpenTypeFont(bytes); // Assert - vmtx should not be introduced by subsetting Assert.IsNull(parsed.VmtxTable, "Subset of font without vmtx should not contain vmtx table"); @@ -88,12 +88,12 @@ public void Subset_FontWithoutVmtx_SubsetDoesNotContainVmtxTable() public void Subset_CjkFont_VheaNumberOfVMetricsMatchesGlyphCount() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); // Act var subset = font.CreateSubset("日本語"); var bytes = subset.Serialize(); - var parsed = new OpenTypeFont(bytes, font.Format); + var parsed = new OpenTypeFont(bytes); SaveFontForCurrentTest(parsed); @@ -112,7 +112,7 @@ public void Subset_CjkFont_VheaNumberOfVMetricsMatchesGlyphCount() public void Subset_CjkFont_VmtxAdvanceHeightPreservedForSubsettedGlyphs() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var text = "日"; // Get original glyph ID and advance height before subsetting @@ -123,7 +123,7 @@ public void Subset_CjkFont_VmtxAdvanceHeightPreservedForSubsettedGlyphs() // Act var subset = font.CreateSubset(text); var bytes = subset.Serialize(); - var parsed = new OpenTypeFont(bytes, font.Format); + var parsed = new OpenTypeFont(bytes); SaveFontForCurrentTest(parsed); @@ -141,12 +141,12 @@ public void Subset_CjkFont_VmtxAdvanceHeightPreservedForSubsettedGlyphs() public void Subset_CjkFont_VmtxEntryCountMatchesGlyphCount() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); // Act var subset = font.CreateSubset("東京"); var bytes = subset.Serialize(); - var parsed = new OpenTypeFont(bytes, font.Format); + var parsed = new OpenTypeFont(bytes); SaveFontForCurrentTest(parsed); @@ -161,12 +161,12 @@ public void Subset_CjkFont_VmtxEntryCountMatchesGlyphCount() public void Subset_CjkFont_PassesValidationAfterSubsetting() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); // Act var subset = font.CreateSubset("日本語テスト"); var bytes = subset.Serialize(); - var parsed = new OpenTypeFont(bytes, font.Format); + var parsed = new OpenTypeFont(bytes); SaveFontForCurrentTest(parsed); diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ChainingContextualSubstitutionTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ChainingContextualSubstitutionTests.cs index 8d3f5c14a..ac06672ea 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ChainingContextualSubstitutionTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ChainingContextualSubstitutionTests.cs @@ -28,7 +28,7 @@ public class ChainingContextualSubstitutionTests : FontTestBase public void ChainingContextual_Roboto_FfiLigature_Office() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -43,7 +43,7 @@ public void ChainingContextual_Roboto_FfiLigature_Office() public void ChainingContextual_Roboto_FfiLigature_AtStart() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act - ffi at the beginning of text (no backtrack context) @@ -58,7 +58,7 @@ public void ChainingContextual_Roboto_FfiLigature_AtStart() public void ChainingContextual_Roboto_FfiLigature_AtEnd() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act - ffi at the end of text (no lookahead context) @@ -73,7 +73,7 @@ public void ChainingContextual_Roboto_FfiLigature_AtEnd() public void ChainingContextual_Roboto_MultipleFfiLigatures() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act - Multiple ffi sequences in same text @@ -93,7 +93,7 @@ public void ChainingContextual_Roboto_MultipleFfiLigatures() public void ChainingContextual_Roboto_Type6BeforeType4_CorrectOrder() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var subset = font.CreateSubset("office fit"); var shaper = new TextShaper(subset); @@ -114,7 +114,7 @@ public void ChainingContextual_Roboto_Type6BeforeType4_CorrectOrder() public void ChainingContextual_Roboto_FfiLigature_HasCorrectMetrics() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs index a62b97f91..0a9fbd88a 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs @@ -1,5 +1,6 @@ -using EPPlus.Fonts.OpenType.TextShaping; -using OfficeOpenXml.Interfaces.Drawing.Text; +using EPPlus.Fonts.OpenType.FontResolver; +using EPPlus.Fonts.OpenType.TextShaping; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Diagnostics; @@ -10,14 +11,21 @@ namespace EPPlus.Fonts.OpenType.Tests.TextShaping { [TestClass] - public class MarkToBaseTests : FontTestBase + public class MarkToBaseTests { - public override TestContext? TestContext { get; set; } + [TestInitialize] + public void TestSetup() + { + OpenTypeFonts.ClearFontCache(); + + } [TestMethod] public void MarkToBaseTest() { - var font = OpenTypeFonts.GetFontData(null, "Roboto", FontSubFamily.Regular, true, true); + var resolver = new DefaultFontResolver(null, true); // system-Roboto, ingen testmapp + OpenTypeFonts.Configure(x => x.SetFontResolver(resolver)); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular, ignoreCache: true); Debug.WriteLine("=== MarkToBaseTest ==="); Debug.WriteLine($"Font instance: {font.GetHashCode()}"); Debug.WriteLine($"CmapTable instance: {font.CmapTable.GetHashCode()}"); diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleAdjustmentsTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleAdjustmentsTests.cs index 85b312086..a9bdb7c13 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleAdjustmentsTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleAdjustmentsTests.cs @@ -1,7 +1,7 @@ using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.TextShaping; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.Diagnostics; namespace EPPlus.Fonts.OpenType.Tests.TextShaping @@ -14,15 +14,11 @@ public class SingleAdjustmentTests : FontTestBase [TestMethod] public void SingleAdjustment_Roboto_DoesNotCrash() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); - // Act - Shape text with glyphs that are in the Single Adjustment coverage - // (even though the adjustments are all zero) var result = shaper.Shape("Hello World"); - // Assert - Should not crash and should produce valid output Assert.IsNotNull(result); Assert.IsTrue(result.Glyphs.Length > 0); Assert.AreEqual("Hello World", result.OriginalText); @@ -31,19 +27,13 @@ public void SingleAdjustment_Roboto_DoesNotCrash() [TestMethod] public void SingleAdjustment_WithZeroValues_DoesNotAffectOutput() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); - // Act - Shape same text with and without positioning var withPositioning = shaper.Shape("AV"); var withoutPositioning = shaper.Shape("AV", ShapingOptions.None); - // Assert - With zero-value Single Adjustments, the glyphs should only - // differ by kerning (not by Single Adjustment since all values are 0) Assert.AreEqual(withoutPositioning.Glyphs.Length, withPositioning.Glyphs.Length); - - // The difference should only be from kerning Assert.IsTrue(withPositioning.TotalAdvanceWidth < withoutPositioning.TotalAdvanceWidth, "Should have kerning applied"); } @@ -51,32 +41,24 @@ public void SingleAdjustment_WithZeroValues_DoesNotAffectOutput() [TestMethod] public void SingleAdjustment_WithZeroValues_DoesNotAffectOutput2() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); - // Act - Shape same text with and without positioning var withPositioning = shaper.Shape("AV"); var withoutPositioning = shaper.Shape("AV", ShapingOptions.None); - // Debug output Debug.WriteLine($"Without positioning: {withoutPositioning.TotalAdvanceWidth}"); Debug.WriteLine($"With positioning: {withPositioning.TotalAdvanceWidth}"); Debug.WriteLine($"Difference: {withoutPositioning.TotalAdvanceWidth - withPositioning.TotalAdvanceWidth}"); Debug.WriteLine("\nWithout positioning glyphs:"); foreach (var g in withoutPositioning.Glyphs) - { Debug.WriteLine($" GlyphId: {g.GlyphId}, XAdvance: {g.XAdvance}, XOffset: {g.XOffset}"); - } Debug.WriteLine("\nWith positioning glyphs:"); foreach (var g in withPositioning.Glyphs) - { Debug.WriteLine($" GlyphId: {g.GlyphId}, XAdvance: {g.XAdvance}, XOffset: {g.XOffset}"); - } - // Assert Assert.AreEqual(withoutPositioning.Glyphs.Length, withPositioning.Glyphs.Length); Assert.IsTrue(withPositioning.TotalAdvanceWidth < withoutPositioning.TotalAdvanceWidth, "Should have kerning applied"); @@ -85,10 +67,9 @@ public void SingleAdjustment_WithZeroValues_DoesNotAffectOutput2() [TestMethod] public void Kerning_IsApplied_ForAVPair() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); - // Shape with only kerning (no single adjustment) var optionsOnlyKern = new ShapingOptions { ApplySubstitutions = false, @@ -108,30 +89,23 @@ public void Kerning_IsApplied_ForAVPair() [TestMethod] public void SingleAdjustment_Verdana_HasRealAdjustments() { - // NOTE: This test requires Verdana font to be installed - // Verdana has Single Adjustment Format 2 with XPlacement=36 for certain glyphs - try { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Verdana", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Verdana"); if (font == null || font.FullName != "Verdana") { Assert.Inconclusive("Verdana font not found - test skipped"); - return; // Koden efter detta körs inte + return; } var shaper = new TextShaper(font); - // Act - Shape some text (we don't know which specific glyphs have adjustments) var withPositioning = shaper.Shape("Hello123"); var withoutPositioning = shaper.Shape("Hello123", ShapingOptions.None); - // Assert - Should produce valid output Assert.IsNotNull(withPositioning); Assert.IsNotNull(withoutPositioning); Assert.AreEqual(withPositioning.Glyphs.Length, withoutPositioning.Glyphs.Length); - // Check if any glyph has XOffset applied (from Single Adjustment) bool hasXOffset = false; for (int i = 0; i < withPositioning.Glyphs.Length; i++) { @@ -142,8 +116,6 @@ public void SingleAdjustment_Verdana_HasRealAdjustments() } } - // Note: We can't assert that hasXOffset is true because we don't know - // which characters map to the adjusted glyphs. But we can verify no crash. System.Console.WriteLine($"Found XOffset adjustments: {hasXOffset}"); } catch (System.IO.FileNotFoundException) @@ -155,23 +127,18 @@ public void SingleAdjustment_Verdana_HasRealAdjustments() [TestMethod] public void SingleAdjustment_Verdana_AdjustmentsAppliedWithDefaultOptions() { - // NOTE: This test requires Verdana font to be installed - try { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Verdana", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Verdana"); if (font == null || font.FullName != "Verdana") { Assert.Inconclusive("Verdana font not found - test skipped"); - return; // Koden efter detta körs inte + return; } var shaper = new TextShaper(font); - // Act - Use default options (which includes positioning) var result = shaper.Shape("Test"); - // Assert - Should not crash and produce valid output Assert.IsNotNull(result); Assert.AreEqual(4, result.Glyphs.Length); Assert.AreEqual("Test", result.OriginalText); @@ -185,18 +152,11 @@ public void SingleAdjustment_Verdana_AdjustmentsAppliedWithDefaultOptions() [TestMethod] public void SingleAdjustment_AppliedBeforeKerning() { - // This test verifies the order of operations: - // Single Adjustment should be applied before Kerning - - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); - // Act var result = shaper.Shape("Test"); - // Assert - Just verify it doesn't crash and produces valid output - // (We can't test the actual order without non-zero Single Adjustment values) Assert.IsNotNull(result); Assert.IsTrue(result.Glyphs.Length > 0); } @@ -204,19 +164,14 @@ public void SingleAdjustment_AppliedBeforeKerning() [TestMethod] public void SingleAdjustment_NotAppliedWithNoneOptions() { - // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); - // Act var result = shaper.Shape("Test", ShapingOptions.None); - // Assert - Should have basic glyph mapping but no positioning Assert.IsNotNull(result); Assert.AreEqual(4, result.Glyphs.Length); - // With ShapingOptions.None, glyphs should have their base advance widths - // (no kerning or Single Adjustment applied) foreach (var glyph in result.Glyphs) { Assert.AreEqual(0, glyph.XOffset, "No offset adjustments with None options"); @@ -227,14 +182,11 @@ public void SingleAdjustment_NotAppliedWithNoneOptions() [TestMethod] public void SingleAdjustmentProvider_HandlesNullFont() { - // Arrange - Create provider with font that has no GPOS - var font = OpenTypeFonts.GetFontData(FontFolders, "SourceSans3", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("SourceSans3"); var shaper = new TextShaper(font); - // Act - Should not crash even though SourceSans3 has no GPOS table var result = shaper.Shape("Test"); - // Assert Assert.IsNotNull(result); Assert.AreEqual(4, result.Glyphs.Length); } diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleSubstitutionTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleSubstitutionTests.cs index 80c601188..b1dceb99a 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleSubstitutionTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleSubstitutionTests.cs @@ -14,7 +14,7 @@ Date Author Change using EPPlus.Fonts.OpenType.Tables.Gsub.Data.Lookups; using EPPlus.Fonts.OpenType.TextShaping; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -38,7 +38,7 @@ public void SingleSubstitution_SmallCaps_SubstitutesGlyphs() { try { - var font = OpenTypeFonts.GetFontData(FontFolders, fontName, FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); // Verify font has smcp feature with Type 1 lookup if (font == null || !font.FullName.Contains(fontName) || font.GsubTable == null) @@ -123,7 +123,7 @@ public void SingleSubstitution_AppliesBeforeLigatures() { try { - var font = OpenTypeFonts.GetFontData(FontFolders, fontName, FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont(fontName); if (font == null || !font.FullName .Contains(fontName) || font.GsubTable == null) continue; diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs index 7d41a06c2..b759947ee 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs @@ -14,7 +14,7 @@ Date Author Change using EPPlus.Fonts.OpenType.Tables.Gpos.Data.Lookups.LookupType2; using EPPlus.Fonts.OpenType.TextShaping; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Diagnostics; @@ -31,7 +31,7 @@ public class TextShaperTests : FontTestBase public void Shape_EmptyString_ReturnsEmptyResult() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -48,7 +48,7 @@ public void Shape_EmptyString_ReturnsEmptyResult() public void Shape_NullString_ReturnsEmptyResult() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -64,7 +64,7 @@ public void Shape_NullString_ReturnsEmptyResult() public void Shape_SingleCharacter_ReturnsOneGlyph() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -82,7 +82,7 @@ public void Shape_SingleCharacter_ReturnsOneGlyph() public void Shape_SimpleWord_ReturnsCorrectGlyphCount() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -99,7 +99,7 @@ public void Shape_SimpleWord_ReturnsCorrectGlyphCount() public void Shape_WithSpace_IncludesSpaceGlyph() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -118,7 +118,7 @@ public void Shape_WithSpace_IncludesSpaceGlyph() public void Shape_WithKerning_ReducesWidth() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); // Act @@ -134,7 +134,7 @@ public void Shape_WithKerning_ReducesWidth() public void Debug_GposKerningFormat() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); Assert.IsNotNull(font.GposTable, "Should have GPOS"); @@ -177,7 +177,7 @@ public void Debug_GposKerningFormat() public void Shape_AVPair_HasNegativeKerning() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -197,7 +197,7 @@ public void Shape_AVPair_HasNegativeKerning() public void Shape_FastOption_StillAppliesKerning() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -217,7 +217,7 @@ public void Shape_FastOption_StillAppliesKerning() public void MeasureText_ReturnsPositiveWidth() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); // Act @@ -231,7 +231,7 @@ public void MeasureText_ReturnsPositiveWidth() public void MeasureTextInPoints_ReturnsReasonableValue() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); // Act @@ -246,7 +246,7 @@ public void MeasureTextInPoints_ReturnsReasonableValue() public void MeasureTextInPixels_ScalesWithDpi() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); // Act @@ -262,7 +262,7 @@ public void MeasureTextInPixels_ScalesWithDpi() public void MeasureText_LargerFontSize_LargerWidth() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); // Act @@ -282,7 +282,7 @@ public void MeasureText_LargerFontSize_LargerWidth() public void Shape_GlyphsHaveClusterIndices() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -298,7 +298,7 @@ public void Shape_GlyphsHaveClusterIndices() public void Shape_GlyphsHaveCharCount() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -315,7 +315,7 @@ public void Shape_GlyphsHaveCharCount() public void Shape_GlyphsHaveValidIds() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -337,7 +337,7 @@ public void Shape_GlyphsHaveValidIds() public void ShapeLines_SingleLine_ReturnsOneElement() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -352,7 +352,7 @@ public void ShapeLines_SingleLine_ReturnsOneElement() public void ShapeLines_TwoLinesWithLF_ReturnsTwoElements() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); // Act @@ -368,7 +368,7 @@ public void ShapeLines_TwoLinesWithLF_ReturnsTwoElements() public void ShapeLines_TwoLinesWithCRLF_ReturnsTwoElements() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); // Act @@ -384,7 +384,7 @@ public void ShapeLines_TwoLinesWithCRLF_ReturnsTwoElements() public void ShapeLines_EmptyLine_PreservesEmptyLine() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var shaper = new TextShaper(font); // Act @@ -401,7 +401,7 @@ public void ShapeLines_EmptyLine_PreservesEmptyLine() public void MeasureLines_SingleLine_MatchesSingleMeasurement() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -417,7 +417,7 @@ public void MeasureLines_SingleLine_MatchesSingleMeasurement() public void MeasureLines_TwoLines_WidthIsMaxOfBoth() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -434,7 +434,7 @@ public void MeasureLines_TwoLines_WidthIsMaxOfBoth() public void MeasureLines_TwoLines_HeightIsDoubleLineHeight() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -454,7 +454,7 @@ public void MeasureLines_TwoLines_HeightIsDoubleLineHeight() public void GetLineHeightInPoints_ReturnsPositiveValue() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -470,7 +470,7 @@ public void GetLineHeightInPoints_ReturnsPositiveValue() public void GetFontHeightInPoints_ReturnsPositiveValue() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -486,7 +486,7 @@ public void GetFontHeightInPoints_ReturnsPositiveValue() public void GetLineHeight_IsGreaterThanFontHeight() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -502,7 +502,7 @@ public void GetLineHeight_IsGreaterThanFontHeight() public void GetLineHeight_ScalesWithFontSize() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -522,7 +522,7 @@ public void GetLineHeight_ScalesWithFontSize() public void ShapedText_GetWidthInPoints_MatchesMeasureText() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -538,7 +538,7 @@ public void ShapedText_GetWidthInPoints_MatchesMeasureText() public void ShapedText_GetWidthInPixels_MatchesMeasureText() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -558,7 +558,7 @@ public void ShapedText_GetWidthInPixels_MatchesMeasureText() public void Shape_OnlySpaces_ReturnsGlyphs() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -573,7 +573,7 @@ public void Shape_OnlySpaces_ReturnsGlyphs() public void Shape_SpecialCharacters_HandlesGracefully() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -588,7 +588,7 @@ public void Shape_SpecialCharacters_HandlesGracefully() public void Shape_Numbers_ReturnsCorrectGlyphs() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -606,7 +606,7 @@ public void Shape_Numbers_ReturnsCorrectGlyphs() public void Shape_FiLigature_CombinesTwoGlyphs() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -622,7 +622,7 @@ public void Shape_FiLigature_CombinesTwoGlyphs() public void Shape_Office_HasFfiLigature() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -637,7 +637,7 @@ public void Shape_Office_HasFfiLigature() public void Shape_Ligature_PreservesClusterIndex() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -656,7 +656,7 @@ public void Shape_Ligature_PreservesClusterIndex() public void Shape_DecomposedUnicode_PositionsAccent() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -685,7 +685,7 @@ public void Shape_DecomposedUnicode_PositionsAccent() public void Shape_PrecomposedVsDecomposed_SimilarWidth() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -709,7 +709,7 @@ public void Shape_PrecomposedVsDecomposed_SimilarWidth() public void Shape_SourceSans3_SingleMark_PositionsCorrectly() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "SourceSans3", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("SourceSans3"); var shaper = new TextShaper(font); // Act - Single combining mark @@ -738,7 +738,7 @@ public void Shape_SourceSans3_SingleMark_PositionsCorrectly() public void Shape_Cafe_HandlesDecomposed() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "SourceSans3", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("SourceSans3"); var shaper = new TextShaper(font); // Act - "café" with decomposed é @@ -759,7 +759,7 @@ public void Shape_Cafe_HandlesDecomposed() [TestMethod] public void Debug_OpenSans_MarkFeature() { - var font = OpenTypeFonts.GetFontData(FontFolders, "OpenSans", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("OpenSans", FontSubFamily.Regular); foreach (var featureRecord in font.GposTable.FeatureList.FeatureRecords) { @@ -808,7 +808,7 @@ public void Discovery_CheckFontsForSingleAdjustment() { try { - var font = OpenTypeFonts.GetFontData(FontFolders, fontName, subFamily); + var font = OpenTypeFonts.LoadFont(fontName, subFamily); if (font.GposTable == null) { diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs index 5a2653445..55ad5d0f4 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs @@ -34,7 +34,7 @@ public static void ClassInitialize(TestContext ctx) public void ShapeVertical_CjkText_ReturnsOneGlyphPerCharacter() { // Arrange - BIZ UDGothic has vmtx and is a CJK font - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); // Act @@ -49,7 +49,7 @@ public void ShapeVertical_CjkText_ReturnsOneGlyphPerCharacter() public void ShapeVertical_CjkText_GlyphsHavePositiveYAdvance() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); // Act @@ -67,7 +67,7 @@ public void ShapeVertical_CjkText_GlyphsHavePositiveYAdvance() public void ShapeVertical_CjkText_TotalAdvanceHeightIsPositive() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); // Act @@ -82,7 +82,7 @@ public void ShapeVertical_CjkText_TotalAdvanceHeightIsPositive() public void ShapeVertical_EmptyString_ReturnsEmptyGlyphArray() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); // Act @@ -98,7 +98,7 @@ public void ShapeVertical_EmptyString_ReturnsEmptyGlyphArray() public void ShapeVertical_ClusterIndexMatchesCharacterPosition() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); var text = "ABC"; @@ -122,7 +122,7 @@ public void ShapeVertical_ClusterIndexMatchesCharacterPosition() public void ShapeLightVertical_CjkText_ReturnsOneEntryPerCharacter() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); // Act @@ -137,7 +137,7 @@ public void ShapeLightVertical_CjkText_ReturnsOneEntryPerCharacter() public void ShapeLightVertical_CjkText_YAdvanceMatchesShapeVertical() { // Arrange - ShapeLightVertical should produce identical YAdvance values to ShapeVertical - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); var text = "東京"; @@ -158,7 +158,7 @@ public void ShapeLightVertical_CjkText_YAdvanceMatchesShapeVertical() public void ShapeLightVertical_EmptyString_ReturnsEmptyArray() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); // Act @@ -178,7 +178,7 @@ public void ShapeLightVertical_EmptyString_ReturnsEmptyArray() public void ShapeVertical_FontWithoutVmtx_FallsBackToHmtxAdvanceWidth() { // Arrange - Calibri has no vmtx table, fallback to hmtx should kick in - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "Roboto", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); Assert.IsNull(font.VmtxTable, "Roboto should not have a vmtx table"); @@ -200,7 +200,7 @@ public void ShapeVertical_FontWithoutVmtx_FallsBackToHmtxAdvanceWidth() public void ShapeVertical_FontWithoutVmtx_YAdvanceMatchesHmtxAdvanceWidth() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "Roboto", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); // Act @@ -221,7 +221,7 @@ public void ShapeVertical_FontWithoutVmtx_YAdvanceMatchesHmtxAdvanceWidth() public void ShapeVertical_OriginalTextIsPreserved() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); var text = "日本語テスト"; @@ -236,7 +236,7 @@ public void ShapeVertical_OriginalTextIsPreserved() public void ShapeVertical_PrimaryFontGlyphs_HaveFontIdZero() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); // Act @@ -258,7 +258,7 @@ public void ShapeVertical_PrimaryFontGlyphs_HaveFontIdZero() public void ShapeVertical_SurrogatePair_ProducesOneGlyphWithCharCountTwo() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); // U+20B9F 𠺟 - a CJK unified ideograph extension B character (surrogate pair in UTF-16) @@ -281,7 +281,7 @@ public void ShapeVertical_SurrogatePair_ProducesOneGlyphWithCharCountTwo() public void ShapeLightVertical_SurrogatePair_ProducesOneEntryWithCharCountTwo() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); var text = "\uD842\uDF9F"; @@ -303,7 +303,7 @@ public void ShapeLightVertical_SurrogatePair_ProducesOneEntryWithCharCountTwo() public void ShapeVertical_CjkText_GlyphsHavePositiveAdvanceWidth() { // Arrange - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); // Act @@ -322,7 +322,7 @@ public void ShapeVertical_AdvanceWidthMatchesHmtxAdvanceWidth() { // Arrange - AdvanceWidth on VerticalShapedGlyph must equal hmtx advanceWidth // since centering calculations depend on this value being accurate - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "BIZ UDGothic", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(font); // Act @@ -343,7 +343,7 @@ public void ShapeVertical_FontWithoutVmtx_AdvanceWidthMatchesHmtxAdvanceWidth() { // Arrange - Roboto has no vmtx table, both YAdvance and AdvanceWidth // should fall back to hmtx and be equal to each other - var font = OpenTypeFonts.GetFontDataOpen(FontFolders, "Roboto", FontSubFamily.Regular, true); + var font = OpenTypeFonts.LoadFont("Roboto"); var shaper = new TextShaper(font); Assert.IsNull(font.VmtxTable, "Roboto should not have a vmtx table"); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Validation/CmapTableValidationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Validation/CmapTableValidationTests.cs index 356024472..6ab526979 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Validation/CmapTableValidationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Validation/CmapTableValidationTests.cs @@ -1,6 +1,7 @@ using EPPlus.Fonts.OpenType.FontValidation; using EPPlus.Fonts.OpenType.Tables.Cmap; using EPPlus.Fonts.OpenType.Tests.Helpers; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Linq; @@ -22,7 +23,7 @@ public static void Initialize(TestContext testContext) [TestMethod] public void CmapTableValidation_Test() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var validator = new CmapTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.CmapTable, context); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Validation/CoreTableValidationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Validation/CoreTableValidationTests.cs index b719992fc..a22cd26cd 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Validation/CoreTableValidationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Validation/CoreTableValidationTests.cs @@ -6,6 +6,7 @@ using EPPlus.Fonts.OpenType.Tables.Os2; using EPPlus.Fonts.OpenType.Tables.Post; using EPPlus.Fonts.OpenType.Tests.Helpers; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Linq; @@ -21,7 +22,7 @@ public class CoreTableValidationTests : FontTestBase [TestMethod] public void HeadTable_Validation_ShouldPass() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var validator = new HeadTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.HeadTable, context); @@ -32,7 +33,7 @@ public void HeadTable_Validation_ShouldPass() [TestMethod] public void MaxpTable_Validation_ShouldPass() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var validator = new MaxpTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.MaxpTable, context); @@ -42,7 +43,7 @@ public void MaxpTable_Validation_ShouldPass() [TestMethod] public void HheaTable_Validation_ShouldPass() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var validator = new HheaTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.HheaTable, context); @@ -52,7 +53,7 @@ public void HheaTable_Validation_ShouldPass() [TestMethod] public void NameTable_Validation_ShouldPass() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var validator = new NameTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.NameTable, context); @@ -62,7 +63,7 @@ public void NameTable_Validation_ShouldPass() [TestMethod] public void Os2Table_Validation_ShouldPass() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var validator = new Os2TableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.Os2Table, context); @@ -72,7 +73,7 @@ public void Os2Table_Validation_ShouldPass() [TestMethod] public void PostTable_Validation_ShouldPass() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); var validator = new PostTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.PostTable, context); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Validation/EntireFontValidationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Validation/EntireFontValidationTests.cs index 6b7d7d8d6..9b2ed9a09 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Validation/EntireFontValidationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Validation/EntireFontValidationTests.cs @@ -11,7 +11,7 @@ public class EntireFontValidationTests : FontTestBase [TestMethod] public void ValidateEntireFont() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto"); var report = font.ValidateFont(FontValidationSeverity.Error); Assert.IsTrue(report.IsValid); } diff --git a/src/EPPlus.Fonts.OpenType.Tests/Validation/GlyphTableValidationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Validation/GlyphTableValidationTests.cs index 23bcbb64c..06d4ea879 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Validation/GlyphTableValidationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Validation/GlyphTableValidationTests.cs @@ -3,6 +3,7 @@ using EPPlus.Fonts.OpenType.Tables.Hmtx; using EPPlus.Fonts.OpenType.Tables.Loca; using EPPlus.Fonts.OpenType.Tests.Helpers; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Linq; @@ -18,7 +19,7 @@ public class GlyphTableValidationTests : FontTestBase [TestMethod] public void LocaTableValidation_Test() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto"); var validator = new LocaTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.LocaTable, context); @@ -28,7 +29,7 @@ public void LocaTableValidation_Test() [TestMethod] public void HmtxTableValidation_Test() { - var font = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont("Roboto"); var validator = new HmtxTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.HmtxTable, context); @@ -42,7 +43,7 @@ public void HmtxTableValidation_Test() [DataRow("Mulish", FontSubFamily.Regular)] public void GlyfTableValidation_Test(string fontName, FontSubFamily subFamily) { - var font = OpenTypeFonts.GetFontData(FontFolders, fontName, subFamily, false, true); + var font = OpenTypeFonts.LoadFont(fontName, subFamily); var validator = new GlyfTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.GlyfTable, context); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Validation/GsubTableValidationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Validation/GsubTableValidationTests.cs index bfe6c285a..4c37fee53 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Validation/GsubTableValidationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Validation/GsubTableValidationTests.cs @@ -16,7 +16,7 @@ public class GsubTableValidationTests : FontTestBase [DataRow("NotoEmoji")] public void GsubTableValidation_Test(string fontName) { - var font = OpenTypeFonts.GetFontData(FontFolders, fontName, FontSubFamily.Regular, false, true); + var font = OpenTypeFonts.LoadFont(fontName); var validator = new GsubTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.GsubTable, context); diff --git a/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs b/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs index 5997ef392..6084e8580 100644 --- a/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs +++ b/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs @@ -9,6 +9,7 @@ This software is licensed under PolyForm Noncommercial License 1.0.0 Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 + 02/24/2026 EPPlus Software AB Dynamic fallback chain with lazy loading *************************************************************************************************/ using System; using System.Collections.Generic; @@ -16,12 +17,14 @@ Date Author Change namespace EPPlus.Fonts.OpenType { /// - /// Default font provider with automatic Noto Emoji fallback. + /// Default font provider with automatic embedded fallback fonts. + /// Fallback fonts are lazy-loaded on first use (thread-safe). + /// Default chain: Primary → Noto Emoji → Noto Sans Math. /// public class DefaultFontProvider : IFontProvider { private readonly OpenTypeFont _primaryFont; - private OpenTypeFont _emojiFont; + private readonly List _fallbackFonts; private readonly object _lock = new object(); public OpenTypeFont PrimaryFont @@ -30,7 +33,8 @@ public OpenTypeFont PrimaryFont } /// - /// Creates a font provider with automatic emoji fallback. + /// Creates a font provider with automatic embedded fallbacks. + /// Default fallback chain: Noto Emoji → Noto Sans Math. /// /// The user's primary font public DefaultFontProvider(OpenTypeFont primaryFont) @@ -39,25 +43,11 @@ public DefaultFontProvider(OpenTypeFont primaryFont) throw new ArgumentNullException("primaryFont"); _primaryFont = primaryFont; - } - - /// - /// Gets the emoji font, loading it on first access (lazy loading). - /// Thread-safe for .NET 3.5 compatibility. - /// - private OpenTypeFont GetEmojiFontLazy() - { - if (_emojiFont == null) + _fallbackFonts = new List { - lock (_lock) - { - if (_emojiFont == null) - { - _emojiFont = EmbeddedFonts.LoadNotoEmoji(); - } - } - } - return _emojiFont; + new LazyFallbackFont(EmbeddedFonts.LoadNotoEmoji), + new LazyFallbackFont(EmbeddedFonts.LoadNotoMath) + }; } public bool TryGetGlyphFont(uint codePoint, out OpenTypeFont font, out ushort glyphId) @@ -69,12 +59,18 @@ public bool TryGetGlyphFont(uint codePoint, out OpenTypeFont font, out ushort gl return true; } - // Fallback to embedded Noto Emoji (lazy-loaded) - var emojiFont = GetEmojiFontLazy(); - if (emojiFont.CmapTable.TryGetGlyphId(codePoint, out glyphId)) + // Try fallback fonts in order (lazy-loaded) + lock (_lock) { - font = emojiFont; - return true; + foreach (var fallback in _fallbackFonts) + { + var fallbackFont = fallback.Font; + if (fallbackFont.CmapTable.TryGetGlyphId(codePoint, out glyphId)) + { + font = fallbackFont; + return true; + } + } } // Not found - return primary with .notdef @@ -87,11 +83,60 @@ public IEnumerable GetAllFonts() { yield return _primaryFont; - // Only include emoji font if it was actually loaded - if (_emojiFont != null) + lock (_lock) { - yield return _emojiFont; + foreach (var fallback in _fallbackFonts) + { + if (fallback.IsLoaded) + { + yield return fallback.Font; + } + } + } + } + + /// + /// Wraps a font loader delegate with lazy, thread-safe initialization. + /// + private class LazyFallbackFont + { + private readonly Func _loader; + private OpenTypeFont _font; + private readonly object _lock = new object(); + + internal LazyFallbackFont(Func loader) + { + _loader = loader; + } + + /// + /// Gets whether the font has been loaded yet. + /// + internal bool IsLoaded + { + get { return _font != null; } + } + + /// + /// Gets the font, loading it on first access. + /// + internal OpenTypeFont Font + { + get + { + if (_font == null) + { + lock (_lock) + { + if (_font == null) + { + _font = _loader(); + } + } + } + return _font; + } } } } -} +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj b/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj index 72eb6b59f..76c52dc79 100644 --- a/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj +++ b/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj @@ -61,10 +61,18 @@ + + + + + + + + PreserveNewest diff --git a/src/EPPlus.Fonts.OpenType/EmbeddedFonts.cs b/src/EPPlus.Fonts.OpenType/EmbeddedFonts.cs index ac9dc5b29..7c6198b17 100644 --- a/src/EPPlus.Fonts.OpenType/EmbeddedFonts.cs +++ b/src/EPPlus.Fonts.OpenType/EmbeddedFonts.cs @@ -1,4 +1,5 @@ using EPPlus.Fonts.OpenType.Scanner; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.IO; @@ -31,6 +32,23 @@ internal static OpenTypeFont LoadNotoMath() return LoadCached("NotoSansMath-Regular.ttf"); } + internal static OpenTypeFont LoadArchivoNarrow(FontSubFamily subFamily) + { + if (subFamily == FontSubFamily.Italic) + { + return LoadCached("ArchivoNarrow-Italic.ttf"); + } + else if (subFamily == FontSubFamily.Bold) + { + return LoadCached("ArchivoNarrow-Bold.ttf"); + } + else if(subFamily == FontSubFamily.BoldItalic) + { + return LoadCached("ArchivoNarrow-BoldItalic.ttf"); + } + return LoadCached("ArchivoNarrow-Regular.ttf"); + } + private static OpenTypeFont LoadCached(string resourceName) { lock (_lock) @@ -50,7 +68,7 @@ private static OpenTypeFont LoadCached(string resourceName) "This is a bug in EPPlus.Fonts.OpenType - please report it."); } - font = OpenTypeFonts.GetFromBytes(bytes: ReadStreamFully(stream), FontFormat.Ttf); + font = OpenTypeFonts.GetFromBytes(bytes: ReadStreamFully(stream)); _cache[resourceName] = font; return font; } diff --git a/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs b/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs new file mode 100644 index 000000000..f4238a9f4 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs @@ -0,0 +1,97 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 02/27/2026 EPPlus Software AB Replaces FontResolutionConfig + *************************************************************************************************/ +using OfficeOpenXml.Interfaces.Fonts; +using System; +using System.Collections.Generic; + +namespace EPPlus.Fonts.OpenType.FontResolver +{ + /// + /// Concrete implementation of . + /// Managed exclusively by — not instantiated by user code. + /// + internal class EpplusFontConfiguration : IEpplusFontConfiguration + { + private readonly Dictionary _fallbacks = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Internal events consumed by OpenTypeFonts in the same assembly. + internal event Action OnReset; + internal event Action OnSetFontResolver; + + // ----------------------------------------------------------------------------------------- + // IEpplusFontConfiguration + // ----------------------------------------------------------------------------------------- + + /// + public IEpplusFontConfiguration AddFallback(string primaryFont, params string[] fallbacks) + { + if (string.IsNullOrEmpty(primaryFont)) + throw new ArgumentNullException("primaryFont"); + if (fallbacks == null || fallbacks.Length == 0) + throw new ArgumentException("At least one fallback must be specified.", "fallbacks"); + + // Additive: merge with any existing fallbacks for this font. + string[] existing; + if (_fallbacks.TryGetValue(primaryFont, out existing)) + { + var merged = new string[existing.Length + fallbacks.Length]; + existing.CopyTo(merged, 0); + fallbacks.CopyTo(merged, existing.Length); + _fallbacks[primaryFont] = merged; + } + else + { + _fallbacks[primaryFont] = fallbacks; + } + + return this; + } + + /// + public IEpplusFontConfiguration SetFontResolver(IFontResolver resolver) + { + if (resolver == null) + throw new ArgumentNullException("resolver"); + + if (OnSetFontResolver != null) + OnSetFontResolver(resolver); + + return this; + } + + /// + public IEpplusFontConfiguration Reset() + { + _fallbacks.Clear(); + + if (OnReset != null) + OnReset(); + + return this; + } + + // ----------------------------------------------------------------------------------------- + // Internal API — consumed by DefaultFontResolver in the same assembly. + // ----------------------------------------------------------------------------------------- + + /// + /// Returns the fallback chain for the given font name, or null if none is configured. + /// + internal string[] GetFallbacks(string fontName) + { + string[] result; + return _fallbacks.TryGetValue(fontName, out result) ? result : null; + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/FontCache/OpenTypeFontCache.cs b/src/EPPlus.Fonts.OpenType/FontCache/OpenTypeFontCache.cs index a6343243a..878d2819c 100644 --- a/src/EPPlus.Fonts.OpenType/FontCache/OpenTypeFontCache.cs +++ b/src/EPPlus.Fonts.OpenType/FontCache/OpenTypeFontCache.cs @@ -1,4 +1,17 @@ -using System; +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 + 02/26/2026 EPPlus Software AB Added overloads accepting prebuilt cache keys + *************************************************************************************************/ +using System; using System.Collections.Generic; using System.Threading; @@ -18,30 +31,14 @@ internal static void Clear() } } - /// - /// Builds a cache key from family name and subfamily. - /// THE SUBFAMILY ENUM SHOULD ALWAYS BE INPUT PARAMETER. - /// Fonts can name themselves however they want, but we map other values in the font to the subfamily. - /// Therefore never change it to e.g. a string input-parameter - /// and never use e.g. font.GetEnglishSubfamily name as this could create mismatches. - /// - /// Font family name - /// MUST STAY as FontSubFamily enum - /// Cache key string - static string BuildCacheKey(string familyName, FontSubFamily subFamily) - { - return string.Format("{0}-{1}", familyName, subFamily.ToString()); - } - /// /// Checks if a font is present in the cache (loaded or loading). /// - public static bool Contains(string familyName, FontSubFamily subFamily) + public static bool Contains(string cacheKey) { - var key = BuildCacheKey(familyName, subFamily); lock (_syncRoot) { - return _cache.ContainsKey(key); + return _cache.ContainsKey(cacheKey); } } @@ -49,14 +46,13 @@ public static bool Contains(string familyName, FontSubFamily subFamily) /// Creates a placeholder entry to indicate that font loading has begun. /// This prevents multiple threads from starting to load the same font. /// - public static void BeginCache(string familyName, FontSubFamily subFamily) + public static void BeginCache(string cacheKey) { lock (_syncRoot) { - var key = BuildCacheKey(familyName, subFamily); - if (!_cache.ContainsKey(key)) + if (!_cache.ContainsKey(cacheKey)) { - _cache[key] = new CachedOpenTypeFont() + _cache[cacheKey] = new CachedOpenTypeFont() { IsLoaded = false, Font = null @@ -66,24 +62,21 @@ public static void BeginCache(string familyName, FontSubFamily subFamily) } /// - /// Adds or updates a fully loaded font in the cache. + /// Adds or updates a fully loaded font using a prebuilt cache key. /// Signals all waiting threads that the font is now available. /// - public static void AddToCache(OpenTypeFont font, string familyName, FontSubFamily subFamily) + public static void AddToCache(OpenTypeFont font, string cacheKey) { lock (_syncRoot) { - var key = BuildCacheKey(familyName, subFamily); - - // Update existing entry OR create new one - if (_cache.ContainsKey(key)) + if (_cache.ContainsKey(cacheKey)) { - _cache[key].Font = font; - _cache[key].IsLoaded = true; + _cache[cacheKey].Font = font; + _cache[cacheKey].IsLoaded = true; } else { - _cache[key] = new CachedOpenTypeFont + _cache[cacheKey] = new CachedOpenTypeFont { Font = font, IsLoaded = true @@ -96,24 +89,19 @@ public static void AddToCache(OpenTypeFont font, string familyName, FontSubFamil } /// - /// Retrieves a font from cache, waiting if it's currently being loaded by another thread. + /// Retrieves a font from cache using a prebuilt cache key. + /// Waits if the font is currently being loaded by another thread. /// Returns null if font is not in cache or if timeout occurs while waiting. /// - /// Font family name - /// Font subfamily - /// Cached font entry or null if not available - public static CachedOpenTypeFont GetFromCache(string familyName, FontSubFamily subFamily) + public static CachedOpenTypeFont GetFromCache(string cacheKey) { - var key = BuildCacheKey(familyName, subFamily); lock (_syncRoot) { - if (_cache.TryGetValue(key, out var cached)) + if (_cache.TryGetValue(cacheKey, out var cached)) { // If already loaded, return immediately if (cached.IsLoaded && cached.Font != null) - { return cached; - } // Wait for another thread to finish loading var timeout = TimeSpan.FromSeconds(2); @@ -123,22 +111,16 @@ public static CachedOpenTypeFont GetFromCache(string familyName, FontSubFamily s { // CRITICAL: Retrieve from dictionary again after Wait()! // The 'cached' reference may be stale after another thread updates the cache - if (_cache.TryGetValue(key, out cached) && cached.IsLoaded && cached.Font != null) - { + if (_cache.TryGetValue(cacheKey, out cached) && cached.IsLoaded && cached.Font != null) return cached; - } - // Wait and release lock temporarily Monitor.Wait(_syncRoot, TimeSpan.FromMilliseconds(50)); } - // Timeout occurred - one final check - if (_cache.TryGetValue(key, out cached) && cached.IsLoaded && cached.Font != null) - { + // One final check after timeout + if (_cache.TryGetValue(cacheKey, out cached) && cached.IsLoaded && cached.Font != null) return cached; - } - // Timeout without result return null; } return null; diff --git a/src/EPPlus.Fonts.OpenType/FontLocalization/MacintoshLanguageMappings.cs b/src/EPPlus.Fonts.OpenType/FontLocalization/MacintoshLanguageMappings.cs index 9046dafe4..1f9525c69 100644 --- a/src/EPPlus.Fonts.OpenType/FontLocalization/MacintoshLanguageMappings.cs +++ b/src/EPPlus.Fonts.OpenType/FontLocalization/MacintoshLanguageMappings.cs @@ -9,21 +9,25 @@ This software is licensed under PolyForm Noncommercial License 1.0.0 Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 + 02/27/2026 EPPlus Software AB Thread-safe initialization *************************************************************************************************/ using System.Collections.Generic; -using System.Linq; namespace EPPlus.Fonts.OpenType.FontLocalization { internal static class MacintoshLanguageMappings { - private static IDictionary _mappings = new Dictionary(); + private static readonly object _syncRoot = new object(); + private static volatile bool _initialized = false; + private static readonly Dictionary _mappings = new Dictionary(); private static void AddMapping(int hexNumber, Languages language) { + // Called only within lock, no additional lock needed var mapping = LanguageMapping.Create(hexNumber, language); _mappings.Add(mapping.code, mapping); } + private static void CreateMappings() { AddMapping(0, Languages.English); @@ -147,12 +151,20 @@ public static IDictionary Mappings { get { - if (_mappings.Count() == 0) + // Double-checked locking pattern, compatible with .NET 3.5+ + if (!_initialized) { - CreateMappings(); + lock (_syncRoot) + { + if (!_initialized) + { + CreateMappings(); + _initialized = true; + } + } } return _mappings; } } } -} +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/FontLocalization/WindowsLanguageMappings.cs b/src/EPPlus.Fonts.OpenType/FontLocalization/WindowsLanguageMappings.cs index 1c2c2ecf4..7c98224f3 100644 --- a/src/EPPlus.Fonts.OpenType/FontLocalization/WindowsLanguageMappings.cs +++ b/src/EPPlus.Fonts.OpenType/FontLocalization/WindowsLanguageMappings.cs @@ -9,33 +9,34 @@ This software is licensed under PolyForm Noncommercial License 1.0.0 Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 + 02/27/2026 EPPlus Software AB Thread-safe initialization *************************************************************************************************/ using System.Collections.Generic; -using System.Linq; namespace EPPlus.Fonts.OpenType.FontLocalization { internal static class WindowsLanguageMappings { - private static IDictionary _mappings = new Dictionary(); + private static readonly object _syncRoot = new object(); + private static volatile bool _initialized = false; + private static readonly Dictionary _mappings = new Dictionary(); + private static void AddMappping(int hexNumber, Languages language) { - lock (_mappings) - { - if (_mappings.ContainsKey(hexNumber)) return; + // Called only within lock during initialization, no additional lock needed + if (_mappings.ContainsKey(hexNumber)) return; - var mapping = LanguageMapping.Create(hexNumber, language); - _mappings.Add(mapping.code, mapping); - } + var mapping = LanguageMapping.Create(hexNumber, language); + _mappings.Add(mapping.code, mapping); } + private static void CreateMappings() { - AddMappping(0x0436, Languages.Afrikaans); AddMappping(0x041C, Languages.Albanian); AddMappping(0x0484, Languages.Alsatian); AddMappping(0x045E, Languages.Amharic); - + AddMappping(0x1401, Languages.Arabic); AddMappping(0x3C01, Languages.Arabic); AddMappping(0x0C01, Languages.Arabic); @@ -47,12 +48,14 @@ private static void CreateMappings() AddMappping(0x1801, Languages.Arabic); AddMappping(0x2001, Languages.Arabic); AddMappping(0x4001, Languages.Arabic); - AddMappping(0x0401, Languages.Arabic); AddMappping(0x2801, Languages.Arabic); AddMappping(0x1C01, Languages.Arabic); AddMappping(0x3801, Languages.Arabic); AddMappping(0x2401, Languages.Arabic); - + AddMappping(0x0401, Languages.Arabic); + AddMappping(0x2801, Languages.Arabic); + AddMappping(0x1401, Languages.Arabic); + AddMappping(0x0429, Languages.Dari); AddMappping(0x042B, Languages.Armenian); AddMappping(0x044D, Languages.Assamese); AddMappping(0x082C, Languages.Azeri_Cyrillic); @@ -64,24 +67,23 @@ private static void CreateMappings() AddMappping(0x0445, Languages.Bengali); AddMappping(0x201A, Languages.Bosnian_Cyrillic); AddMappping(0x141A, Languages.Bosnian_Latin); - AddMappping(0x047E, Languages.Breton); AddMappping(0x0402, Languages.Bulgarian); AddMappping(0x0403, Languages.Catalan); - AddMappping(0x1404, Languages.Chinese); - AddMappping(0x0804, Languages.Chinese); - AddMappping(0x1004, Languages.Chinese); - AddMappping(0x0404, Languages.Chinese); + AddMappping(0x0C04, Languages.Chinese_Traditional); + AddMappping(0x1404, Languages.Chinese_Traditional); + AddMappping(0x0804, Languages.Chinese_Simplified); + AddMappping(0x1004, Languages.Chinese_Simplified); + AddMappping(0x0404, Languages.Chinese_Traditional); AddMappping(0x0483, Languages.Corsican); AddMappping(0x041A, Languages.Croatian); - AddMappping(0x101A, Languages.Croatian_Latin); - AddMappping(0x0405, Languages.Czech ); + AddMappping(0x0405, Languages.Czech); AddMappping(0x0406, Languages.Danish); AddMappping(0x048C, Languages.Dari); AddMappping(0x0465, Languages.Divehi); - AddMappping(0x0813, Languages.Dutch); AddMappping(0x0413, Languages.Dutch); + AddMappping(0x0813, Languages.Dutch); AddMappping(0x0C09, Languages.English); AddMappping(0x2809, Languages.English); @@ -107,7 +109,7 @@ private static void CreateMappings() AddMappping(0x080C, Languages.French); AddMappping(0x0C0C, Languages.French); AddMappping(0x040C, Languages.French); - AddMappping(0x140c, Languages.French); + AddMappping(0x140C, Languages.French); AddMappping(0x180C, Languages.French); AddMappping(0x100C, Languages.French); AddMappping(0x0462, Languages.Frisian); @@ -258,12 +260,20 @@ public static IDictionary Mappings { get { - if(_mappings.Count() == 0) + // Double-checked locking pattern, compatible with .NET 3.5+ + if (!_initialized) { - CreateMappings(); + lock (_syncRoot) + { + if (!_initialized) + { + CreateMappings(); + _initialized = true; + } + } } return _mappings; } } } -} +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/FontResolver/DefaultFontLocations.cs b/src/EPPlus.Fonts.OpenType/FontResolver/DefaultFontLocations.cs new file mode 100644 index 000000000..636744881 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/FontResolver/DefaultFontLocations.cs @@ -0,0 +1,83 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 + *************************************************************************************************/ +using EPPlus.Fonts.OpenType.Utils.Platform; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.FontResolver +{ + internal static class DefaultFontLocations + { + private static string GetWindowsFolder() + { + var winfolder = @"c:\Windows"; +#if !NET35 + var wf = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + if (!string.IsNullOrEmpty(wf) && Directory.Exists(wf)) return wf; +#endif + var ewf = Environment.GetEnvironmentVariable("WINDIR"); + if (!string.IsNullOrEmpty(ewf) && Directory.Exists(ewf)) + { + winfolder = ewf; + } + return winfolder; + } + + internal static readonly List winFontLocations = new List() + { + Path.Combine(GetWindowsFolder(), "Fonts"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\Windows\\Fonts"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft\\Windows\\Fonts"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\FontCache"), + }; + + internal static readonly List linFontLocations = new List() + { + "/usr/share/fonts", + "/usr/local/share/fonts", + "/usr/share/X11/fonts", + Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? "~", ".fonts"), + Path.Combine(Path.Combine(Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? "~", ".local"), "share"), "fonts"), + }; + + internal static readonly List macFontLocations = new List() + { + "/System/Library/Fonts", + "/Library/Fonts", + "/Network/Library/Fonts", + Path.Combine(Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? "~", "Library"), "Fonts"), + }; + + internal static List GetLocationsCollection(IEnumerable fontDirectories, bool searchSystemDirectories = true) + { + var fontLocations = new List(); + fontLocations.AddRange(fontDirectories ?? Enumerable.Empty()); + + if (searchSystemDirectories) + { + var platform = PlatformUtils.GetPlatform(); + if (platform == PlatformUtils.OperatingSystem.Windows) + fontLocations.AddRange(winFontLocations); + else if (platform == PlatformUtils.OperatingSystem.Mac) + fontLocations.AddRange(macFontLocations); + else + fontLocations.AddRange(linFontLocations); + } + + return fontLocations; + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/FontResolver/DefaultFontResolver.cs b/src/EPPlus.Fonts.OpenType/FontResolver/DefaultFontResolver.cs new file mode 100644 index 000000000..3c5bf0b17 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/FontResolver/DefaultFontResolver.cs @@ -0,0 +1,80 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 + 02/26/2026 EPPlus Software AB Removed caching (moved to OpenTypeFonts) + 02/27/2026 EPPlus Software AB Replaced FontResolutionConfig with EpplusFontConfiguration, added Archivo Narrow fallback + *************************************************************************************************/ +using EPPlus.Fonts.OpenType.Scanner; +using OfficeOpenXml.Interfaces.Fonts; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace EPPlus.Fonts.OpenType.FontResolver +{ + /// + /// Default IFontResolver implementation that resolves fonts from the file system. + /// Searches additional font directories and optionally system font directories. + /// Supports fallback font chains via EpplusFontConfiguration. + /// + internal class DefaultFontResolver : IFontResolver + { + private readonly IEnumerable _fontDirectories; + private readonly bool _searchSystemDirectories; + private readonly EpplusFontConfiguration _config; + + public DefaultFontResolver( + IEnumerable fontDirectories = null, + bool searchSystemDirectories = true, + EpplusFontConfiguration config = null) + { + _fontDirectories = fontDirectories ?? Enumerable.Empty(); + _searchSystemDirectories = searchSystemDirectories; + _config = config; + } + + public byte[] ResolveFont(string fontName, FontSubFamily subFamily) + { + // Try exact match first + var face = FontScannerV2.FindBestMatch( + _fontDirectories, fontName, subFamily, _searchSystemDirectories); + + if (face != null && face.IsExactMatch) + return ReadFontBytes(face.FilePath); + + // No exact match — try user-configured fallback chain + if (_config != null) + { + var fallbacks = _config.GetFallbacks(fontName); + if (fallbacks != null) + { + foreach (var fallbackName in fallbacks) + { + var fallbackFace = FontScannerV2.FindBestMatch( + _fontDirectories, fallbackName, subFamily, _searchSystemDirectories); + + if (fallbackFace != null && fallbackFace.IsExactMatch) + return ReadFontBytes(fallbackFace.FilePath); + } + } + } + + // No match found — fall back to built-in Archivo Narrow. + // Only applies when using DefaultFontResolver (i.e. no custom resolver installed). + return EmbeddedFonts.LoadArchivoNarrow(subFamily).RawData; + } + + private static byte[] ReadFontBytes(string filePath) + { + return File.ReadAllBytes(filePath); + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/FontSubsetBuilder.cs b/src/EPPlus.Fonts.OpenType/FontSubsetBuilder.cs deleted file mode 100644 index aecc0e1d8..000000000 --- a/src/EPPlus.Fonts.OpenType/FontSubsetBuilder.cs +++ /dev/null @@ -1,59 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 - *************************************************************************************************/ -using EPPlus.Fonts.OpenType.Tables.Glyph; -using EPPlus.Fonts.OpenType.Tables.Head; -using EPPlus.Fonts.OpenType.Tables.Loca; -using System.Collections.Generic; - -namespace EPPlus.Fonts.OpenType -{ - public class FontSubsetBuilder - { - private readonly OpenTypeFont _original; - - public FontSubsetBuilder(OpenTypeFont original) - { - _original = original; - } - - public OpenTypeFont BuildSubset(HashSet glyphIds, IEnumerable usedChars) - { - var subsetFont = new OpenTypeFont(_original.Format); - - subsetFont.AddOrReplaceTable(_original.HeadTable.Clone()); - subsetFont.AddOrReplaceTable(_original.MaxpTable.Clone()); - subsetFont.MaxpTable.numGlyphs = (ushort)glyphIds.Count; - - // Build glyf subset - var glyfProcessor = new GlyphSubsetProcessor(_original.GlyfTable); - var glyphSubsetResult = glyfProcessor.CreateSubset(glyphIds); - subsetFont.AddOrReplaceTable(glyphSubsetResult.GlyfTable); - - - var glyfSize = glyphSubsetResult.GlyfTable.GetLength(subsetFont); - var indexToLocFormat = glyfSize < 65536 - ? HeadTable.IndexToLocFormats.Offset16 - : HeadTable.IndexToLocFormats.Offset32; - - // Update HeadTable - subsetFont.HeadTable.IndexToLocFormat = indexToLocFormat; - - // Build Loca-table for the subset - subsetFont.AddOrReplaceTable( - LocaTable.CreateSubset(glyphSubsetResult.LocaOffsets, indexToLocFormat) - ); - - return subsetFont; - } - } -} diff --git a/src/EPPlus.Fonts.OpenType/FontSubsetManager.cs b/src/EPPlus.Fonts.OpenType/FontSubsetManager.cs new file mode 100644 index 000000000..969751703 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/FontSubsetManager.cs @@ -0,0 +1,139 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 02/25/2026 EPPlus Software AB Font subset manager for PDF export + *************************************************************************************************/ +using EPPlus.Fonts.OpenType.Utils; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EPPlus.Fonts.OpenType +{ + /// + /// Prepares subsetted fonts for PDF export by pre-scanning text, + /// distributing code points to the correct font via the fallback chain, + /// and creating minimal subsets of all fonts (including fallbacks). + /// + /// Usage: + /// 1. Create with an IFontProvider (e.g., DefaultFontProvider) + /// 2. Call AddText() for all text that will be rendered (e.g., all cell values) + /// 3. Call CreateSubsettedProvider() to get a new IFontProvider with subsetted fonts + /// 4. Use the returned provider for shaping and PDF rendering + /// + public class FontSubsetManager + { + private readonly IFontProvider _sourceProvider; + + // Code points collected per font (key = original font instance) + private readonly Dictionary> _codePointsByFont = + new Dictionary>(); + + public FontSubsetManager(IFontProvider sourceProvider) + { + if (sourceProvider == null) + throw new ArgumentNullException("sourceProvider"); + + _sourceProvider = sourceProvider; + } + + public FontSubsetManager(OpenTypeFont font) + : this(new DefaultFontProvider(font)) + { + + } + + /// + /// Scans text and distributes each code point to the font that will render it. + /// Call this for every piece of text that will appear in the document. + /// + public void AddText(string text) + { + if (string.IsNullOrEmpty(text)) + return; + + var codePoints = CodePointUtil.ExtractCodePoints(text); + + foreach (var cp in codePoints) + { + OpenTypeFont font; + ushort glyphId; + _sourceProvider.TryGetGlyphFont((uint)cp, out font, out glyphId); + + HashSet fontCodePoints; + if (!_codePointsByFont.TryGetValue(font, out fontCodePoints)) + { + fontCodePoints = new HashSet(); + _codePointsByFont[font] = fontCodePoints; + } + + fontCodePoints.Add(cp); + } + } + + /// + /// Creates a new IFontProvider where all fonts (primary + fallbacks) are subsetted + /// to contain only the glyphs needed for the collected text. + /// Fonts that had no text collected are excluded from the result. + /// + public IFontProvider CreateSubsettedProvider() + { + var primaryFont = _sourceProvider.PrimaryFont; + var allFonts = _sourceProvider.GetAllFonts().ToList(); + + // Subset each font that has collected code points + var subsetMap = new Dictionary(); + + foreach (var kvp in _codePointsByFont) + { + var originalFont = kvp.Key; + var codePoints = kvp.Value; + + if (codePoints.Count == 0) + continue; + + try + { + var chars = CodePointUtil.CodePointsToString(codePoints); + var subset = originalFont.CreateSubset(chars); + subsetMap[originalFont] = subset; + } + catch (Exception ex) + { + // If subsetting fails, use the original font + System.Diagnostics.Debug.WriteLine( + $"Warning: Could not subset '{originalFont.NameTable?.GetFullFontName()}': {ex.Message}"); + subsetMap[originalFont] = originalFont; + } + } + + // Build new provider with subsetted fonts, preserving fallback order + var subsetPrimary = subsetMap.ContainsKey(primaryFont) + ? subsetMap[primaryFont] + : primaryFont; + + var provider = new CustomFontProvider(subsetPrimary); + + // Add fallback fonts in their original order (skip primary) + for (int i = 1; i < allFonts.Count; i++) + { + var originalFallback = allFonts[i]; + + if (subsetMap.ContainsKey(originalFallback)) + { + provider.AddFallback(subsetMap[originalFallback]); + } + // If no code points were collected for this fallback, skip it entirely + } + + return provider; + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/Integration/FragmentPosition.cs b/src/EPPlus.Fonts.OpenType/Integration/FragmentPosition.cs index f20b3cfaa..73c5734d2 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/FragmentPosition.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/FragmentPosition.cs @@ -11,6 +11,7 @@ Date Author Change 01/20/2025 EPPlus Software AB TextLayoutEngine implementation *************************************************************************************************/ using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; namespace EPPlus.Fonts.OpenType.Integration { diff --git a/src/EPPlus.Fonts.OpenType/Integration/OpenTypeFontTextMeasurer.cs b/src/EPPlus.Fonts.OpenType/Integration/OpenTypeFontTextMeasurer.cs index 7852b15ae..5aa8dda49 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/OpenTypeFontTextMeasurer.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/OpenTypeFontTextMeasurer.cs @@ -12,6 +12,7 @@ Date Author Change *************************************************************************************************/ using EPPlus.Fonts.OpenType.TextShaping; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System; namespace EPPlus.Fonts.OpenType.Integration diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs index 3ac45fba4..6c173acad 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs @@ -11,6 +11,7 @@ Date Author Change 01/20/2025 EPPlus Software AB TextLayoutEngine implementation *************************************************************************************************/ using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; namespace EPPlus.Fonts.OpenType.Integration { diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.Helpers.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.Helpers.cs index a650eb113..aefc23b45 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.Helpers.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.Helpers.cs @@ -1,9 +1,9 @@ -using OfficeOpenXml.Interfaces.Drawing.Text; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using EPPlus.Fonts.OpenType.Utilities; +using OfficeOpenXml.Interfaces.Fonts; namespace EPPlus.Fonts.OpenType.Integration diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 78498120b..060b105e0 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -14,7 +14,7 @@ Date Author Change 02/23/2026 EPPlus Software AB Performance fix: Shape() → ShapeLight() in ProcessFragment *************************************************************************************************/ using EPPlus.Fonts.OpenType.Utilities; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Text; diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs index cd799dc75..af04bd38c 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs @@ -17,6 +17,7 @@ Date Author Change using EPPlus.Fonts.OpenType.TextShaping; using EPPlus.Fonts.OpenType.Utilities; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Linq; @@ -219,10 +220,10 @@ private ITextShaper GetShaperForFont(MeasurementFont font) return cachedShaper; } - var openTypeFont = OpenTypeFonts.GetFontData( - fontDirectories: _fontDirectories, + var openTypeFont = OpenTypeFonts.LoadFont( fontName: font.FontFamily, subFamily: GetFontSubFamily(font.Style), + fontDirectories: _fontDirectories, searchSystemDirectories: _searchSystemDirectories ); diff --git a/src/EPPlus.Fonts.OpenType/OpenTypeFont.cs b/src/EPPlus.Fonts.OpenType/OpenTypeFont.cs index c81d1b848..6fdb0e651 100644 --- a/src/EPPlus.Fonts.OpenType/OpenTypeFont.cs +++ b/src/EPPlus.Fonts.OpenType/OpenTypeFont.cs @@ -32,6 +32,7 @@ Date Author Change using EPPlus.Fonts.OpenType.Utils; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace EPPlus.Fonts.OpenType @@ -39,6 +40,7 @@ namespace EPPlus.Fonts.OpenType /// /// Base class for open-type fonts /// + [DebuggerDisplay("{FullName}, IsSubset: {IsSubset}")] public class OpenTypeFont { internal TableCache _localTableCache; @@ -60,14 +62,18 @@ internal OpenTypeFont(FontFormat format, bool isSubset = false) } - internal OpenTypeFont(byte[] fontBytes, FontFormat format) - : this(fontBytes, -1, format) + internal OpenTypeFont(byte[] fontBytes) + : this(fontBytes, -1) { } - internal OpenTypeFont(byte[] fontBytes, long startOffset, FontFormat format) + internal OpenTypeFont(byte[] fontBytes, long startOffset) { - Format = format; + if (fontBytes == null || fontBytes.Length < 4) + throw new ArgumentException("Invalid font data: too short to contain a valid SFNT header.", nameof(fontBytes)); + + Format = DetectFormat(fontBytes, startOffset > -1 ? startOffset : 0); + _fontBytes = fontBytes; var tableReaderFactory = new FontTableReaderFactory(fontBytes); using var reader = tableReaderFactory.CreateReader(startOffset); @@ -120,6 +126,40 @@ internal OpenTypeFont(byte[] fontBytes, long startOffset, FontFormat format) : null; } + /// + /// Detects the font format from the SFNT version field in the header. + /// + /// Raw font bytes + /// Offset to the start of the SFNT header + /// Detected FontFormat + /// Thrown if the header contains an unrecognized SFNT version + private static FontFormat DetectFormat(byte[] fontBytes, long offset) + { + // sfntVersion is a big-endian UInt32 at the start of the SFNT header + uint sfntVersion = + ((uint)fontBytes[offset + 0] << 24) | + ((uint)fontBytes[offset + 1] << 16) | + ((uint)fontBytes[offset + 2] << 8) | + ((uint)fontBytes[offset + 3]); + + switch (sfntVersion) + { + case 0x00010000: // TrueType + case 0x74727565: // 'true' — Apple TrueType + return FontFormat.Ttf; + + case 0x4F54544F: // 'OTTO' — OpenType/CFF + case 0x74797031: // 'typ1' — PostScript Type 1 + return FontFormat.Otf; + + default: + throw new ArgumentException( + $"Unrecognized SFNT version 0x{sfntVersion:X8}. " + + "The data does not appear to be a valid TTF or OTF font.", + "fontBytes"); + } + } + Os2TableLoader _os2TableLoader; NameTableLoader _nameTableLoader; HheaTableLoader _hheaTableLoader; @@ -606,21 +646,25 @@ internal void AddOrReplaceTable(T table) public OpenTypeFont CreateSubset(IEnumerable usedChars) { + // Validate input if (usedChars == null) throw new ArgumentNullException(nameof(usedChars)); - var charArray = usedChars.ToArray(); - if (charArray.Length == 0) + + if (!usedChars.Any()) throw new ArgumentException("Text cannot be empty", nameof(usedChars)); var subsetBuilder = new SubsetFontBuilder(); - var codePoints = CharacterUtil.ExtractCodePointsFromChars(charArray); - // Använd "this" direkt — IsReadOnly-skyddet på AddOrReplaceTable - // garanterar att denna instans aldrig modifieras av subsetting. + // Extract Unicode code points, correctly handling surrogate pairs. + // A string like "Hello 😀" contains 7 chars but 6 code points, + // because 😀 (U+1F600) is encoded as two UTF-16 surrogates. + var codePoints = CodePointUtil.ExtractCodePoints(usedChars); + var newFont = subsetBuilder.CreateSubset(this, codePoints); var postProcessor = new SubsetPostProcessor(); postProcessor.PostProcessSubset(newFont); + return newFont; } diff --git a/src/EPPlus.Fonts.OpenType/OpenTypeFontFactory.cs b/src/EPPlus.Fonts.OpenType/OpenTypeFontFactory.cs index 85524b03e..ff1668b69 100644 --- a/src/EPPlus.Fonts.OpenType/OpenTypeFontFactory.cs +++ b/src/EPPlus.Fonts.OpenType/OpenTypeFontFactory.cs @@ -25,20 +25,20 @@ public static OpenTypeFont CreateFromFace(FontFaceInfo face) { // Pass the start offset to the constructor // This tells OpenTypeFont where this font's table directory starts - return new OpenTypeFont(fontData, face.OffsetInFile, FontFormat.Ttf); + return new OpenTypeFont(fontData, face.OffsetInFile); } else // Regular TTF/OTF { var format = Path.GetExtension(face.FilePath).ToLowerInvariant() == ".otf" ? FontFormat.Otf : FontFormat.Ttf; - return new OpenTypeFont(fontData, format); + return new OpenTypeFont(fontData); } } - public static OpenTypeFont CreateFromBytes(byte[] bytes, FontFormat format) + public static OpenTypeFont CreateFromBytes(byte[] bytes) { - return new OpenTypeFont(bytes, format); + return new OpenTypeFont(bytes); } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs b/src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs index ea2b78991..712fca93d 100644 --- a/src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs +++ b/src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs @@ -1,7 +1,7 @@ /************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes https://polyformproject.org/licenses/noncommercial/1.0.0/ A commercial license to use this software can be purchased at https://epplussoftware.com @@ -11,15 +11,16 @@ Date Author Change 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 01/10/2026 EPPlus Software AB Fix threading issue with global lock 01/23/2026 EPPlus Software AB Improved thread-safety with per-font locking + 02/26/2026 EPPlus Software AB Moved caching from DefaultFontResolver to here + 02/27/2026 EPPlus Software AB Replaced Configure overloads with IEpplusFontConfiguration *************************************************************************************************/ using EPPlus.Fonts.OpenType.FontCache; +using EPPlus.Fonts.OpenType.FontResolver; using EPPlus.Fonts.OpenType.Scanner; -using EPPlus.Fonts.OpenType.Tables; -using EPPlus.Fonts.OpenType.Tables.Cmap; -using EPPlus.Fonts.OpenType.Utils.Platform; +using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; @@ -30,69 +31,73 @@ public static class OpenTypeFonts private static readonly object _syncRoot = new object(); private static readonly Dictionary _fontLocks = new Dictionary(); - #region --- Platform-specific font locations --- + // Active resolver — replaced on Reset() and SetFontResolver(). + private static volatile IFontResolver _fontResolver; + private static volatile bool _hasCustomResolver; - private static string GetWindowsFolder() + // Singleton configuration instance. Internal events wired up in the static constructor. + private static readonly EpplusFontConfiguration _configuration; + + static OpenTypeFonts() { - var winfolder = @"c:\Windows"; -#if !NET35 - var wf = Environment.GetFolderPath(Environment.SpecialFolder.Windows); - if (!string.IsNullOrEmpty(wf) && Directory.Exists(wf)) return wf; -#endif - var ewf = Environment.GetEnvironmentVariable("WINDIR"); - if (!string.IsNullOrEmpty(ewf) && Directory.Exists(ewf)) + _configuration = new EpplusFontConfiguration(); + + _configuration.OnReset += () => { - winfolder = ewf; - } - return winfolder; - } + _hasCustomResolver = false; + _fontResolver = new DefaultFontResolver(); + ClearFontCache(); + }; - internal static readonly List winFontLocations = new List() - { - Path.Combine(GetWindowsFolder(), "Fonts"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\Windows\\Fonts"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft\\Windows\\Fonts"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\FontCache"), - }; + _configuration.OnSetFontResolver += resolver => + { + _hasCustomResolver = true; + _fontResolver = resolver; + ClearFontCache(); + }; - internal static readonly List linFontLocations = new List() - { - "/usr/share/fonts", - "/usr/local/share/fonts", - "/usr/share/X11/fonts", - Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? "~", ".fonts"), - Path.Combine(Path.Combine(Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? "~", ".local"), "share"), "fonts"), - }; - - internal static readonly List macFontLocations = new List() - { - "/System/Library/Fonts", - "/Library/Fonts", - "/Network/Library/Fonts", - Path.Combine(Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? "~", "Library"), "Fonts"), - }; + // Default resolver — no fallback config yet. + _fontResolver = new DefaultFontResolver(); + } - internal static List GetLocationsCollection(IEnumerable fontDirectories, bool searchSystemDirectories = true) + // ----------------------------------------------------------------------------------------- + // Public API + // ----------------------------------------------------------------------------------------- + + /// + /// The single entry point for configuring font behaviour. + /// Changes are global and persist for the lifetime of the application unless + /// is called inside the lambda. + /// + /// + /// // Simple fallback chain: + /// OpenTypeFonts.Configure(config => + /// { + /// config.AddFallback("Arial", "Helvetica", "Roboto"); + /// }); + /// + /// // Custom resolver (built-in Archivo Narrow fallback is bypassed): + /// OpenTypeFonts.Configure(config => + /// { + /// config.SetFontResolver(new MyDatabaseFontResolver()); + /// }); + /// + /// // Reset to factory defaults: + /// OpenTypeFonts.Configure(config => config.Reset()); + /// + public static void Configure(Action configure) { - var fontLocations = new List(); - fontLocations.AddRange(fontDirectories ?? Enumerable.Empty()); + if (configure == null) + throw new ArgumentNullException("configure"); - if (searchSystemDirectories) - { - var platform = PlatformUtils.GetPlatform(); - if (platform == PlatformUtils.OperatingSystem.Windows) - fontLocations.AddRange(winFontLocations); - else if (platform == PlatformUtils.OperatingSystem.Mac) - fontLocations.AddRange(macFontLocations); - else - fontLocations.AddRange(linFontLocations); - } + configure(_configuration); - return fontLocations; + // If the user did not install a custom resolver via SetFontResolver(), rebuild + // DefaultFontResolver so it picks up any newly added fallback chains. + if (!_hasCustomResolver) + _fontResolver = new DefaultFontResolver(config: _configuration); } - #endregion - /// /// Clears all cached fonts and font locks. /// Thread-safe operation. @@ -107,30 +112,31 @@ public static void ClearFontCache() } } + // ----------------------------------------------------------------------------------------- + // Internal font loading + // ----------------------------------------------------------------------------------------- + /// - /// Returns a fully loaded OpenTypeFont with thread-safe caching. - /// Uses per-font locking to ensure only one thread loads each unique font. - /// Uses FontScannerV2 under the hood. + /// Loads a font by name and subfamily, with thread-safe caching. + /// When fontDirectories or searchSystemDirectories are specified, they take precedence + /// over the globally configured resolver for this call only — thread-safe. /// - /// Additional directories to search for fonts - /// Font family name - /// Font subfamily (Regular, Bold, Italic, etc.) - /// Whether to search system font directories - /// If true, bypasses cache and loads font directly - /// Loaded OpenTypeFont or null if not found - public static OpenTypeFont GetFontDataOpen( - IEnumerable fontDirectories, + public static OpenTypeFont LoadFont( string fontName, FontSubFamily subFamily = FontSubFamily.Regular, + IEnumerable fontDirectories = null, bool searchSystemDirectories = true, bool ignoreCache = false) { - // Create or retrieve per-font lock key - // This ensures different fonts can be loaded in parallel, - // but the same font is only loaded once even if requested by multiple threads - string lockKey = string.Format("{0}_{1}", fontName, subFamily); - object fontLock; + var resolver = fontDirectories != null + ? new DefaultFontResolver(fontDirectories, searchSystemDirectories) + : _fontResolver; + + if (ignoreCache) + return ResolveAndCreate(resolver, fontName, subFamily); + string lockKey = BuildCacheKey(fontName, subFamily, fontDirectories, searchSystemDirectories); + object fontLock; lock (_syncRoot) { if (!_fontLocks.TryGetValue(lockKey, out fontLock)) @@ -140,79 +146,39 @@ public static OpenTypeFont GetFontDataOpen( } } - // Lock PER FONT, not globally - // This allows parallel loading of different fonts lock (fontLock) { - // Check cache inside the font-specific lock - // This ensures we don't load the same font twice - if (!ignoreCache) + var cached = OpenTypeFontCache.GetFromCache(lockKey); + if (cached != null && cached.Font != null && cached.IsLoaded) { - var cached = OpenTypeFontCache.GetFromCache(fontName, subFamily); - if (cached != null && cached.Font != null && cached.IsLoaded) - { - cached.Font.EnsureFullyLoaded(); - Debug.WriteLine($"[CACHE HIT] {fontName}_{subFamily} → Font={cached.Font.GetHashCode()}, CmapSubTables={cached.Font.CmapTable?.SubTables?.Count}"); - return cached.Font; - } + cached.Font.EnsureFullyLoaded(); + return cached.Font; } - // Mark as loading to prevent other threads from starting to load - // (they will wait in GetFromCache instead) - if (!ignoreCache) - { - OpenTypeFontCache.BeginCache(fontName, subFamily); - } + OpenTypeFontCache.BeginCache(lockKey); - // Find the font face - var face = FontScannerV2.FindBestMatch(fontDirectories, fontName, subFamily, searchSystemDirectories); - if (face == null) + var font = ResolveAndCreate(resolver, fontName, subFamily); + if (font == null) return null; - // Load the font from file - var font = OpenTypeFontFactory.CreateFromFace(face); font.EnsureFullyLoaded(); - - // Add to cache and signal waiting threads - if (!ignoreCache) - { - font.IsReadOnly = true; - OpenTypeFontCache.AddToCache(font, fontName, subFamily); - } - + font.IsReadOnly = true; + OpenTypeFontCache.AddToCache(font, lockKey); return font; } } - /// - /// Legacy wrapper – kept for backward compatibility. - /// Calls GetFontDataOpen internally. - /// - public static OpenTypeFont GetFontData( - IEnumerable fontDirectories, - string fontName, - FontSubFamily subFamily = FontSubFamily.Regular, - bool searchSystemDirectories = true, - bool ignoreCache = false) - { - return GetFontDataOpen(fontDirectories, fontName, subFamily, searchSystemDirectories, ignoreCache); - } - /// /// Returns all available font faces as fully loaded OpenTypeFont instances. /// Skips corrupt or unreadable fonts, but logs detailed information for diagnostics. /// This method is NOT cached and may take significant time to complete. /// - /// Additional directories to search - /// Whether to search system font directories - /// Optional filter for font format (TrueType or OpenType/CFF) - /// List of successfully loaded fonts public static List GetAllBaseFontData( List fontDirectories, bool searchSystemDirectories = true, FontFormat? formatTarget = null) { - var locations = GetLocationsCollection(fontDirectories, searchSystemDirectories); + var locations = DefaultFontLocations.GetLocationsCollection(fontDirectories, searchSystemDirectories); var faces = FontScannerV2.EnumerateAllFaces(locations); var result = new List(faces.Count); @@ -220,14 +186,16 @@ public static List GetAllBaseFontData( foreach (var face in faces) { - // Filter by format if requested if (formatTarget.HasValue) { string ext = Path.GetExtension(face.FilePath); if (!string.IsNullOrEmpty(ext)) { ext = ext.ToLowerInvariant(); - var format = (ext == ".otf" || ext == ".cff") ? FontFormat.Otf : FontFormat.Ttf; + var format = (ext == ".otf" || ext == ".cff") + ? FontFormat.Otf + : FontFormat.Ttf; + if (format != formatTarget.Value) continue; } @@ -235,60 +203,73 @@ public static List GetAllBaseFontData( try { - OpenTypeFont font = OpenTypeFontFactory.CreateFromFace(face); - if (font != null) - { - result.Add(font); - } - else - { - failures++; - } - } - catch (Exception ex) when ( - ex is IOException || - ex is UnauthorizedAccessException || - ex is InvalidOperationException || - ex is ArgumentException || - ex is NotSupportedException || - ex is EndOfStreamException) - { - // These are expected for corrupt or inaccessible fonts - failures++; + var font = new OpenTypeFont(File.ReadAllBytes(face.FilePath)); + font.EnsureFullyLoaded(); + result.Add(font); } catch (Exception ex) { - // Unexpected exceptions – log with full details (never swallow these silently) failures++; System.Diagnostics.Debug.WriteLine( - string.Format( - "[OpenTypeFonts] UNEXPECTED ERROR loading font: {0} [TTC offset: {1}]\r\n" + - " Exception: {2}\r\n" + - " Message: {3}\r\n" + - " Stack: {4}", - face.FilePath, - face.OffsetInFile, - ex.GetType().Name, - ex.Message, - ex.StackTrace)); + string.Format("[OpenTypeFonts] Failed to load font: {0} → {1}: {2}", + face.FilePath, ex.GetType().Name, ex.Message)); } } if (failures > 0) - { System.Diagnostics.Debug.WriteLine( - string.Format( - "[OpenTypeFonts] GetAllBaseFontData completed. Loaded {0} fonts, skipped {1} due to errors.", - result.Count, - failures)); - } + string.Format("[OpenTypeFonts] {0} font(s) failed to load.", failures)); return result; } - public static OpenTypeFont GetFromBytes(byte[] bytes, FontFormat format) + /// + /// Creates an OpenTypeFont directly from raw font bytes. + /// Font format (TTF/OTF) is detected automatically from the SFNT header. + /// + /// Raw TTF/OTF font bytes. + /// A fully loaded OpenTypeFont instance. + public static OpenTypeFont GetFromBytes(byte[] bytes) + { + if (bytes == null) + throw new ArgumentNullException("bytes"); + + var font = new OpenTypeFont(bytes); + font.EnsureFullyLoaded(); + return font; + } + + // ----------------------------------------------------------------------------------------- + // Internal helpers + // ----------------------------------------------------------------------------------------- + + internal static string BuildCacheKey( + string fontName, + FontSubFamily subFamily, + IEnumerable fontDirectories, + bool searchSystemDirectories) + { + if (fontDirectories == null) + return string.Format("{0}_{1}", fontName, subFamily); + + var dirs = string.Join("|", fontDirectories.ToArray()); + return string.Format("{0}_{1}_{2}_{3}", fontName, subFamily, dirs, searchSystemDirectories); + } + + private static OpenTypeFont ResolveAndCreate(IFontResolver resolver, string fontName, FontSubFamily subFamily) + { + var bytes = resolver.ResolveFont(fontName, subFamily); + if (bytes == null) + return null; + + return new OpenTypeFont(bytes); + } + + internal static List GetLocationsCollection( + IEnumerable fontDirectories, + bool searchSystemDirectories) { - return OpenTypeFontFactory.CreateFromBytes(bytes, format); + return DefaultFontLocations.GetLocationsCollection(fontDirectories, searchSystemDirectories); } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-Bold.ttf b/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-Bold.ttf new file mode 100644 index 000000000..05861d99f Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-Bold.ttf differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-BoldItalic.ttf b/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-BoldItalic.ttf new file mode 100644 index 000000000..ab2d41999 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-BoldItalic.ttf differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-Italic.ttf b/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-Italic.ttf new file mode 100644 index 000000000..daf0f93f5 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-Italic.ttf differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-Regular.ttf b/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-Regular.ttf new file mode 100644 index 000000000..f1c9c6d93 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/ArchivoNarrow-Regular.ttf differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/THIRD_PARTY_LICENSES.txt b/src/EPPlus.Fonts.OpenType/Resources/THIRD_PARTY_LICENSES.txt index 26398079a..787f89b3a 100644 --- a/src/EPPlus.Fonts.OpenType/Resources/THIRD_PARTY_LICENSES.txt +++ b/src/EPPlus.Fonts.OpenType/Resources/THIRD_PARTY_LICENSES.txt @@ -17,4 +17,14 @@ This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: https://openfontlicense.org The font and its source code are available at: -https://github.com/notofonts/math \ No newline at end of file +https://github.com/notofonts/math + +Archivo Narrow Font +Copyright 2011 The Archivo Project Authors (https://github.com/Omnibus-Type/ArchivoNarrow) +Licensed under the SIL Open Font License, Version 1.1 + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is available with a FAQ at: https://openfontlicense.org + +The font and its source code are available at: +https://github.com/Omnibus-Type/ArchivoNarrow \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/Scanner/FontFaceInfo.cs b/src/EPPlus.Fonts.OpenType/Scanner/FontFaceInfo.cs index c593c1028..1979c0f95 100644 --- a/src/EPPlus.Fonts.OpenType/Scanner/FontFaceInfo.cs +++ b/src/EPPlus.Fonts.OpenType/Scanner/FontFaceInfo.cs @@ -1,4 +1,5 @@ -using System; +using OfficeOpenXml.Interfaces.Fonts; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -56,6 +57,8 @@ public class FontFaceInfo public ushort FsSelection { get; internal set; } + public bool IsExactMatch { get; internal set; } + /// /// Table directory for this face. /// diff --git a/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.Ttc.cs b/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.Ttc.cs index 7ad7bd495..21e0afe61 100644 --- a/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.Ttc.cs +++ b/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.Ttc.cs @@ -24,8 +24,6 @@ private static List ScanTtcFile(string filePath) uint tag = reader.ReadUInt32BigEndian(); if (tag != 0x74746366) // "ttcf" { - System.Diagnostics.Debug.WriteLine( - $"[FontScannerV2] Expected TTC header but got 0x{tag:X8} in file: {filePath}"); return faces; // Not a TTC file – return empty list } @@ -35,8 +33,6 @@ private static List ScanTtcFile(string filePath) // Sanity check – prevent huge or corrupt TTC from causing issues if (numFonts == 0 || numFonts > 1024) { - System.Diagnostics.Debug.WriteLine( - $"[FontScannerV2] Invalid number of fonts in TTC ({numFonts}) in file: {filePath}"); return faces; } @@ -51,8 +47,6 @@ private static List ScanTtcFile(string filePath) // Skip obviously invalid offsets if (offset >= fs.Length) { - System.Diagnostics.Debug.WriteLine( - $"[FontScannerV2] Skipping invalid TTC offset 0x{offset:X8} (beyond file end) in: {filePath}"); continue; } diff --git a/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.cs b/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.cs index 17b3dc308..72c269029 100644 --- a/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.cs +++ b/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.cs @@ -2,6 +2,8 @@ using System.IO; using System; using System.Linq; +using EPPlus.Fonts.OpenType.FontResolver; +using OfficeOpenXml.Interfaces.Fonts; namespace EPPlus.Fonts.OpenType.Scanner { @@ -23,7 +25,7 @@ public static FontFaceInfo FindBestMatch( FontSubFamily desiredStyle, bool searchSystemDirectories = true) { - var directories = OpenTypeFonts.GetLocationsCollection(additionalDirectories, searchSystemDirectories); + var directories = DefaultFontLocations.GetLocationsCollection(additionalDirectories, searchSystemDirectories); var candidates = EnumerateAllFaces(directories); FontFaceInfo bestMatch = null; @@ -42,7 +44,9 @@ public static FontFaceInfo FindBestMatch( bestMatch = face; } } - + if (bestMatch == null) + return null; + bestMatch.IsExactMatch = bestScore >= 9_000; return bestMatch; } diff --git a/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2Core.cs b/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2Core.cs index 8c5474712..8e15b5227 100644 --- a/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2Core.cs +++ b/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2Core.cs @@ -1,4 +1,5 @@ using EPPlus.Fonts.OpenType.Tables.Name; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.IO; diff --git a/src/EPPlus.Fonts.OpenType/Subsetting/GposSubsetProcessor.cs b/src/EPPlus.Fonts.OpenType/Subsetting/GposSubsetProcessor.cs index cefdc5fe2..cc50506f5 100644 --- a/src/EPPlus.Fonts.OpenType/Subsetting/GposSubsetProcessor.cs +++ b/src/EPPlus.Fonts.OpenType/Subsetting/GposSubsetProcessor.cs @@ -77,8 +77,6 @@ public void Rewrite(FontSubsettingContext context) var oldGpos = context.OriginalFont.GposTable; if (oldGpos == null) return; - Debug.WriteLine($"[GPOS Subset] Original GPOS has {oldGpos.LookupList.Lookups.Count} lookups"); - // Count Type 4 (MarkToBase) lookups int markToBaseCount = 0; foreach (var lookup in oldGpos.LookupList.Lookups) @@ -90,13 +88,11 @@ public void Rewrite(FontSubsettingContext context) markToBaseCount++; } } - Debug.WriteLine($"[GPOS Subset] Original has {markToBaseCount} MarkToBase lookups"); var newGpos = oldGpos.Rewrite(context); if (newGpos != null) { - Debug.WriteLine($"[GPOS Subset] New GPOS has {newGpos.LookupList.Lookups.Count} lookups"); context.SubsetFont.AddOrReplaceTable(newGpos); } } diff --git a/src/EPPlus.Fonts.OpenType/Tables/Common/Layout/Features/FeatureListTable.cs b/src/EPPlus.Fonts.OpenType/Tables/Common/Layout/Features/FeatureListTable.cs index 9eb9c9bba..b19ed2e3e 100644 --- a/src/EPPlus.Fonts.OpenType/Tables/Common/Layout/Features/FeatureListTable.cs +++ b/src/EPPlus.Fonts.OpenType/Tables/Common/Layout/Features/FeatureListTable.cs @@ -68,27 +68,7 @@ internal FeatureRewriteResult Rewrite(FontSubsettingContext context, Dictionary< { var feature = this.FeatureRecords[oldIndex]; - if (oldIndex >= 5 && oldIndex <= 7) // Debug 'liga' features - { - Debug.WriteLine($"\n=== Processing feature[{oldIndex}]: '{feature.FeatureTag.Value}' ==="); - if (feature.FeatureTable != null) - { - Debug.WriteLine($" Original lookups: [{string.Join(", ", feature.FeatureTable.LookupListIndices.Select(i => i.ToString()).ToArray())}]"); - } - } - var rewrittenFeature = feature.Rewrite(context, lookupMap); - if (oldIndex >= 5 && oldIndex <= 7) - { - if (rewrittenFeature != null && rewrittenFeature.FeatureTable != null) - { - Debug.WriteLine($" Rewritten lookups: [{string.Join(", ", rewrittenFeature.FeatureTable.LookupListIndices.Select(i => i.ToString()).ToArray())}]"); - } - else - { - Debug.WriteLine($" ❌ REMOVED (no valid lookups remain)"); - } - } if (rewrittenFeature != null) { int newIndex = newFeatures.Count; diff --git a/src/EPPlus.Fonts.OpenType/Tables/Gpos/Data/Lookups/LookupType2/PairPosSubTableFormat2.cs b/src/EPPlus.Fonts.OpenType/Tables/Gpos/Data/Lookups/LookupType2/PairPosSubTableFormat2.cs index 7818c8ea7..95797b15b 100644 --- a/src/EPPlus.Fonts.OpenType/Tables/Gpos/Data/Lookups/LookupType2/PairPosSubTableFormat2.cs +++ b/src/EPPlus.Fonts.OpenType/Tables/Gpos/Data/Lookups/LookupType2/PairPosSubTableFormat2.cs @@ -54,11 +54,9 @@ public override bool TryGetPairAdjustment( value1 = null; value2 = null; - System.Diagnostics.Debug.WriteLine($"[Format2] Trying pair: {firstGlyph} + {secondGlyph}"); if (Coverage == null || ClassDef1 == null || ClassDef2 == null || ClassMatrix == null) { - System.Diagnostics.Debug.WriteLine($" ✗ Missing data: Cov={Coverage != null}, CD1={ClassDef1 != null}, CD2={ClassDef2 != null}, Matrix={ClassMatrix != null}"); return false; } diff --git a/src/EPPlus.Fonts.OpenType/Tables/Gsub/GsubTable.cs b/src/EPPlus.Fonts.OpenType/Tables/Gsub/GsubTable.cs index a7f29f689..9d2d65565 100644 --- a/src/EPPlus.Fonts.OpenType/Tables/Gsub/GsubTable.cs +++ b/src/EPPlus.Fonts.OpenType/Tables/Gsub/GsubTable.cs @@ -127,29 +127,6 @@ public GsubTable Rewrite(FontSubsettingContext context) if (this.FeatureList != null && lookupResult != null) { featureResult = this.FeatureList.Rewrite(context, lookupResult.OldToNewIndexMap); - if (featureResult != null) - { - Debug.WriteLine("\n=== FEATURE INDEX MAPPING ==="); - Debug.WriteLine($"Original features: {this.FeatureList.FeatureRecords.Count}"); - Debug.WriteLine($"New features: {featureResult.NewFeatureList.FeatureRecords.Count}"); - Debug.WriteLine($"Mapping entries: {featureResult.OldToNewIndexMap.Count}"); - - Debug.WriteLine("\nMapping details:"); - foreach (var kvp in featureResult.OldToNewIndexMap.OrderBy(k => k.Key)) - { - var oldFeature = this.FeatureList.FeatureRecords[kvp.Key]; - var newFeature = featureResult.NewFeatureList.FeatureRecords[kvp.Value]; - Debug.WriteLine($" Old[{kvp.Key}] '{oldFeature.FeatureTag.Value}' → New[{kvp.Value}] '{newFeature.FeatureTag.Value}'"); - } - - Debug.WriteLine("\nOriginal script DFLT had features:"); - var origDflt = this.ScriptList.ScriptRecords.FirstOrDefault(s => s.ScriptTag.Value == "DFLT"); - if (origDflt?.ScriptTable?.DefaultLangSys != null) - { - var indices = string.Join(", ", origDflt.ScriptTable.DefaultLangSys.FeatureIndices.Select(i => i.ToString()).ToArray()); - Debug.WriteLine($" [{indices}]"); - } - } if (featureResult == null || featureResult.NewFeatureList == null || featureResult.NewFeatureList.FeatureRecords.Count == 0) { // No features remain diff --git a/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs b/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs index bfd0bbe24..c471f4de6 100644 --- a/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs +++ b/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs @@ -11,6 +11,7 @@ Date Author Change 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ using EPPlus.Fonts.OpenType.FontLocalization; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.IO; diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/Contextual/ChainingContextualProcessor.cs b/src/EPPlus.Fonts.OpenType/TextShaping/Contextual/ChainingContextualProcessor.cs index 50d370a55..03dfdd15a 100644 --- a/src/EPPlus.Fonts.OpenType/TextShaping/Contextual/ChainingContextualProcessor.cs +++ b/src/EPPlus.Fonts.OpenType/TextShaping/Contextual/ChainingContextualProcessor.cs @@ -15,7 +15,7 @@ Date Author Change using EPPlus.Fonts.OpenType.Tables.Gsub.Data.Lookups; using EPPlus.Fonts.OpenType.TextShaping.Ligatures; using EPPlus.Fonts.OpenType.TextShaping.Substitutions; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; namespace EPPlus.Fonts.OpenType.TextShaping.Contextual diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/Ligatures/LigatureProcessor.cs b/src/EPPlus.Fonts.OpenType/TextShaping/Ligatures/LigatureProcessor.cs index b58f3f750..9ad8d344b 100644 --- a/src/EPPlus.Fonts.OpenType/TextShaping/Ligatures/LigatureProcessor.cs +++ b/src/EPPlus.Fonts.OpenType/TextShaping/Ligatures/LigatureProcessor.cs @@ -13,7 +13,7 @@ Date Author Change using EPPlus.Fonts.OpenType.Tables.Common.Layout.Lookups; using EPPlus.Fonts.OpenType.Tables.Gsub; using EPPlus.Fonts.OpenType.Tables.Gsub.Data.Lookups; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; using System.Linq; diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/Positioning/MarkToBaseProvider.cs b/src/EPPlus.Fonts.OpenType/TextShaping/Positioning/MarkToBaseProvider.cs index 1e5d51cc7..1e6c6568f 100644 --- a/src/EPPlus.Fonts.OpenType/TextShaping/Positioning/MarkToBaseProvider.cs +++ b/src/EPPlus.Fonts.OpenType/TextShaping/Positioning/MarkToBaseProvider.cs @@ -12,7 +12,7 @@ Date Author Change *************************************************************************************************/ using EPPlus.Fonts.OpenType.Tables.Gpos; using EPPlus.Fonts.OpenType.Tables.Gpos.Data.Lookups.LookupType4; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; namespace EPPlus.Fonts.OpenType.TextShaping.Positioning diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/Substitutions/SingleSubstitutionProcessor.cs b/src/EPPlus.Fonts.OpenType/TextShaping/Substitutions/SingleSubstitutionProcessor.cs index 704ccdf01..b3ba88a38 100644 --- a/src/EPPlus.Fonts.OpenType/TextShaping/Substitutions/SingleSubstitutionProcessor.cs +++ b/src/EPPlus.Fonts.OpenType/TextShaping/Substitutions/SingleSubstitutionProcessor.cs @@ -12,7 +12,7 @@ Date Author Change *************************************************************************************************/ using EPPlus.Fonts.OpenType.Tables.Gsub; using EPPlus.Fonts.OpenType.Tables.Gsub.Data.Lookups; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; namespace EPPlus.Fonts.OpenType.TextShaping.Substitutions diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.Vertical.cs b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.Vertical.cs index b507e334c..aa0b166f6 100644 --- a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.Vertical.cs +++ b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.Vertical.cs @@ -11,7 +11,7 @@ Date Author Change 02/19/2026 EPPlus Software AB Vertical text shaping support (Excel rotation 255) Requires TextShaper.cs to be declared as partial *************************************************************************************************/ -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; namespace EPPlus.Fonts.OpenType.TextShaping diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs index 2a8d14b30..cf7095a93 100644 --- a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs +++ b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs @@ -17,7 +17,7 @@ Date Author Change using EPPlus.Fonts.OpenType.TextShaping.Ligatures; using EPPlus.Fonts.OpenType.TextShaping.Positioning; using EPPlus.Fonts.OpenType.TextShaping.Substitutions; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextParagraph.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextParagraph.cs index 2b7b81170..bf3c40647 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextParagraph.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextParagraph.cs @@ -2,6 +2,7 @@ using EPPlus.Fonts.OpenType.Tables.Cmap.Mappings; using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections; using System.Collections.Generic; diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs index 9ccb8793d..5f85d357b 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs @@ -17,6 +17,7 @@ Date Author Change using System.Linq; using EPPlus.Fonts.OpenType.TrueTypeMeasurer; using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; +using OfficeOpenXml.Interfaces.Fonts; namespace EPPlus.Fonts.OpenType { diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index ecece0334..d0e40ec8f 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -15,7 +15,7 @@ Date Author Change using EPPlus.Fonts.OpenType.Tables.Kern; using EPPlus.Fonts.OpenType.TrueTypeMeasurer; using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; -using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.Linq; @@ -37,7 +37,7 @@ internal static class TextData internal static OpenTypeFont GetFontData(string fontName, FontSubFamily subFamily) { - return OpenTypeFonts.GetFontData(FontDirectories, fontName, subFamily, SearchSystemDirectories); + return OpenTypeFonts.LoadFont(fontName, subFamily, FontDirectories, SearchSystemDirectories); } /// @@ -1441,15 +1441,4 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa return outputTextLines; } } -} - -namespace EPPlus.Fonts.OpenType -{ - public enum FontSubFamily - { - Regular, - Bold, - Italic, - BoldItalic - } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/Utils/CodePointUtil.cs b/src/EPPlus.Fonts.OpenType/Utils/CodePointUtil.cs new file mode 100644 index 000000000..c427caa18 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Utils/CodePointUtil.cs @@ -0,0 +1,85 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 + *************************************************************************************************/ +using System.Collections.Generic; + +namespace EPPlus.Fonts.OpenType.Utils +{ + internal static class CodePointUtil + { + /// + /// Extracts distinct Unicode code points from a sequence of chars, + /// correctly combining surrogate pairs into supplementary plane code points. + /// Lone surrogates are skipped. + /// + internal static IEnumerable ExtractCodePoints(IEnumerable chars) + { + var codePoints = new HashSet(); + char? pendingHighSurrogate = null; + + foreach (var c in chars) + { + if (pendingHighSurrogate.HasValue) + { + if (char.IsLowSurrogate(c)) + { + // Valid pair - combine into code point + int cp = char.ConvertToUtf32(pendingHighSurrogate.Value, c); + codePoints.Add(cp); + pendingHighSurrogate = null; + continue; + } + + // Previous high surrogate had no matching low - skip it + pendingHighSurrogate = null; + } + + if (char.IsHighSurrogate(c)) + { + pendingHighSurrogate = c; + } + else if (!char.IsLowSurrogate(c)) + { + // Normal BMP character + codePoints.Add(c); + } + // Lone low surrogates are skipped + } + + return codePoints; + } + + /// + /// Converts a set of Unicode code points to a string, + /// correctly encoding supplementary plane characters as surrogate pairs. + /// + internal static string CodePointsToString(HashSet codePoints) + { + var sb = new System.Text.StringBuilder(codePoints.Count * 2); + + foreach (var cp in codePoints) + { + if (cp <= 0xFFFF) + { + sb.Append((char)cp); + } + else + { + // Supplementary plane - encode as surrogate pair + sb.Append(char.ConvertFromUtf32(cp)); + } + } + + return sb.ToString(); + } + } +} diff --git a/src/EPPlus.Interfaces/Fonts/FontSubFamily.cs b/src/EPPlus.Interfaces/Fonts/FontSubFamily.cs new file mode 100644 index 000000000..9c5659cf5 --- /dev/null +++ b/src/EPPlus.Interfaces/Fonts/FontSubFamily.cs @@ -0,0 +1,22 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 01/15/2025 EPPlus Software AB Initial implementation + *************************************************************************************************/ +namespace OfficeOpenXml.Interfaces.Fonts +{ + public enum FontSubFamily + { + Regular, + Bold, + Italic, + BoldItalic + } +} diff --git a/src/EPPlus.Interfaces/Drawing/Text/GlyphWidth.cs b/src/EPPlus.Interfaces/Fonts/GlyphWidth.cs similarity index 98% rename from src/EPPlus.Interfaces/Drawing/Text/GlyphWidth.cs rename to src/EPPlus.Interfaces/Fonts/GlyphWidth.cs index f7932cc4c..ab4086dfd 100644 --- a/src/EPPlus.Interfaces/Drawing/Text/GlyphWidth.cs +++ b/src/EPPlus.Interfaces/Fonts/GlyphWidth.cs @@ -13,7 +13,7 @@ Date Author Change using System.Diagnostics; using System.Runtime.InteropServices; -namespace OfficeOpenXml.Interfaces.Drawing.Text +namespace OfficeOpenXml.Interfaces.Fonts { /// /// Lightweight glyph representation optimized for text measurement and wrapping. diff --git a/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs b/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs new file mode 100644 index 000000000..2359b5639 --- /dev/null +++ b/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs @@ -0,0 +1,59 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 02/27/2026 EPPlus Software AB Initial implementation + *************************************************************************************************/ +namespace OfficeOpenXml.Interfaces.Fonts +{ + /// + /// Global font configuration for EPPlus. + /// Accessed exclusively via OpenTypeFonts.Configure(Action<IEpplusFontConfiguration>). + /// Changes are global and persist for the lifetime of the application unless + /// is called. + /// + public interface IEpplusFontConfiguration + { + /// + /// Adds a font-name fallback chain. + /// "If Arial is not found, try Helvetica, then Roboto." + /// Additive — does not replace previously added fallbacks for the same font. + /// + /// The font that may be missing. + /// + /// One or more font names to try in order when + /// cannot be resolved. + /// + /// This instance, for fluent chaining. + IEpplusFontConfiguration AddFallback(string primaryFont, params string[] fallbacks); + + /// + /// Replaces the font resolver entirely. + /// The user takes full responsibility for font resolution. + /// EPPlus built-in fallbacks (Archivo Narrow) will NOT be applied. + /// + /// + /// A custom that returns raw TTF/OTF bytes. + /// + /// This instance, for fluent chaining. + IEpplusFontConfiguration SetFontResolver(IFontResolver resolver); + + /// + /// Restores all settings to factory defaults: + /// + /// Clears all font-name fallbacks. + /// Restores DefaultFontResolver (with Archivo Narrow built-in fallback). + /// Restores DefaultFontProvider (with Noto Emoji + Noto Sans Math chain). + /// Clears the font cache. + /// + /// + /// This instance, for fluent chaining. + IEpplusFontConfiguration Reset(); + } +} \ No newline at end of file diff --git a/src/EPPlus.Interfaces/Fonts/IFontResolver.cs b/src/EPPlus.Interfaces/Fonts/IFontResolver.cs new file mode 100644 index 000000000..9d71cb4ec --- /dev/null +++ b/src/EPPlus.Interfaces/Fonts/IFontResolver.cs @@ -0,0 +1,31 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 + 02/26/2026 EPPlus Software AB Simplified to return raw font bytes + *************************************************************************************************/ +namespace OfficeOpenXml.Interfaces.Fonts +{ + /// + /// Resolves a font by name and subfamily to raw font bytes. + /// Implement this interface to provide fonts from any source (file system, database, embedded resources, etc.). + /// Font format (TTF/OTF) is detected automatically from the returned bytes. + /// + public interface IFontResolver + { + /// + /// Resolves a font to its raw bytes. + /// + /// Font family name (e.g. "Roboto") + /// Font subfamily (Regular, Bold, Italic, etc.) + /// Raw font bytes, or null if the font could not be found + byte[] ResolveFont(string fontName, FontSubFamily subFamily); + } +} \ No newline at end of file diff --git a/src/EPPlus.Interfaces/Drawing/Text/ITextShaper.cs b/src/EPPlus.Interfaces/Fonts/ITextShaper.cs similarity index 99% rename from src/EPPlus.Interfaces/Drawing/Text/ITextShaper.cs rename to src/EPPlus.Interfaces/Fonts/ITextShaper.cs index 2710f5f96..4a563c0e6 100644 --- a/src/EPPlus.Interfaces/Drawing/Text/ITextShaper.cs +++ b/src/EPPlus.Interfaces/Fonts/ITextShaper.cs @@ -11,7 +11,7 @@ Date Author Change 01/15/2025 EPPlus Software AB Initial implementation *************************************************************************************************/ -namespace OfficeOpenXml.Interfaces.Drawing.Text +namespace OfficeOpenXml.Interfaces.Fonts { /// /// Core text shaping - converts text to positioned glyphs. diff --git a/src/EPPlus.Interfaces/Drawing/Text/ShapedGlyph.cs b/src/EPPlus.Interfaces/Fonts/ShapedGlyph.cs similarity index 99% rename from src/EPPlus.Interfaces/Drawing/Text/ShapedGlyph.cs rename to src/EPPlus.Interfaces/Fonts/ShapedGlyph.cs index 8b59de60f..fe05a6a18 100644 --- a/src/EPPlus.Interfaces/Drawing/Text/ShapedGlyph.cs +++ b/src/EPPlus.Interfaces/Fonts/ShapedGlyph.cs @@ -15,7 +15,7 @@ Date Author Change using System.Diagnostics; using System.Runtime.InteropServices; -namespace OfficeOpenXml.Interfaces.Drawing.Text +namespace OfficeOpenXml.Interfaces.Fonts { /// /// Represents a shaped glyph with positioning information. diff --git a/src/EPPlus.Interfaces/Drawing/Text/ShapedText.cs b/src/EPPlus.Interfaces/Fonts/ShapedText.cs similarity index 98% rename from src/EPPlus.Interfaces/Drawing/Text/ShapedText.cs rename to src/EPPlus.Interfaces/Fonts/ShapedText.cs index a816a550d..24a6b9344 100644 --- a/src/EPPlus.Interfaces/Drawing/Text/ShapedText.cs +++ b/src/EPPlus.Interfaces/Fonts/ShapedText.cs @@ -14,7 +14,7 @@ Date Author Change using System.Diagnostics; using System.Text; -namespace OfficeOpenXml.Interfaces.Drawing.Text +namespace OfficeOpenXml.Interfaces.Fonts { /// /// Result of text shaping operation containing positioned glyphs. diff --git a/src/EPPlus.Interfaces/Drawing/Text/ShapedVerticalText.cs b/src/EPPlus.Interfaces/Fonts/ShapedVerticalText.cs similarity index 98% rename from src/EPPlus.Interfaces/Drawing/Text/ShapedVerticalText.cs rename to src/EPPlus.Interfaces/Fonts/ShapedVerticalText.cs index 6f7081513..5df675f71 100644 --- a/src/EPPlus.Interfaces/Drawing/Text/ShapedVerticalText.cs +++ b/src/EPPlus.Interfaces/Fonts/ShapedVerticalText.cs @@ -12,7 +12,7 @@ Date Author Change *************************************************************************************************/ using System.Diagnostics; -namespace OfficeOpenXml.Interfaces.Drawing.Text +namespace OfficeOpenXml.Interfaces.Fonts { /// /// Result of a vertical text shaping operation containing positioned glyphs. diff --git a/src/EPPlus.Interfaces/Drawing/Text/ShapingOptions.cs b/src/EPPlus.Interfaces/Fonts/ShapingOptions.cs similarity index 99% rename from src/EPPlus.Interfaces/Drawing/Text/ShapingOptions.cs rename to src/EPPlus.Interfaces/Fonts/ShapingOptions.cs index a1d84d37b..a62ca99d7 100644 --- a/src/EPPlus.Interfaces/Drawing/Text/ShapingOptions.cs +++ b/src/EPPlus.Interfaces/Fonts/ShapingOptions.cs @@ -12,7 +12,7 @@ Date Author Change *************************************************************************************************/ using System.Collections.Generic; -namespace OfficeOpenXml.Interfaces.Drawing.Text +namespace OfficeOpenXml.Interfaces.Fonts { /// /// Options for controlling text shaping behavior. diff --git a/src/EPPlus.Interfaces/Drawing/Text/VerticalGlyphHeight.cs b/src/EPPlus.Interfaces/Fonts/VerticalGlyphHeight.cs similarity index 97% rename from src/EPPlus.Interfaces/Drawing/Text/VerticalGlyphHeight.cs rename to src/EPPlus.Interfaces/Fonts/VerticalGlyphHeight.cs index bdd4cb0d6..aae70c5df 100644 --- a/src/EPPlus.Interfaces/Drawing/Text/VerticalGlyphHeight.cs +++ b/src/EPPlus.Interfaces/Fonts/VerticalGlyphHeight.cs @@ -13,7 +13,7 @@ Date Author Change using System.Diagnostics; using System.Runtime.InteropServices; -namespace OfficeOpenXml.Interfaces.Drawing.Text +namespace OfficeOpenXml.Interfaces.Fonts { /// /// Lightweight glyph representation optimized for vertical text measurement. diff --git a/src/EPPlus.Interfaces/Drawing/Text/VerticalShapedGlyph.cs b/src/EPPlus.Interfaces/Fonts/VerticalShapedGlyph.cs similarity index 98% rename from src/EPPlus.Interfaces/Drawing/Text/VerticalShapedGlyph.cs rename to src/EPPlus.Interfaces/Fonts/VerticalShapedGlyph.cs index 7fb6a89a4..5e83a5d57 100644 --- a/src/EPPlus.Interfaces/Drawing/Text/VerticalShapedGlyph.cs +++ b/src/EPPlus.Interfaces/Fonts/VerticalShapedGlyph.cs @@ -12,7 +12,7 @@ Date Author Change *************************************************************************************************/ using System.Diagnostics; -namespace OfficeOpenXml.Interfaces.Drawing.Text +namespace OfficeOpenXml.Interfaces.Fonts { /// /// Represents a shaped glyph with vertical positioning information.