diff --git a/src/EPPlus.Export.Pdf/PdfResources/PdfFontResource.cs b/src/EPPlus.Export.Pdf/PdfResources/PdfFontResource.cs index f564d5dbd..fbbc146b0 100644 --- a/src/EPPlus.Export.Pdf/PdfResources/PdfFontResource.cs +++ b/src/EPPlus.Export.Pdf/PdfResources/PdfFontResource.cs @@ -36,6 +36,7 @@ internal class PdfFontResource : PdfResource internal int fontWidthObjectNumber = -1; internal int cidSetObjectNumber = -1; internal OpenTypeFont fontData; + private OpenTypeFontEngine _fontEngine; private int firstChar = 32; private int lastChar = 255; private CIDSystemInfo cidSystemInfo = null; @@ -55,13 +56,14 @@ public PdfFontResource(string fontName, FontSubFamily subFamily, int labelNumber : base("F", labelNumber) { this.fontName = fontName; - fontData = OpenTypeFonts.LoadFont(fontName, subFamily, pageSettings.FontDirectories, pageSettings.SearchSystemDirectories); - fontSubsetManager = new FontSubsetManager(fontData); + _fontEngine = pageSettings.FontEngine; + fontData = _fontEngine.LoadFont(fontName, subFamily); + fontSubsetManager = new FontSubsetManager(pageSettings.FontEngine, fontData); } internal static OpenTypeFont GetFontData(PdfPageSettings pageSettings, string fontName, FontSubFamily subFamily) { - return OpenTypeFonts.LoadFont(fontName,subFamily, pageSettings.FontDirectories, pageSettings.SearchSystemDirectories); + return pageSettings.FontEngine.LoadFont(fontName,subFamily); } //Get font data from fontResources. If font does not exsist, add it to fontResources. @@ -81,12 +83,6 @@ internal static OpenTypeFont GetFontResourceData(Dictionary public class PdfPageSettings { + private OpenTypeFontEngine _fontEngine; + public OpenTypeFontEngine FontEngine + { + get + { + if(_fontEngine == null) + { + _fontEngine = new OpenTypeFontEngine(x => + { + if(FontDirectories != null && FontDirectories.Any()) + { + foreach(var dir in FontDirectories) + { + if (!System.IO.Directory.Exists(dir)) + { + throw new System.IO.DirectoryNotFoundException($"Font directory not found: {dir}"); + } + x.FontDirectories.Add(dir); + } + x.SearchSystemDirectories = SearchSystemDirectories; + + } + + }); + } + return _fontEngine; + } + } /// /// Add additional folders to search for fonts. /// diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/ExtractCharWidthsBenchmark.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/ExtractCharWidthsBenchmark.cs index 734d92727..f37975e81 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/ExtractCharWidthsBenchmark.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/ExtractCharWidthsBenchmark.cs @@ -17,8 +17,12 @@ public class ExtractCharWidthsBenchmark public void Setup() { var fontFolders = new List { /* your font paths */ }; - var font = OpenTypeFonts.LoadFont("Calibri"); - _shaper = new TextShaper(font); + var fontEngine = new OpenTypeFontEngine(x => + { + x.SearchSystemDirectories = true; + }); + var font = fontEngine.LoadFont("Calibri"); + _shaper = new TextShaper(fontEngine, font); _options = ShapingOptions.Default; // Short: typical Excel cell diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs index 75802f928..9a3f72455 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs @@ -55,13 +55,14 @@ public void Setup() // Configure the global font system to search the benchmark's local Fonts directory // exclusively. Must happen before any LoadFont call. - OpenTypeFonts.Configure(cfg => + var fontEngine = new OpenTypeFontEngine(cfg => { cfg.Reset(); cfg.FontDirectories.Add(fontsPath); cfg.SearchSystemDirectories = false; }); + Console.WriteLine("\nAvailable Roboto fonts:"); foreach (var file in Directory.GetFiles(fontsPath, "Roboto*.ttf")) { @@ -74,7 +75,7 @@ public void Setup() Console.WriteLine(string.Format("Loaded: {0} {1} ({2} glyphs)", font.FullName, font.SubFamily, font.GlyfTable.Glyphs.Count)); - var shaper = new TextShaper(font); + var shaper = new TextShaper(fontEngine, font); _layoutEngine = new TextLayoutEngine(shaper); Console.WriteLine("\nPre-warming font cache (Regular, Bold, Italic)..."); diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/TextLayoutEngineBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/TextLayoutEngineBenchmarks.cs index c89ccdf46..21f945344 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/TextLayoutEngineBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/TextLayoutEngineBenchmarks.cs @@ -156,8 +156,14 @@ public List New_Wrap_10Paragraphs_Sequential() [Benchmark] public double[] OnlyExtractWidths() { - var font = OpenTypeFonts.LoadFont(FontFamily, FontSubFamily.Regular); - var shaper = new TextShaper(font); + var fontsPath = Path.Combine(AppContext.BaseDirectory, "Fonts"); + var fontEngine = new OpenTypeFontEngine(x => + { + x.FontDirectories.Add(fontsPath); + x.SearchSystemDirectories = false; + }); + var font = fontEngine.LoadFont(FontFamily, FontSubFamily.Regular); + var shaper = new TextShaper(fontEngine, 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 87ce151d4..e8b72a80a 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/TextShapingBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/TextShapingBenchmarks.cs @@ -9,12 +9,18 @@ namespace EPPlus.Fonts.OpenType.Benchmarks public class TextShapingBenchmarks { private OpenTypeFont _roboto; + private OpenTypeFontEngine _engine; private TextShaper _shaper; [GlobalSetup] // Runs once before all benchmarks public void Setup() { var fontsPath = Path.Combine(AppContext.BaseDirectory, "Fonts"); + _engine = new OpenTypeFontEngine(x => + { + x.FontDirectories.Add(fontsPath); + x.SearchSystemDirectories = false; + }); if (!Directory.Exists(fontsPath)) { @@ -22,8 +28,8 @@ public void Setup() } var fontFolders = new List { fontsPath }; - _roboto = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); - _shaper = new TextShaper(_roboto); + _roboto = _engine.LoadFont("Roboto", FontSubFamily.Regular); + _shaper = new TextShaper(_engine, _roboto); } [Benchmark] diff --git a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs index 254cbe8ba..8f9ceac8d 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs @@ -1,6 +1,6 @@ ο»Ώ/************************************************************************************************* Font Provider Unit Tests - Tests for automatic emoji fallback functionality + Tests for automatic emoji fallback functionality and script-based glyph fallback. *************************************************************************************************/ using EPPlus.Fonts.OpenType.TextShaping; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -16,6 +16,9 @@ public class FontProviderTests : FontTestBase private OpenTypeFont _robotoFont; + // U+6F22 = ζΌ’ (a common Han ideograph used in Chinese/Japanese) + private const string HanCharacter = "\u6F22"; + [TestInitialize] public void TestSetup() { @@ -26,7 +29,7 @@ public void TestSetup() public void DefaultFontProvider_EmojiGlyph_ShouldUseFallbackFont() { // Arrange - var shaper = new TextShaper(_robotoFont); + var shaper = new TextShaper(TestFolderEngine, _robotoFont); // Act var shaped = shaper.Shape("πŸ˜€"); @@ -35,18 +38,20 @@ public void DefaultFontProvider_EmojiGlyph_ShouldUseFallbackFont() // Assert Assert.AreEqual(1, shaped.Glyphs.Length, "Should have 1 glyph"); Assert.AreNotEqual((ushort)0, shaped.Glyphs[0].GlyphId, "Emoji should not be .notdef"); - Assert.AreEqual((byte)0, shaped.Glyphs[0].FontId, "Emoji is the only font used (FontId=0)"); + Assert.AreNotEqual((byte)0, shaped.Glyphs[0].FontId, "Emoji should come from a fallback font, not primary"); - // Verify it's NOT the primary font - Assert.AreEqual(1, usedFonts.Count, "Should only use one font (emoji fallback)"); - Assert.AreNotEqual(_robotoFont, usedFonts[0], "Should be emoji font, not Roboto"); + // Primary is always registered at FontId 0, even if no glyphs come from it. + // The emoji fallback occupies FontId 1. + Assert.AreEqual(2, usedFonts.Count, "Used fonts should be [primary, emoji fallback]"); + Assert.AreEqual(_robotoFont, usedFonts[0], "Primary font is always FontId 0"); + Assert.AreNotEqual(_robotoFont, usedFonts[1], "Fallback should be the emoji font"); } [TestMethod] public void DefaultFontProvider_LatinText_ShouldUsePrimaryFont() { // Arrange - var shaper = new TextShaper(_robotoFont); + var shaper = new TextShaper(TestFolderEngine, _robotoFont); // Act var shaped = shaper.Shape("Hello World"); @@ -62,7 +67,7 @@ public void DefaultFontProvider_LatinText_ShouldUsePrimaryFont() public void DefaultFontProvider_MixedTextAndEmoji_ShouldUseMultipleFonts() { // Arrange - var shaper = new TextShaper(_robotoFont); + var shaper = new TextShaper(TestFolderEngine, _robotoFont); // Act var shaped = shaper.Shape("Hello πŸ˜€ World"); @@ -78,7 +83,7 @@ public void DefaultFontProvider_MixedTextAndEmoji_ShouldUseMultipleFonts() public void TextShaper_SurrogatePair_ShouldMapToSingleGlyph() { // Arrange - var shaper = new TextShaper(_robotoFont); + var shaper = new TextShaper(TestFolderEngine, _robotoFont); string text = "πŸ˜€"; // U+1F600 = 2 chars in UTF-16 // Act @@ -95,7 +100,7 @@ public void TextShaper_SurrogatePair_ShouldMapToSingleGlyph() public void TextShaper_MultipleEmoji_ShouldMapCorrectly() { // Arrange - var shaper = new TextShaper(_robotoFont); + var shaper = new TextShaper(TestFolderEngine, _robotoFont); string text = "πŸ˜€πŸ˜πŸ˜‚"; // 3 emoji = 6 chars in UTF-16 // Act @@ -114,5 +119,153 @@ public void TextShaper_MultipleEmoji_ShouldMapCorrectly() Assert.AreEqual((ushort)4, shaped.Glyphs[2].ClusterIndex, "Third emoji at char 4"); Assert.AreEqual((byte)2, shaped.Glyphs[2].CharCount, "Third emoji spans 2 chars"); } + + // ----------------------------------------------------------------------------------------- + // Script-based glyph fallback + // ----------------------------------------------------------------------------------------- + + [TestMethod] + public void DefaultFontProvider_HanGlyphWithConfiguredFallback_RoutesToFallbackFont() + { + // Arrange β€” explicit script fallback to BIZ UDGothic (which is in the test font folder + // and contains Han glyphs). Use a fresh engine so the default Han chain doesn't + // interfere. + var engine = new OpenTypeFontEngine(cfg => + { + foreach (var folder in FontFolders) + cfg.FontDirectories.Add(folder); + cfg.SearchSystemDirectories = false; + cfg.SetScriptFallback(UnicodeScript.Han, "BIZ UDGothic"); + }); + + var roboto = engine.LoadFont("Roboto", FontSubFamily.Regular); + var shaper = new TextShaper(engine, roboto); + + // Act + var shaped = shaper.Shape(HanCharacter); + var usedFonts = shaper.GetUsedFonts().ToList(); + + // Assert β€” glyph must come from the fallback, not from Roboto's .notdef + Assert.AreEqual(1, shaped.Glyphs.Length); + Assert.AreNotEqual((ushort)0, shaped.Glyphs[0].GlyphId, "Han glyph should not be .notdef"); + Assert.AreNotEqual((byte)0, shaped.Glyphs[0].FontId, "Han glyph should come from a fallback font"); + + Assert.AreEqual(2, usedFonts.Count, "Should use 2 fonts (primary + Han fallback)"); + Assert.AreEqual(roboto, usedFonts[0]); + Assert.AreEqual("BIZ UDGothic", usedFonts[1].NameTable.GetFamilyName()); + } + + [TestMethod] + public void DiagnoseScriptFallback() + { + var engine = new OpenTypeFontEngine(cfg => + { + foreach (var folder in FontFolders) + cfg.FontDirectories.Add(folder); + cfg.SearchSystemDirectories = false; + cfg.SetScriptFallback(UnicodeScript.Han, "BIZ UDGothic"); + }); + + // Diagnostik 1: vad sΓ€tter engine fΓΆr Han efter konstruktion? + var chain = engine.GetScriptFallback(UnicodeScript.Han); + System.Console.WriteLine($"[DIAG] Han chain: [{string.Join(", ", chain ?? new string[0])}]"); + + // Diagnostik 2: shape och se vad providern returnerar + var roboto = engine.LoadFont("Roboto", FontSubFamily.Regular); + var shaper = new TextShaper(engine, roboto); + var shaped = shaper.Shape("\u6F22"); + + System.Console.WriteLine($"[DIAG] Glyph count: {shaped.Glyphs.Length}"); + System.Console.WriteLine($"[DIAG] First glyph: GlyphId={shaped.Glyphs[0].GlyphId}, FontId={shaped.Glyphs[0].FontId}"); + + var usedFonts = shaper.GetUsedFonts().ToList(); + System.Console.WriteLine($"[DIAG] Used fonts count: {usedFonts.Count}"); + foreach (var f in usedFonts) + { + System.Console.WriteLine($"[DIAG] - {f.NameTable.GetFamilyName()}"); + } + } + + [TestMethod] + public void DefaultFontProvider_HanGlyphWithNoInstalledFallback_ReturnsNotdef() + { + // Arrange β€” TestFolderEngine's default Han chain points at Microsoft YaHei, SimSun, etc., + // none of which are present in the test font folder. So a Han character should fall + // through to .notdef. + var roboto = TestFolderEngine.LoadFont("Roboto", FontSubFamily.Regular); + var shaper = new TextShaper(TestFolderEngine, roboto); + + // Act + var shaped = shaper.Shape(HanCharacter); + + // Assert + Assert.AreEqual(1, shaped.Glyphs.Length); + Assert.AreEqual((ushort)0, shaped.Glyphs[0].GlyphId, "Glyph should be .notdef"); + Assert.AreEqual((byte)0, shaped.Glyphs[0].FontId, "Provider returns primary font when not found"); + } + + [TestMethod] + public void DefaultFontProvider_EmptyScriptFallbackChain_DisablesFallbackForScript() + { + // Arrange β€” explicitly disable Han fallback. Even if BIZ UDGothic would have worked + // by default, an empty chain says "do not route Han characters anywhere". + var engine = new OpenTypeFontEngine(cfg => + { + foreach (var folder in FontFolders) + cfg.FontDirectories.Add(folder); + cfg.SearchSystemDirectories = false; + cfg.SetScriptFallback(UnicodeScript.Han, new string[0]); + }); + + var roboto = engine.LoadFont("Roboto", FontSubFamily.Regular); + var shaper = new TextShaper(engine, roboto); + + // Act + var shaped = shaper.Shape(HanCharacter); + + // Assert + Assert.AreEqual(1, shaped.Glyphs.Length); + Assert.AreEqual((ushort)0, shaped.Glyphs[0].GlyphId, "Glyph should be .notdef when chain is empty"); + } + + [TestMethod] + public void EpplusFontConfiguration_Reset_RestoresDefaultScriptChains() + { + // Arrange β€” start with a custom Han chain, then call Reset on the configuration. + // After Reset, the default Han chain (Microsoft YaHei, SimSun, Noto Sans CJK SC, + // PingFang SC) should be reinstated. + // + // We can't directly observe the chain from outside the engine, so we verify the + // behavior indirectly: with the test font folder only and the default chain back + // in place, a Han character should once again be .notdef (none of the default + // chain fonts are in the test folder). + var engine = new OpenTypeFontEngine(cfg => + { + foreach (var folder in FontFolders) + cfg.FontDirectories.Add(folder); + cfg.SearchSystemDirectories = false; + cfg.SetScriptFallback(UnicodeScript.Han, "BIZ UDGothic"); + + // Custom chain was just set above β€” now undo via Reset. + cfg.Reset(); + + // After Reset we lose the font directories too, so re-add them. + foreach (var folder in FontFolders) + cfg.FontDirectories.Add(folder); + cfg.SearchSystemDirectories = false; + }); + + var roboto = engine.LoadFont("Roboto", FontSubFamily.Regular); + var shaper = new TextShaper(engine, roboto); + + // Act + var shaped = shaper.Shape(HanCharacter); + + // Assert β€” back to default chain. None of (Microsoft YaHei, SimSun, ...) is in the + // test font folder, so the Han character lands on .notdef. + Assert.AreEqual(1, shaped.Glyphs.Length); + Assert.AreEqual((ushort)0, shaped.Glyphs[0].GlyphId, + "After Reset, the default Han chain should be active again; with no matching fonts installed, glyph is .notdef"); + } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/FontSubsetManagerTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FontSubsetManagerTests.cs index c87653c42..2e7289e10 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/FontSubsetManagerTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/FontSubsetManagerTests.cs @@ -22,7 +22,7 @@ public void CreateSubsettedProvider_WithAsciiText_ReturnsSubsettedPrimaryFont() { // Arrange var font = LoadTestFont(); - var manager = new FontSubsetManager(font); + var manager = new FontSubsetManager(TestFolderEngine, font); // Act manager.AddText("Hello World"); @@ -49,7 +49,7 @@ public void CreateSubsettedProvider_WithEmoji_SubsetsFallbackFont() { // Arrange var font = LoadTestFont(); - var provider = new DefaultFontProvider(font); + var provider = new DefaultFontProvider(TestFolderEngine, font); var manager = new FontSubsetManager(provider); // Act - Add text with emoji (U+1F600 = πŸ˜€, handled by Noto Emoji fallback) @@ -77,7 +77,7 @@ public void CreateSubsettedProvider_WithMultipleAddTextCalls_CollectsAllCodePoin { // Arrange var font = LoadTestFont(); - var manager = new FontSubsetManager(font); + var manager = new FontSubsetManager(TestFolderEngine, font); // Act - Add text in multiple calls (simulates scanning multiple cells) manager.AddText("ABC"); @@ -101,7 +101,7 @@ public void CreateSubsettedProvider_UnusedFallbackFontsAreExcluded() { // Arrange - DefaultFontProvider has Noto Emoji + Noto Math as fallbacks var font = LoadTestFont(); - var provider = new DefaultFontProvider(font); + var provider = new DefaultFontProvider(TestFolderEngine, font); var manager = new FontSubsetManager(provider); // Act - Only ASCII text, no emoji or math symbols @@ -119,7 +119,7 @@ public void AddText_WithNullOrEmpty_DoesNotThrow() { // Arrange var font = LoadTestFont(); - var manager = new FontSubsetManager(font); + var manager = new FontSubsetManager(TestFolderEngine, font); // Act & Assert - Should handle gracefully manager.AddText(null); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 5d2823439..88f3566da 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -20,8 +20,8 @@ public void WrapText_ShortText_NoWrapping() { RequireFont(SystemFontsEngine, "Calibri"); // Arrange - var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); + var shaper = SystemFontsEngine.GetTextShaper("Calibri"); var layout = new TextLayoutEngine(shaper); // Act @@ -38,7 +38,7 @@ public void WrapText_LongText_WrapsAtSpaces() RequireFont(SystemFontsEngine, "Calibri"); // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = SystemFontsEngine.GetTextShaper("Calibri"); var layout = new TextLayoutEngine(shaper); // Act - narrow width forces wrapping @@ -61,7 +61,7 @@ public void WrapText_WithLineBreaks_PreservesBreaks() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = new TextShaper(SystemFontsEngine, font); var layout = new TextLayoutEngine(shaper); // Act @@ -87,7 +87,7 @@ public void WrapText_TestWhenOnExactWrapPlusSpaces2() var maxWidthPoints = 54d; - ITextShaper shaper = new TextShaper(font); + ITextShaper shaper = SystemFontsEngine.GetTextShaper("Aptos Narrow"); using var layoutEngine = new TextLayoutEngine(shaper); var wrappedLines = layoutEngine.WrapText( text, @@ -109,7 +109,7 @@ public void WrapText_TestWhenOnExactWrap() 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" }; - ITextShaper shaper = new TextShaper(font); + ITextShaper shaper = SystemFontsEngine.GetTextShaper("Aptos Narrow"); using var layoutEngine = new TextLayoutEngine(shaper); var wrappedLines = layoutEngine.WrapText( text, @@ -132,7 +132,7 @@ public void WrapText_TestFragments() 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."; var savedStrings = SavedComparisonString.Split("\r\n"); - ITextShaper shaper = new TextShaper(font); + ITextShaper shaper = new TextShaper(SystemFontsEngine, font); using var layoutEngine = new TextLayoutEngine(shaper); var wrappedLines = layoutEngine.WrapText( Lorem20Str, @@ -172,11 +172,11 @@ public void WrapText_WithPreExistingWidth_AccountsForIt() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = SystemFontsEngine.GetTextShaper("Calibri"); var layout = new TextLayoutEngine(shaper); // Measure "Hello " to get its width - var testShaper = new TextShaper(font); + var testShaper = SystemFontsEngine.GetTextShaper("Calibri"); var shaped = testShaper.Shape("Hello ", ShapingOptions.Default); double preWidth = shaped.GetWidthInPoints(11f); @@ -194,7 +194,7 @@ public void WrapText_EmptyString_ReturnsEmptyLine() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = SystemFontsEngine.GetTextShaper("Calibri"); var layout = new TextLayoutEngine(shaper); // Act @@ -210,7 +210,7 @@ public void WrapText_WithKerning_MeasuresCorrectly() { // Arrange var font = TestFolderEngine.LoadFont("Roboto", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = TestFolderEngine.GetTextShaper("Roboto"); var layout = new TextLayoutEngine(shaper); // Act - "AV" has kerning in Roboto @@ -237,8 +237,8 @@ public void ImportingFromCells() [TestMethod] public void MyVeryGoodRichTextWrapper() { - var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); - var shaper = new TextShaper(font); + var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); + var shaper = SystemFontsEngine.GetTextShaper("Calibri"); //Text containing emoji var inputText = "My long and 😝😱 bothersome 😝😱 text"; @@ -260,7 +260,7 @@ public void WrapRichText_SingleFragment_BehavesLikeSingleFont() RequireFont(SystemFontsEngine, "Calibri", FontSubFamily.Regular); // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = SystemFontsEngine.GetTextShaper("Calibri"); var layout = new TextLayoutEngine(shaper); var fragments = new List @@ -288,7 +288,7 @@ public void WrapRichText_MultipleFragments_ConcatenatesCorrectly() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = SystemFontsEngine.GetTextShaper("Calibri"); var layout = new TextLayoutEngine(shaper); var fragments = new List @@ -319,7 +319,7 @@ public void WrapRichText_DifferentFonts_WrapsCorrectly() RequireFont(SystemFontsEngine, "Calibri", FontSubFamily.Regular); // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = new TextShaper(SystemFontsEngine, font); var layout = new TextLayoutEngine(shaper); var fragments = new List @@ -393,7 +393,7 @@ public void WrapLongRichTextWord() var fragment = new TextFragment() { Text = longWord, Font = mFont }; var fragLst = new List() { fragment }; - ITextShaper shaper = new TextShaper(font); + ITextShaper shaper = new TextShaper(SystemFontsEngine, font); using var layout = new TextLayoutEngine(shaper); var wrappedLines = layout.WrapRichText(fragLst, 54); @@ -447,7 +447,7 @@ public void WrapRichTextDifficultCase() lap = sw.ElapsedMilliseconds; - var shaper = new TextShaper(startFont); + var shaper = new TextShaper(SystemFontsEngine, startFont); lap = sw.ElapsedMilliseconds; @@ -551,11 +551,11 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrap() List comparatorLst = new() { "Strike", "Goudy size"}; var font = SystemFontsEngine.LoadFont("Aptos Narrow", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = new TextShaper(SystemFontsEngine, font); var points1 = shaper.MeasureTextInPoints(comparatorLst[0], 11); var font2 = SystemFontsEngine.LoadFont("Goudy Stout", FontSubFamily.Regular); - var otherShaper = new TextShaper(font2); + var otherShaper = new TextShaper(SystemFontsEngine, font2); var points2 = otherShaper.MeasureTextInPoints(comparatorLst[1], 16); @@ -586,7 +586,7 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrap() var startFont = SystemFontsEngine.LoadFont(font11.FontFamily, GetFontSubType(font11.Style)); var goudyFont = SystemFontsEngine.LoadFont(font22.FontFamily, GetFontSubType(font22.Style)); - ITextShaper shaper2 = new TextShaper(font); + ITextShaper shaper2 = new TextShaper(SystemFontsEngine, font); using var layoutEngine = new TextLayoutEngine(shaper); var wrappedLines = layoutEngine.WrapRichTextLines(comparatorFragments, 225d); @@ -604,11 +604,11 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrapAndSpaceTrail() List comparatorLst = new() { "Strike", "Goudy size " }; var font = SystemFontsEngine.LoadFont("Aptos Narrow", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = new TextShaper(SystemFontsEngine, font); var points1 = shaper.MeasureTextInPoints(comparatorLst[0], 11); var font2 = SystemFontsEngine.LoadFont("Goudy Stout", FontSubFamily.Regular); - var otherShaper = new TextShaper(font2); + var otherShaper = new TextShaper(SystemFontsEngine, font2); var points2 = otherShaper.MeasureTextInPoints(comparatorLst[1], 16); @@ -636,10 +636,10 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrapAndSpaceTrail() comparatorFragments.Add(frag1); comparatorFragments.Add(frag2); - var startFont = OpenTypeFonts.LoadFont(font11.FontFamily, GetFontSubType(font11.Style), FontFolders); - var goudyFont = OpenTypeFonts.LoadFont(font22.FontFamily, GetFontSubType(font22.Style), FontFolders); + var startFont = SystemFontsEngine.LoadFont(font11.FontFamily, GetFontSubType(font11.Style)); + var goudyFont = SystemFontsEngine.LoadFont(font22.FontFamily, GetFontSubType(font22.Style)); - ITextShaper shaper2 = new TextShaper(font); + ITextShaper shaper2 = new TextShaper(SystemFontsEngine, font); using var layoutEngine = new TextLayoutEngine(shaper); var wrappedLines = layoutEngine.WrapRichTextLines(comparatorFragments, 225d); @@ -697,9 +697,9 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() } var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); - var startFont = OpenTypeFonts.LoadFont(font2.FontFamily, GetFontSubType(font2.Style)); + var startFont = SystemFontsEngine.LoadFont(font2.FontFamily, GetFontSubType(font2.Style)); - var shaper = new TextShaper(startFont); + var shaper = new TextShaper(SystemFontsEngine, startFont); var layout = new TextLayoutEngine(shaper); var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); @@ -763,7 +763,7 @@ public void TestParagraphs() Assert.AreEqual(lstOfRichText[1], styleRuns[1]); - var layout = OpenTypeFonts.GetTextLayoutEngineForFont(font); + var layout = SystemFontsEngine.GetTextLayoutEngineForFont(font); var wrappedLines = layout.WrapRichTextLines(fragments, 225d); var wrappedLinesPara = paragraph.Wrap(FontFolders, 225d); @@ -986,7 +986,7 @@ public void WrapRichTextDifficultCaseCompare() var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); var startFont = SystemFontsEngine.LoadFont(font1.FontFamily, GetFontSubType(font1.Style)); - var shaper = new TextShaper(startFont); + var shaper = new TextShaper(SystemFontsEngine, startFont); var layout = new TextLayoutEngine(shaper); var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); @@ -1039,7 +1039,7 @@ public void WrapRichText_WordSpanningFragments_MeasuresCorrectly() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = new TextShaper(SystemFontsEngine, font); var layout = new TextLayoutEngine(shaper); // "Hello" split across two fragments with different fonts @@ -1072,7 +1072,7 @@ public void WrapRichText_WithLineBreaks_PreservesBreaks() RequireFont(SystemFontsEngine, "Arial"); // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = new TextShaper(SystemFontsEngine, font); var layout = new TextLayoutEngine(shaper); var fragments = new List @@ -1106,7 +1106,7 @@ public void WrapRichText_EmptyFragments_HandlesGracefully() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = new TextShaper(SystemFontsEngine, font); var layout = new TextLayoutEngine(shaper); var fragments = new List @@ -1143,7 +1143,7 @@ public void WrapRichText_NullFragmentList_ReturnsEmptyLine() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = new TextShaper(SystemFontsEngine, font); var layout = new TextLayoutEngine(shaper); // Act @@ -1162,8 +1162,8 @@ public void WrapRichText_NullFragmentList_ReturnsEmptyLine() public void WrapRichText_SameFontMultipleTimes_UsesCache() { // Arrange - var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); - var shaper = new TextShaper(font); + var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); + var shaper = new TextShaper(SystemFontsEngine, font); var layout = new TextLayoutEngine(shaper); var fragments = new List @@ -1202,11 +1202,11 @@ public void WrapText_Continous_Long_Word() { RequireFont(SystemFontsEngine, "Aptos Narrow"); - var font = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular); + var font = SystemFontsEngine.LoadFont("Aptos Narrow", FontSubFamily.Regular); var longWord = "pellentesquer"; - ITextShaper shaper = new TextShaper(font); + ITextShaper shaper = new TextShaper(SystemFontsEngine, font); using var layoutEngine = new TextLayoutEngine(shaper); var wrappedLines = layoutEngine.WrapText( longWord, @@ -1226,7 +1226,7 @@ public void WrapRichText_MeasureCorrectly() // Arrange var font = SystemFontsEngine.LoadFont("Aptos Narrow", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var shaper = new TextShaper(SystemFontsEngine,font); var layout = new TextLayoutEngine(shaper); var fragments = new List @@ -1263,7 +1263,7 @@ public void VerifyWrappingSingleChar() var maxWidthPt = 31.8125234375d; var gottenFont = SystemFontsEngine.LoadFont("Aptos Narrow", FontSubFamily.Regular); - var shaper = new TextShaper(gottenFont); + var shaper = new TextShaper(SystemFontsEngine, gottenFont); var layout = new TextLayoutEngine(shaper); List fragments = new List() { new TextFragment() { Font = font1, Text = lstOfRichText[0] } }; diff --git a/src/EPPlus.Fonts.OpenType.Tests/Scripts/UnicodeScriptClassifierTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Scripts/UnicodeScriptClassifierTests.cs new file mode 100644 index 000000000..4d7e828e9 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/Scripts/UnicodeScriptClassifierTests.cs @@ -0,0 +1,58 @@ +ο»Ώ/************************************************************************************************* + 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 + ************************************************************************************************* + 05/20/2026 EPPlus Software AB Boundary tests for binary-search classifier + *************************************************************************************************/ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml.Interfaces.Fonts; + +namespace EPPlus.Fonts.OpenType.Tests +{ + /// + /// Boundary tests for . + /// + /// We do not test every script's content β€” that would be source-code duplication, since + /// the classifier is effectively a static lookup table. Instead we verify that the binary + /// search treats range endpoints as inclusive on both sides, which protects against + /// off-by-one bugs if the algorithm is rewritten. + /// + /// Functional correctness of the table is exercised indirectly by higher-level tests + /// (DefaultFontProvider routing emoji to Noto Emoji, etc.). + /// + [TestClass] + public class UnicodeScriptClassifierTests : FontTestBase + { + public override TestContext? TestContext { get; set; } + + [TestMethod] + public void Classify_CodePointOnRangeStart_IsInclusive() + { + // U+4E00 is the first code point of the CJK Unified Ideographs main block. + // If the binary search treats Start as exclusive, this returns Unknown. + Assert.AreEqual(UnicodeScript.Han, UnicodeScriptClassifier.OfCodePoint(0x4E00)); + } + + [TestMethod] + public void Classify_CodePointOnRangeEnd_IsInclusive() + { + // U+9FFF is the last code point of the CJK Unified Ideographs main block. + // If the binary search treats End as exclusive, this returns Unknown. + Assert.AreEqual(UnicodeScript.Han, UnicodeScriptClassifier.OfCodePoint(0x9FFF)); + } + + [TestMethod] + public void Classify_CodePointJustOutsideRange_ReturnsUnknown() + { + // U+036F is the last Combining Diacritical Mark; U+0370 starts Greek. + // Verifies that the classifier does not over-shoot a range on the high side. + Assert.AreEqual(UnicodeScript.Unknown, UnicodeScriptClassifier.OfCodePoint(0x036F)); + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/SubsettingEdgeCasesTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/SubsettingEdgeCasesTests.cs index 524744cf4..9808c7586 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.LoadFont("Roboto"); + var font = TestFolderEngine.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.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var subset = font.CreateSubset((char[])null); } [TestMethod] public void Subset_SingleChar_ShouldHaveMinimalGlyphs() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.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.LoadFont("Roboto"); + var font = TestFolderEngine.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.LoadFont("Roboto"); + var font = TestFolderEngine.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.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); // Get ALL characters from cmap var allChars = new HashSet(); @@ -115,12 +115,12 @@ public void Subset_AllGlyphs_ShouldBeSimilarSizeToOriginal() public void Subset_PreservesRobotoKerning() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var builder = new SubsetFontBuilder(); // Act var subset = font.CreateSubset("AV"); - var shaper = new TextShaper(subset); + var shaper = new TextShaper(TestFolderEngine, subset); var result = shaper.Shape("AV"); // Assert diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ChainingContextualSubstitutionTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ChainingContextualSubstitutionTests.cs index ac06672ea..49b793c6d 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ChainingContextualSubstitutionTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ChainingContextualSubstitutionTests.cs @@ -28,8 +28,8 @@ public class ChainingContextualSubstitutionTests : FontTestBase public void ChainingContextual_Roboto_FfiLigature_Office() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.Shape("office"); @@ -43,8 +43,8 @@ public void ChainingContextual_Roboto_FfiLigature_Office() public void ChainingContextual_Roboto_FfiLigature_AtStart() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); // Act - ffi at the beginning of text (no backtrack context) var result = shaper.Shape("fficer"); @@ -58,8 +58,8 @@ public void ChainingContextual_Roboto_FfiLigature_AtStart() public void ChainingContextual_Roboto_FfiLigature_AtEnd() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act - ffi at the end of text (no lookahead context) var result = shaper.Shape("offi"); @@ -73,8 +73,8 @@ public void ChainingContextual_Roboto_FfiLigature_AtEnd() public void ChainingContextual_Roboto_MultipleFfiLigatures() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); // Act - Multiple ffi sequences in same text var result = shaper.Shape("office officer"); @@ -93,9 +93,9 @@ public void ChainingContextual_Roboto_MultipleFfiLigatures() public void ChainingContextual_Roboto_Type6BeforeType4_CorrectOrder() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var subset = font.CreateSubset("office fit"); - var shaper = new TextShaper(subset); + var shaper = new TextShaper(TestFolderEngine,subset); // Act - Text with both ffi (Type 6) and fi (Type 4) ligatures var result = shaper.Shape("office fit"); @@ -114,8 +114,8 @@ public void ChainingContextual_Roboto_Type6BeforeType4_CorrectOrder() public void ChainingContextual_Roboto_FfiLigature_HasCorrectMetrics() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); // Act var result = shaper.Shape("ffi"); diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs index b5e31d670..337e76e8c 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs @@ -26,9 +26,9 @@ public void TestSetup() [TestMethod] public void MarkToBaseTest() { - var font = OpenTypeFonts.LoadFont("EB Garamond", FontSubFamily.Regular, ignoreCache: true); + var font = TestFolderEngine.LoadFont("EB Garamond", FontSubFamily.Regular, ignoreCache: true); - var shaper = new TextShaper(font); + var shaper = new TextShaper(TestFolderEngine,font); string test = "e\u0301"; // e + combining acute var shaped = shaper.Shape(test, ShapingOptions.Full); diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ShapeLightTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ShapeLightTests.cs index 5bc571010..adc209f87 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ShapeLightTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/ShapeLightTests.cs @@ -28,8 +28,8 @@ public class ShapeLightTests : FontTestBase public void ShapeLight_SimpleText_ReturnsSameGlyphCountAsShape() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); // Act var full = shaper.Shape("Hello"); @@ -44,8 +44,8 @@ public void ShapeLight_SimpleText_ReturnsSameGlyphCountAsShape() public void ShapeLight_SimpleText_HasFontUnitsPerEm() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); // Act var result = shaper.ShapeLight("Hello"); @@ -60,8 +60,8 @@ public void ShapeLight_SimpleText_HasFontUnitsPerEm() public void ShapeLight_EmojiOnly_UsesFallbackFont() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.ShapeLight("πŸ˜€πŸ˜πŸ˜‚"); @@ -69,12 +69,13 @@ public void ShapeLight_EmojiOnly_UsesFallbackFont() // Assert Assert.AreEqual(3, result.Glyphs.Length, "Should have 3 glyphs for 3 emojis"); Assert.IsNotNull(result.FontUnitsPerEm); - Assert.IsTrue(result.FontUnitsPerEm.Length >= 1, "Should have at least one font"); + Assert.IsTrue(result.FontUnitsPerEm.Length >= 2, "Should have at least primary + emoji fallback"); - // All glyphs should have FontId 0 (the only used font is emoji fallback) + // Primary font is always registered at FontId 0. Emoji glyphs come from a fallback, + // so they should have FontId != 0. foreach (var glyph in result.Glyphs) { - Assert.AreEqual(0, glyph.FontId, "All emoji glyphs should be FontId 0"); + Assert.AreNotEqual(0, glyph.FontId, "Emoji glyphs should come from fallback, not primary"); Assert.IsTrue(glyph.XAdvance > 0, "Emoji glyphs should have positive width"); } } @@ -83,8 +84,8 @@ public void ShapeLight_EmojiOnly_UsesFallbackFont() public void ShapeLight_MixedTextAndEmoji_HasMultipleFonts() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.ShapeLight("Hi πŸ˜€ there"); @@ -105,8 +106,8 @@ public void ShapeLight_MixedTextAndEmoji_HasMultipleFonts() public void ShapeLight_GetWidthInPoints_ConsistentWithShape() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); float fontSize = 12f; // Act @@ -125,8 +126,8 @@ public void ShapeLight_GetWidthInPoints_ConsistentWithShape() public void ShapeLight_FillCharWidths_ProducesCorrectWidths() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); string text = "ABC"; float fontSize = 12f; var charWidths = new double[text.Length]; @@ -153,8 +154,8 @@ public void ShapeLight_FillCharWidths_ProducesCorrectWidths() public void ShapeLight_FillCharWidths_MixedEmoji_CorrectPerGlyphScale() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); string text = "AπŸ˜€B"; float fontSize = 12f; var charWidths = new double[text.Length]; @@ -182,8 +183,8 @@ public void ShapeLight_FillCharWidths_MixedEmoji_CorrectPerGlyphScale() public void ShapeLight_EmptyString_ReturnsEmptyResult() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.ShapeLight(""); @@ -199,8 +200,8 @@ public void ShapeLight_EmptyString_ReturnsEmptyResult() public void ShapeLight_TextEmojiAndMath_UsesThreeFonts() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // U+1D400 = 𝐀 (Mathematical Bold Capital A) β€” not in Roboto or Noto Emoji, // should fall back to Noto Sans Math. diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleAdjustmentsTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleAdjustmentsTests.cs index a9bdb7c13..b8800e80d 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleAdjustmentsTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleAdjustmentsTests.cs @@ -14,8 +14,8 @@ public class SingleAdjustmentTests : FontTestBase [TestMethod] public void SingleAdjustment_Roboto_DoesNotCrash() { - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); var result = shaper.Shape("Hello World"); @@ -27,8 +27,8 @@ public void SingleAdjustment_Roboto_DoesNotCrash() [TestMethod] public void SingleAdjustment_WithZeroValues_DoesNotAffectOutput() { - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); var withPositioning = shaper.Shape("AV"); var withoutPositioning = shaper.Shape("AV", ShapingOptions.None); @@ -41,8 +41,8 @@ public void SingleAdjustment_WithZeroValues_DoesNotAffectOutput() [TestMethod] public void SingleAdjustment_WithZeroValues_DoesNotAffectOutput2() { - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); var withPositioning = shaper.Shape("AV"); var withoutPositioning = shaper.Shape("AV", ShapingOptions.None); @@ -67,8 +67,8 @@ public void SingleAdjustment_WithZeroValues_DoesNotAffectOutput2() [TestMethod] public void Kerning_IsApplied_ForAVPair() { - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); var optionsOnlyKern = new ShapingOptions { @@ -91,13 +91,13 @@ public void SingleAdjustment_Verdana_HasRealAdjustments() { try { - var font = OpenTypeFonts.LoadFont("Verdana"); + var font = SystemFontsEngine.LoadFont("Verdana"); if (font == null || font.FullName != "Verdana") { Assert.Inconclusive("Verdana font not found - test skipped"); return; } - var shaper = new TextShaper(font); + var shaper = new TextShaper(SystemFontsEngine, font); var withPositioning = shaper.Shape("Hello123"); var withoutPositioning = shaper.Shape("Hello123", ShapingOptions.None); @@ -129,13 +129,13 @@ public void SingleAdjustment_Verdana_AdjustmentsAppliedWithDefaultOptions() { try { - var font = OpenTypeFonts.LoadFont("Verdana"); + var font = SystemFontsEngine.LoadFont("Verdana"); if (font == null || font.FullName != "Verdana") { Assert.Inconclusive("Verdana font not found - test skipped"); return; } - var shaper = new TextShaper(font); + var shaper = new TextShaper(SystemFontsEngine, font); var result = shaper.Shape("Test"); @@ -152,8 +152,8 @@ public void SingleAdjustment_Verdana_AdjustmentsAppliedWithDefaultOptions() [TestMethod] public void SingleAdjustment_AppliedBeforeKerning() { - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); var result = shaper.Shape("Test"); @@ -164,8 +164,8 @@ public void SingleAdjustment_AppliedBeforeKerning() [TestMethod] public void SingleAdjustment_NotAppliedWithNoneOptions() { - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); var result = shaper.Shape("Test", ShapingOptions.None); @@ -182,8 +182,8 @@ public void SingleAdjustment_NotAppliedWithNoneOptions() [TestMethod] public void SingleAdjustmentProvider_HandlesNullFont() { - var font = OpenTypeFonts.LoadFont("SourceSans3"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("SourceSans3"); + var shaper = new TextShaper(TestFolderEngine,font); var result = shaper.Shape("Test"); diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleSubstitutionTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleSubstitutionTests.cs index b1dceb99a..19fa6e100 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleSubstitutionTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/SingleSubstitutionTests.cs @@ -38,7 +38,7 @@ public void SingleSubstitution_SmallCaps_SubstitutesGlyphs() { try { - var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); + var font = TestFolderEngine.LoadFont(fontName, FontSubFamily.Regular); // Verify font has smcp feature with Type 1 lookup if (font == null || !font.FullName.Contains(fontName) || font.GsubTable == null) @@ -72,7 +72,7 @@ public void SingleSubstitution_SmallCaps_SubstitutesGlyphs() } // Found a font with smcp using Type 1! - var shaper = new TextShaper(font); + var shaper = new TextShaper(TestFolderEngine, font); // Act - lowercase letters should become small caps var normal = shaper.Shape("hello", ShapingOptions.Default); @@ -123,7 +123,7 @@ public void SingleSubstitution_AppliesBeforeLigatures() { try { - var font = OpenTypeFonts.LoadFont(fontName); + var font = TestFolderEngine.LoadFont(fontName); if (font == null || !font.FullName .Contains(fontName) || font.GsubTable == null) continue; @@ -157,7 +157,7 @@ public void SingleSubstitution_AppliesBeforeLigatures() if (!hasSmcpWithType1 || !hasLiga) continue; - var shaper = new TextShaper(font); + var shaper = new TextShaper(TestFolderEngine, font); // Act - Apply both features (single substitution should happen first) var bothFeatures = shaper.Shape("office", new ShapingOptions diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs index 55b0d5ca4..2a528b111 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs @@ -31,8 +31,8 @@ public class TextShaperTests : FontTestBase public void Shape_EmptyString_ReturnsEmptyResult() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); // Act var shaped = shaper.Shape(""); @@ -48,8 +48,8 @@ public void Shape_EmptyString_ReturnsEmptyResult() public void Shape_NullString_ReturnsEmptyResult() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var shaped = shaper.Shape(null); @@ -64,8 +64,8 @@ public void Shape_NullString_ReturnsEmptyResult() public void Shape_SingleCharacter_ReturnsOneGlyph() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var shaped = shaper.Shape("A"); @@ -82,8 +82,8 @@ public void Shape_SingleCharacter_ReturnsOneGlyph() public void Shape_SimpleWord_ReturnsCorrectGlyphCount() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); // Act var shaped = shaper.Shape("Hello"); @@ -99,8 +99,8 @@ public void Shape_SimpleWord_ReturnsCorrectGlyphCount() public void Shape_WithSpace_IncludesSpaceGlyph() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); // Act var shaped = shaper.Shape("A B"); @@ -118,7 +118,7 @@ public void Shape_WithSpace_IncludesSpaceGlyph() public void Shape_WithKerning_ReducesWidth() { // Arrange - var shaper = OpenTypeFonts.GetTextShaper("Roboto", FontSubFamily.Regular); + var shaper = TestFolderEngine.GetTextShaper("Roboto", FontSubFamily.Regular); // Act var withKerning = shaper.Shape("WAVE", ShapingOptions.Default); @@ -133,7 +133,7 @@ public void Shape_WithKerning_ReducesWidth() public void Debug_GposKerningFormat() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); Assert.IsNotNull(font.GposTable, "Should have GPOS"); @@ -176,7 +176,7 @@ public void Debug_GposKerningFormat() public void Shape_AVPair_HasNegativeKerning() { // Arrange - var shaper = OpenTypeFonts.GetTextShaper("Roboto", FontSubFamily.Regular); + var shaper = TestFolderEngine.GetTextShaper("Roboto", FontSubFamily.Regular); // Act var withKerning = shaper.Shape("AV"); @@ -195,7 +195,7 @@ public void Shape_AVPair_HasNegativeKerning() public void Shape_FastOption_StillAppliesKerning() { // Arrange - var shaper = OpenTypeFonts.GetTextShaper("Roboto", FontSubFamily.Regular); + var shaper = TestFolderEngine.GetTextShaper("Roboto", FontSubFamily.Regular); // Act var fast = shaper.Shape("WAVE", ShapingOptions.Fast); @@ -214,8 +214,8 @@ public void Shape_FastOption_StillAppliesKerning() public void MeasureTextInPoints_ReturnsReasonableValue() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto", FontSubFamily.Regular); + var shaper = new TextShaper(TestFolderEngine, font); // Act float width = shaper.MeasureTextInPoints("Hello", 12); @@ -229,8 +229,8 @@ public void MeasureTextInPoints_ReturnsReasonableValue() public void MeasureTextInPixels_ScalesWithDpi() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto", FontSubFamily.Regular); + var shaper = new TextShaper(TestFolderEngine, font); // Act float width72 = shaper.MeasureTextInPixels("Hello", 12, 72); @@ -245,8 +245,8 @@ public void MeasureTextInPixels_ScalesWithDpi() public void MeasureText_LargerFontSize_LargerWidth() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto", FontSubFamily.Regular); + var shaper = new TextShaper(TestFolderEngine, font); // Act float width12 = shaper.MeasureTextInPoints("Hello", 12); @@ -265,8 +265,8 @@ public void MeasureText_LargerFontSize_LargerWidth() public void Shape_GlyphsHaveClusterIndices() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var shaped = shaper.Shape("ABC"); @@ -281,8 +281,8 @@ public void Shape_GlyphsHaveClusterIndices() public void Shape_GlyphsHaveCharCount() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var shaped = shaper.Shape("ABC"); @@ -298,8 +298,8 @@ public void Shape_GlyphsHaveCharCount() public void Shape_GlyphsHaveValidIds() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var shaped = shaper.Shape("Hello"); @@ -320,8 +320,8 @@ public void Shape_GlyphsHaveValidIds() public void ShapeLines_SingleLine_ReturnsOneElement() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var lines = shaper.ShapeLines("Hello"); @@ -335,8 +335,8 @@ public void ShapeLines_SingleLine_ReturnsOneElement() public void ShapeLines_TwoLinesWithLF_ReturnsTwoElements() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto", FontSubFamily.Regular); + var shaper = new TextShaper(TestFolderEngine, font); // Act var lines = shaper.ShapeLines("Hello\nWorld"); @@ -351,8 +351,8 @@ public void ShapeLines_TwoLinesWithLF_ReturnsTwoElements() public void ShapeLines_TwoLinesWithCRLF_ReturnsTwoElements() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto", FontSubFamily.Regular); + var shaper = new TextShaper(TestFolderEngine, font); // Act var lines = shaper.ShapeLines("Hello\r\nWorld"); @@ -367,8 +367,8 @@ public void ShapeLines_TwoLinesWithCRLF_ReturnsTwoElements() public void ShapeLines_EmptyLine_PreservesEmptyLine() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto", FontSubFamily.Regular); + var shaper = new TextShaper(TestFolderEngine, font); // Act var lines = shaper.ShapeLines("Hello\n\nWorld"); @@ -384,8 +384,8 @@ public void ShapeLines_EmptyLine_PreservesEmptyLine() public void MeasureLines_SingleLine_MatchesSingleMeasurement() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var metrics = shaper.MeasureLines("Hello", 12); @@ -400,8 +400,8 @@ public void MeasureLines_SingleLine_MatchesSingleMeasurement() public void MeasureLines_TwoLines_WidthIsMaxOfBoth() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var metrics = shaper.MeasureLines("Hi\nHello", 12); @@ -417,8 +417,8 @@ public void MeasureLines_TwoLines_WidthIsMaxOfBoth() public void MeasureLines_TwoLines_HeightIsDoubleLineHeight() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var metrics = shaper.MeasureLines("Hello\nWorld", 12); @@ -437,7 +437,7 @@ public void MeasureLines_TwoLines_HeightIsDoubleLineHeight() public void GetLineHeightInPoints_ReturnsPositiveValue() { // Arrange - var shaper = OpenTypeFonts.GetTextShaper("Roboto", FontSubFamily.Regular); + var shaper = TestFolderEngine.GetTextShaper("Roboto", FontSubFamily.Regular); // Act float lineHeight = shaper.GetLineHeightInPoints(12); @@ -452,7 +452,7 @@ public void GetLineHeightInPoints_ReturnsPositiveValue() public void GetFontHeightInPoints_ReturnsPositiveValue() { // Arrange - var shaper = OpenTypeFonts.GetTextShaper("Roboto", FontSubFamily.Regular); + var shaper = TestFolderEngine.GetTextShaper("Roboto", FontSubFamily.Regular); // Act float fontHeight = shaper.GetFontHeightInPoints(12); @@ -467,7 +467,7 @@ public void GetFontHeightInPoints_ReturnsPositiveValue() public void GetLineHeight_IsGreaterThanFontHeight() { // Arrange - var shaper = OpenTypeFonts.GetTextShaper("Roboto", FontSubFamily.Regular); + var shaper = TestFolderEngine.GetTextShaper("Roboto", FontSubFamily.Regular); // Act float lineHeight = shaper.GetLineHeightInPoints(12); @@ -482,7 +482,7 @@ public void GetLineHeight_IsGreaterThanFontHeight() public void GetLineHeight_ScalesWithFontSize() { // Arrange - var shaper = OpenTypeFonts.GetTextShaper("Roboto", FontSubFamily.Regular); + var shaper = TestFolderEngine.GetTextShaper("Roboto", FontSubFamily.Regular); // Act float height12 = shaper.GetLineHeightInPoints(12); @@ -501,8 +501,8 @@ public void GetLineHeight_ScalesWithFontSize() public void ShapedText_GetWidthInPoints_MatchesMeasureText() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var shaped = shaper.Shape("Hello"); @@ -517,8 +517,8 @@ public void ShapedText_GetWidthInPoints_MatchesMeasureText() public void ShapedText_GetWidthInPixels_MatchesMeasureText() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); // Act var shaped = shaper.Shape("Hello"); @@ -537,8 +537,8 @@ public void ShapedText_GetWidthInPixels_MatchesMeasureText() public void Shape_OnlySpaces_ReturnsGlyphs() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var shaped = shaper.Shape(" "); @@ -552,8 +552,8 @@ public void Shape_OnlySpaces_ReturnsGlyphs() public void Shape_SpecialCharacters_HandlesGracefully() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var shaped = shaper.Shape("@#$%"); @@ -567,8 +567,8 @@ public void Shape_SpecialCharacters_HandlesGracefully() public void Shape_Numbers_ReturnsCorrectGlyphs() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var shaped = shaper.Shape("12345"); @@ -585,8 +585,8 @@ public void Shape_Numbers_ReturnsCorrectGlyphs() public void Shape_FiLigature_CombinesTwoGlyphs() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var withLigatures = shaper.Shape("fi", ShapingOptions.Default); @@ -601,8 +601,8 @@ public void Shape_FiLigature_CombinesTwoGlyphs() public void Shape_Office_HasFfiLigature() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.Shape("office"); @@ -616,8 +616,8 @@ public void Shape_Office_HasFfiLigature() public void Shape_Ligature_PreservesClusterIndex() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.Shape("afi"); @@ -635,7 +635,7 @@ public void Shape_Ligature_PreservesClusterIndex() public void Shape_DecomposedUnicode_PositionsAccent() { // Arrange - var shaper = OpenTypeFonts.GetTextShaper("Roboto", FontSubFamily.Regular); + var shaper = TestFolderEngine.GetTextShaper("Roboto", FontSubFamily.Regular); // Act // U+0065 = 'e', U+0301 = combining acute accent @@ -663,7 +663,7 @@ public void Shape_DecomposedUnicode_PositionsAccent() public void Shape_PrecomposedVsDecomposed_SimilarWidth() { // Arrange - var shaper = OpenTypeFonts.GetTextShaper("Roboto", FontSubFamily.Regular); + var shaper = TestFolderEngine.GetTextShaper("Roboto", FontSubFamily.Regular); // Act var precomposed = shaper.Shape("\u00e9"); // Γ© (single codepoint) @@ -686,7 +686,7 @@ public void Shape_PrecomposedVsDecomposed_SimilarWidth() public void Shape_SourceSans3_SingleMark_PositionsCorrectly() { // Arrange - var shaper = OpenTypeFonts.GetTextShaper("SourceSans3"); + var shaper = TestFolderEngine.GetTextShaper("SourceSans3"); // Act - Single combining mark var result = shaper.Shape("e\u0301"); // e + combining acute (Γ©) @@ -714,7 +714,7 @@ public void Shape_SourceSans3_SingleMark_PositionsCorrectly() public void Shape_Cafe_HandlesDecomposed() { // Arrange - var shaper = OpenTypeFonts.GetTextShaper("SourceSans3"); + var shaper = TestFolderEngine.GetTextShaper("SourceSans3"); // Act - "cafΓ©" with decomposed Γ© var result = shaper.Shape("cafe\u0301"); @@ -734,7 +734,7 @@ public void Shape_Cafe_HandlesDecomposed() [TestMethod] public void Debug_OpenSans_MarkFeature() { - var font = OpenTypeFonts.LoadFont("OpenSans", FontSubFamily.Regular); + var font = TestFolderEngine.LoadFont("OpenSans", FontSubFamily.Regular); foreach (var featureRecord in font.GposTable.FeatureList.FeatureRecords) { diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs index 55ad5d0f4..c2817363b 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs @@ -34,8 +34,8 @@ public static void ClassInitialize(TestContext ctx) public void ShapeVertical_CjkText_ReturnsOneGlyphPerCharacter() { // Arrange - BIZ UDGothic has vmtx and is a CJK font - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.ShapeVertical("ζ—₯本θͺž"); @@ -49,8 +49,8 @@ public void ShapeVertical_CjkText_ReturnsOneGlyphPerCharacter() public void ShapeVertical_CjkText_GlyphsHavePositiveYAdvance() { // Arrange - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine,font); // Act var result = shaper.ShapeVertical("ζ—₯本θͺž"); @@ -67,8 +67,8 @@ public void ShapeVertical_CjkText_GlyphsHavePositiveYAdvance() public void ShapeVertical_CjkText_TotalAdvanceHeightIsPositive() { // Arrange - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.ShapeVertical("γƒ†γ‚Ήγƒˆ"); @@ -83,7 +83,7 @@ public void ShapeVertical_EmptyString_ReturnsEmptyGlyphArray() { // Arrange var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.ShapeVertical(string.Empty); @@ -99,7 +99,7 @@ public void ShapeVertical_ClusterIndexMatchesCharacterPosition() { // Arrange var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var shaper = new TextShaper(TestFolderEngine, font); var text = "ABC"; // Act @@ -122,8 +122,8 @@ public void ShapeVertical_ClusterIndexMatchesCharacterPosition() public void ShapeLightVertical_CjkText_ReturnsOneEntryPerCharacter() { // Arrange - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.ShapeLightVertical("ζ—₯本θͺž"); @@ -137,8 +137,8 @@ public void ShapeLightVertical_CjkText_ReturnsOneEntryPerCharacter() public void ShapeLightVertical_CjkText_YAdvanceMatchesShapeVertical() { // Arrange - ShapeLightVertical should produce identical YAdvance values to ShapeVertical - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine, font); var text = "東京"; // Act @@ -158,8 +158,8 @@ public void ShapeLightVertical_CjkText_YAdvanceMatchesShapeVertical() public void ShapeLightVertical_EmptyString_ReturnsEmptyArray() { // Arrange - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine,font); // Act var result = shaper.ShapeLightVertical(string.Empty); @@ -178,8 +178,8 @@ 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.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine,font); Assert.IsNull(font.VmtxTable, "Roboto should not have a vmtx table"); // Act @@ -200,8 +200,8 @@ public void ShapeVertical_FontWithoutVmtx_FallsBackToHmtxAdvanceWidth() public void ShapeVertical_FontWithoutVmtx_YAdvanceMatchesHmtxAdvanceWidth() { // Arrange - var font = OpenTypeFonts.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.ShapeVertical("A"); @@ -221,8 +221,8 @@ public void ShapeVertical_FontWithoutVmtx_YAdvanceMatchesHmtxAdvanceWidth() public void ShapeVertical_OriginalTextIsPreserved() { // Arrange - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine, font); var text = "ζ—₯本θͺžγƒ†γ‚Ήγƒˆ"; // Act @@ -236,8 +236,8 @@ public void ShapeVertical_OriginalTextIsPreserved() public void ShapeVertical_PrimaryFontGlyphs_HaveFontIdZero() { // Arrange - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.ShapeVertical("ζ—₯本θͺž"); @@ -258,8 +258,8 @@ public void ShapeVertical_PrimaryFontGlyphs_HaveFontIdZero() public void ShapeVertical_SurrogatePair_ProducesOneGlyphWithCharCountTwo() { // Arrange - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine,font); // U+20B9F 𠺟 - a CJK unified ideograph extension B character (surrogate pair in UTF-16) var text = "\uD842\uDF9F"; @@ -281,8 +281,8 @@ public void ShapeVertical_SurrogatePair_ProducesOneGlyphWithCharCountTwo() public void ShapeLightVertical_SurrogatePair_ProducesOneEntryWithCharCountTwo() { // Arrange - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine, font); var text = "\uD842\uDF9F"; // Act @@ -303,8 +303,8 @@ public void ShapeLightVertical_SurrogatePair_ProducesOneEntryWithCharCountTwo() public void ShapeVertical_CjkText_GlyphsHavePositiveAdvanceWidth() { // Arrange - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine, font); // Act var result = shaper.ShapeVertical("ζ—₯本θͺž"); @@ -322,10 +322,10 @@ public void ShapeVertical_AdvanceWidthMatchesHmtxAdvanceWidth() { // Arrange - AdvanceWidth on VerticalShapedGlyph must equal hmtx advanceWidth // since centering calculations depend on this value being accurate - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); + var shaper = new TextShaper(TestFolderEngine, font); - // Act + // Act var result = shaper.ShapeVertical("ζ—₯本"); // Assert @@ -343,8 +343,8 @@ 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.LoadFont("Roboto"); - var shaper = new TextShaper(font); + var font = TestFolderEngine.LoadFont("Roboto"); + var shaper = new TextShaper(TestFolderEngine, font); Assert.IsNull(font.VmtxTable, "Roboto should not have a vmtx table"); // Act diff --git a/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs b/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs index 6084e8580..34e4a2592 100644 --- a/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs +++ b/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs @@ -10,93 +10,255 @@ 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 + 05/20/2026 EPPlus Software AB Script-classified fallback via engine reference *************************************************************************************************/ +using EPPlus.Fonts.OpenType.FontResolver; +using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; namespace EPPlus.Fonts.OpenType { /// - /// 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. + /// Default font provider with script-classified glyph fallback. + /// + /// When a code point is missing from the primary font, the provider routes the lookup + /// based on the code point's Unicode script: + /// * Emoji β†’ embedded Noto Emoji (bundled with EPPlus) + /// * Math β†’ embedded Noto Math (bundled with EPPlus) + /// * Other β†’ per-script fallback chain configured on the engine (best-effort, + /// resolves named fonts via the engine β€” works only when the named fonts + /// are installed) + /// + /// Per-script chains and their fonts are lazy-loaded the first time a code point in + /// that script is encountered, then cached for the lifetime of this provider. /// public class DefaultFontProvider : IFontProvider { + private readonly OpenTypeFontEngine _engine; private readonly OpenTypeFont _primaryFont; - private readonly List _fallbackFonts; + + // Embedded fallbacks, lazy-loaded on first use. + private readonly LazyFallbackFont _notoEmoji; + private readonly LazyFallbackFont _notoMath; + + // Per-script named fallbacks, resolved via the engine on first use of each script. + // Inner list is the resolved chain of fonts for that script; entries that fail to + // resolve are omitted (so the list may be shorter than the configured chain). + private readonly Dictionary> _resolvedScriptChains + = new Dictionary>(); + + // Tracks which fallback fonts have actually been used (returned a glyph for some + // code point). Used by GetAllFonts to expose only the fonts that mattered, which + // matters for subsetting and PDF embedding. + private readonly HashSet _usedFallbacks = new HashSet(); + private readonly object _lock = new object(); + /// public OpenTypeFont PrimaryFont { get { return _primaryFont; } } /// - /// Creates a font provider with automatic embedded fallbacks. - /// Default fallback chain: Noto Emoji β†’ Noto Sans Math. + /// Creates a font provider that uses the given engine to resolve per-script fallback + /// fonts on demand. Both arguments are required. /// - /// The user's primary font - public DefaultFontProvider(OpenTypeFont primaryFont) + /// The engine to use for resolving named fallback fonts. + /// The primary font for text in the user's chosen typeface. + public DefaultFontProvider(OpenTypeFontEngine engine, OpenTypeFont primaryFont) { + if (engine == null) + throw new ArgumentNullException("engine"); if (primaryFont == null) throw new ArgumentNullException("primaryFont"); + _engine = engine; _primaryFont = primaryFont; - _fallbackFonts = new List - { - new LazyFallbackFont(EmbeddedFonts.LoadNotoEmoji), - new LazyFallbackFont(EmbeddedFonts.LoadNotoMath) - }; + _notoEmoji = new LazyFallbackFont(EmbeddedFonts.LoadNotoEmoji); + _notoMath = new LazyFallbackFont(EmbeddedFonts.LoadNotoMath); } + /// public bool TryGetGlyphFont(uint codePoint, out OpenTypeFont font, out ushort glyphId) { - // Try primary font first + // 1. Primary font wins whenever it has the glyph. if (_primaryFont.CmapTable.TryGetGlyphId(codePoint, out glyphId)) { font = _primaryFont; return true; } - // Try fallback fonts in order (lazy-loaded) - lock (_lock) + // 2. Classify the code point and route to the appropriate fallback. + var script = UnicodeScriptClassifier.OfCodePoint(codePoint); + + switch (script) { - foreach (var fallback in _fallbackFonts) - { - var fallbackFont = fallback.Font; - if (fallbackFont.CmapTable.TryGetGlyphId(codePoint, out glyphId)) - { - font = fallbackFont; + case UnicodeScript.Emoji: + if (TryGlyphInLazyFallback(_notoEmoji, codePoint, out font, out glyphId)) return true; - } - } + break; + + case UnicodeScript.Math: + if (TryGlyphInLazyFallback(_notoMath, codePoint, out font, out glyphId)) + return true; + break; + + case UnicodeScript.Unknown: + // No script classification β€” no useful fallback to route to. + break; + + default: + if (TryGlyphInScriptChain(script, codePoint, out font, out glyphId)) + return true; + break; } - // Not found - return primary with .notdef + // 3. Nothing found β€” return primary with .notdef. font = _primaryFont; glyphId = 0; return false; } + /// public IEnumerable GetAllFonts() { yield return _primaryFont; + // Only return fallback fonts that have actually been used. Subsetting and PDF + // embedding only need fonts whose glyphs the shaper actually placed. lock (_lock) { - foreach (var fallback in _fallbackFonts) + foreach (var f in _usedFallbacks) { - if (fallback.IsLoaded) - { - yield return fallback.Font; - } + yield return f; } } } + // ----------------------------------------------------------------------------------------- + // Internal helpers + // ----------------------------------------------------------------------------------------- + /// - /// Wraps a font loader delegate with lazy, thread-safe initialization. + /// Tries to find the glyph in a lazy-loaded embedded fallback font (Noto Emoji / Math). + /// + private bool TryGlyphInLazyFallback( + LazyFallbackFont lazy, + uint codePoint, + out OpenTypeFont font, + out ushort glyphId) + { + var fallbackFont = lazy.Font; // triggers load on first use (thread-safe inside) + if (fallbackFont.CmapTable.TryGetGlyphId(codePoint, out glyphId)) + { + font = fallbackFont; + MarkUsed(fallbackFont); + return true; + } + + font = null; + glyphId = 0; + return false; + } + + /// + /// Tries to find the glyph by walking the per-script fallback chain configured on + /// the engine. Resolves the chain lazily on first use of each script. + /// + private bool TryGlyphInScriptChain( + UnicodeScript script, + uint codePoint, + out OpenTypeFont font, + out ushort glyphId) + { + var chain = GetOrResolveScriptChain(script); + + foreach (var candidate in chain) + { + if (candidate.CmapTable.TryGetGlyphId(codePoint, out glyphId)) + { + font = candidate; + MarkUsed(candidate); + return true; + } + } + + font = null; + glyphId = 0; + return false; + } + + /// + /// Returns the resolved chain of fonts for a script. The first time a script is + /// queried, the configured chain of font names is read from the engine's configuration + /// and each name is resolved via the engine. Names that fail to resolve are omitted. + /// + private List GetOrResolveScriptChain(UnicodeScript script) + { + lock (_lock) + { + List resolved; + if (_resolvedScriptChains.TryGetValue(script, out resolved)) + return resolved; + + resolved = ResolveScriptChain(script); + _resolvedScriptChains[script] = resolved; + return resolved; + } + } + + /// + /// Reads the configured chain for a script from the engine and loads each named font. + /// + private List ResolveScriptChain(UnicodeScript script) + { + var result = new List(); + + var chainNames = _engine.GetScriptFallback(script); + if (chainNames == null || chainNames.Length == 0) + return result; + + foreach (var fontName in chainNames) + { + if (string.IsNullOrEmpty(fontName)) + continue; + + // Only accept exact matches β€” falling back from "Microsoft YaHei" to Archivo + // Narrow defeats the purpose of script fallback. We rely on the engine's + // availability check rather than blindly loading. + var availability = _engine.GetFontAvailability(fontName, FontSubFamily.Regular); + if (availability != FontAvailability.Exact) + continue; + + try + { + var font = _engine.LoadFont(fontName, FontSubFamily.Regular); + if (font != null) + result.Add(font); + } + catch + { + // If a named fallback fails to load for any reason, skip it silently. + // The chain is best-effort β€” we never want a fallback font's loading + // error to break primary text rendering. + } + } + + return result; + } + + private void MarkUsed(OpenTypeFont font) + { + lock (_lock) + { + _usedFallbacks.Add(font); + } + } + + /// + /// Wraps an embedded font loader with lazy, thread-safe initialization. /// private class LazyFallbackFont { @@ -109,17 +271,6 @@ 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 diff --git a/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs b/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs index bbf104f1a..e39aca5b5 100644 --- a/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs +++ b/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 02/27/2026 EPPlus Software AB Replaces FontResolutionConfig 05/06/2026 EPPlus Software AB Property-based transactional configuration + 05/20/2026 EPPlus Software AB Added per-script glyph fallback configuration *************************************************************************************************/ using OfficeOpenXml.Interfaces.Fonts; using System; @@ -29,10 +30,13 @@ internal class EpplusFontConfiguration : IEpplusFontConfiguration private readonly List _fontDirectories = new List(); private readonly Dictionary _fontFallbacks = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _scriptFallbacks = + new Dictionary(); public EpplusFontConfiguration() { SearchSystemDirectories = true; + ApplyDefaultScriptFallbacks(); } /// @@ -53,6 +57,21 @@ public IDictionary FontFallbacks get { return _fontFallbacks; } } + /// + public void SetScriptFallback(UnicodeScript script, params string[] fallbackFontNames) + { + if (fallbackFontNames == null) + { + _scriptFallbacks[script] = new string[0]; + return; + } + + // Copy to insulate against caller mutating the array after the call. + var copy = new string[fallbackFontNames.Length]; + Array.Copy(fallbackFontNames, copy, fallbackFontNames.Length); + _scriptFallbacks[script] = copy; + } + /// public void Reset() { @@ -60,10 +79,13 @@ public void Reset() SearchSystemDirectories = true; FontResolver = null; _fontFallbacks.Clear(); + _scriptFallbacks.Clear(); + ApplyDefaultScriptFallbacks(); } // ----------------------------------------------------------------------------------------- - // Internal API β€” consumed by DefaultFontResolver in the same assembly. + // Internal API β€” consumed by DefaultFontResolver and DefaultFontProvider + // in the same assembly. // ----------------------------------------------------------------------------------------- /// @@ -75,5 +97,72 @@ internal string[] GetFallbacks(string fontName) string[] result; return _fontFallbacks.TryGetValue(fontName, out result) ? result : null; } + + /// + /// Returns the configured fallback chain for the given Unicode script, or null if + /// none is configured. A returned empty array means fallback is explicitly disabled + /// for the script. + /// + internal string[] GetScriptFallback(UnicodeScript script) + { + string[] result; + return _scriptFallbacks.TryGetValue(script, out result) ? result : null; + } + + // ----------------------------------------------------------------------------------------- + // Default per-script chains + // + // These cover the scripts most likely to appear in Office documents. Each chain prefers + // platform-native fonts first (Windows / macOS), then Noto as a Linux / open-source + // fallback. Chains stay within a single language family β€” falling back from Japanese + // to Chinese, or vice versa, would render incorrect glyph forms for shared CJK + // ideographs. + // + // Emoji and Math are intentionally NOT in this table. They are served by EPPlus's + // bundled Noto Emoji and Noto Math fonts and are routed before per-script lookup. + // ----------------------------------------------------------------------------------------- + + private void ApplyDefaultScriptFallbacks() + { + _scriptFallbacks[UnicodeScript.Han] = new[] + { + "Microsoft YaHei", "SimSun", "Noto Sans CJK SC", "PingFang SC" + }; + + _scriptFallbacks[UnicodeScript.Hiragana] = new[] + { + "Yu Gothic", "MS Gothic", "Meiryo", "Noto Sans CJK JP" + }; + + _scriptFallbacks[UnicodeScript.Katakana] = new[] + { + "Yu Gothic", "MS Gothic", "Meiryo", "Noto Sans CJK JP" + }; + + _scriptFallbacks[UnicodeScript.Hangul] = new[] + { + "Malgun Gothic", "Gulim", "Noto Sans CJK KR" + }; + + _scriptFallbacks[UnicodeScript.Arabic] = new[] + { + "Segoe UI", "Tahoma", "Arial", "Noto Sans Arabic" + }; + + _scriptFallbacks[UnicodeScript.Hebrew] = new[] + { + "Segoe UI", "Tahoma", "Arial", "Noto Sans Hebrew" + }; + + _scriptFallbacks[UnicodeScript.Thai] = new[] + { + "Tahoma", "Leelawadee UI", "Noto Sans Thai" + }; + + _scriptFallbacks[UnicodeScript.Devanagari] = new[] + { + "Mangal", "Nirmala UI", "Noto Sans Devanagari" + }; + } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/FontSubsetManager.cs b/src/EPPlus.Fonts.OpenType/FontSubsetManager.cs index 969751703..6e0094ff5 100644 --- a/src/EPPlus.Fonts.OpenType/FontSubsetManager.cs +++ b/src/EPPlus.Fonts.OpenType/FontSubsetManager.cs @@ -44,8 +44,8 @@ public FontSubsetManager(IFontProvider sourceProvider) _sourceProvider = sourceProvider; } - public FontSubsetManager(OpenTypeFont font) - : this(new DefaultFontProvider(font)) + public FontSubsetManager(OpenTypeFontEngine engine, OpenTypeFont font) + : this(new DefaultFontProvider(engine, font)) { } diff --git a/src/EPPlus.Fonts.OpenType/Integration/OpenTypeFontTextMeasurer.cs b/src/EPPlus.Fonts.OpenType/Integration/OpenTypeFontTextMeasurer.cs index 5746e7c23..1dfa66ebc 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/OpenTypeFontTextMeasurer.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/OpenTypeFontTextMeasurer.cs @@ -33,14 +33,6 @@ public OpenTypeFontTextMeasurer(ITextShaper shaper, ShapingOptions options = nul MeasureWrappedTextCells = true; } - /// - /// Convenience constructor that creates a TextShaper internally. - /// - public OpenTypeFontTextMeasurer(OpenTypeFont font, ShapingOptions options = null) - : this(new TextShaper(font), options) - { - } - /// /// Always valid - pure .NET implementation with no external dependencies. /// diff --git a/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs b/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs index 33795f766..292b96d86 100644 --- a/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs +++ b/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs @@ -128,7 +128,7 @@ public TextShaper GetTextShaper(string fontName, FontSubFamily subFamily = FontS if (font == null) return null; - shaper = new TextShaper(font); + shaper = new TextShaper(this, font); perEngineMap[key] = shaper; } @@ -348,6 +348,16 @@ public FontAvailability GetFontAvailability( // Internal helpers // ----------------------------------------------------------------------------------------- + /// + /// Returns the configured fallback chain for the given Unicode script, or null if + /// none is configured. An empty array means fallback is explicitly disabled for the + /// script. Used by DefaultFontProvider to look up script-level glyph fallbacks. + /// + internal string[] GetScriptFallback(UnicodeScript script) + { + return _configuration.GetScriptFallback(script); + } + internal static string BuildCacheKey(string fontName, FontSubFamily subFamily) { return string.Format("{0}_{1}", fontName, subFamily); diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs index 20ce30010..d135aa833 100644 --- a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs +++ b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs @@ -59,10 +59,8 @@ public ushort UnitsPerEm /// instances directly. It provides a thread-local cached instance and avoids /// duplicate caches across the codebase. /// - public TextShaper(OpenTypeFont font) - : this(new DefaultFontProvider(font)) - { - } + public TextShaper(OpenTypeFontEngine engine, OpenTypeFont font) + : this(new DefaultFontProvider(engine, font)) { } /// /// Creates a TextShaper with custom font provider. @@ -105,11 +103,22 @@ public IEnumerable GetUsedFonts() /// /// Resets font tracking state. Called automatically at the start of each /// shaping operation β€” Shape(), ExtractCharWidths(), ShapeLight(). + /// + /// The primary font is registered immediately with FontId 0, regardless of + /// whether it ends up contributing any glyphs. This preserves the invariant + /// that FontId 0 always refers to the primary font β€” without it, a string that + /// uses only fallback glyphs (e.g. pure CJK text against a Latin primary) would + /// leave the primary out of _usedFonts entirely, and the first fallback would + /// end up with FontId 0. /// private void ResetFontTracking() { _usedFonts.Clear(); _fontToIdMap.Clear(); + + // Reserve FontId 0 for the primary font, even before any glyphs are mapped. + _usedFonts.Add(_primaryFont); + _fontToIdMap[_primaryFont] = 0; } /// diff --git a/src/EPPlus.Fonts.OpenType/UnicodeScriptClassifier.cs b/src/EPPlus.Fonts.OpenType/UnicodeScriptClassifier.cs new file mode 100644 index 000000000..c20aa5a18 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/UnicodeScriptClassifier.cs @@ -0,0 +1,180 @@ +ο»Ώ/************************************************************************************************* + 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 + ************************************************************************************************* + 05/20/2026 EPPlus Software AB Initial implementation + 05/20/2026 EPPlus Software AB Auto-sort ranges so source can stay grouped by script + *************************************************************************************************/ +using OfficeOpenXml.Interfaces.Fonts; +using System; + +namespace EPPlus.Fonts.OpenType +{ + /// + /// Classifies Unicode code points into values. + /// Used during text shaping to route glyphs missing from the primary font to the + /// appropriate per-script fallback chain. + /// + /// Lookup is performed by binary search over a sorted range table, giving O(log n) + /// classification per code point. The table covers all supported scripts; code points + /// outside any range return . + /// + /// The source-code form of the table is grouped by script for readability. The actual + /// search table is built once at type initialization by sorting a copy by Start. + /// + internal static class UnicodeScriptClassifier + { + /// + /// Returns the Unicode script that contains the given code point, or + /// if the code point is not in any supported range. + /// + public static UnicodeScript OfCodePoint(uint codePoint) + { + // Binary search for the range containing codePoint. Ranges are sorted by Start + // and are non-overlapping. + int lo = 0; + int hi = _sortedRanges.Length - 1; + + while (lo <= hi) + { + int mid = lo + ((hi - lo) >> 1); + var range = _sortedRanges[mid]; + + if (codePoint < range.Start) + { + hi = mid - 1; + } + else if (codePoint > range.End) + { + lo = mid + 1; + } + else + { + return range.Script; + } + } + + return UnicodeScript.Unknown; + } + + private readonly struct Range + { + public readonly uint Start; + public readonly uint End; + public readonly UnicodeScript Script; + + public Range(uint start, uint end, UnicodeScript script) + { + Start = start; + End = end; + Script = script; + } + } + + // Source-form table β€” grouped by script for readability. Order does NOT matter here; + // the actual search table (_sortedRanges) is built by sorting this by Start. Ranges + // must still be non-overlapping; that invariant is not enforced. + // Coverage focuses on what realistically appears in Office documents; obscure scripts + // and historic blocks are omitted. + private static readonly Range[] _sourceRanges = new Range[] + { + // Latin + new Range(0x0020, 0x007F, UnicodeScript.Latin), // Basic Latin (ASCII) + new Range(0x00A0, 0x024F, UnicodeScript.Latin), // Latin-1 Supplement + Latin Extended-A/B + new Range(0x1E00, 0x1EFF, UnicodeScript.Latin), // Latin Extended Additional + new Range(0x2C60, 0x2C7F, UnicodeScript.Latin), // Latin Extended-C + + // Greek + new Range(0x0370, 0x03FF, UnicodeScript.Greek), // Greek and Coptic + new Range(0x1F00, 0x1FFF, UnicodeScript.Greek), // Greek Extended + + // Cyrillic + new Range(0x0400, 0x04FF, UnicodeScript.Cyrillic), // Cyrillic + new Range(0x0500, 0x052F, UnicodeScript.Cyrillic), // Cyrillic Supplement + new Range(0x2DE0, 0x2DFF, UnicodeScript.Cyrillic), // Cyrillic Extended-A + new Range(0xA640, 0xA69F, UnicodeScript.Cyrillic), // Cyrillic Extended-B + + // Hebrew + new Range(0x0590, 0x05FF, UnicodeScript.Hebrew), // Hebrew + + // Arabic + new Range(0x0600, 0x06FF, UnicodeScript.Arabic), // Arabic + new Range(0x0750, 0x077F, UnicodeScript.Arabic), // Arabic Supplement + new Range(0x08A0, 0x08FF, UnicodeScript.Arabic), // Arabic Extended-A + new Range(0xFB50, 0xFDFF, UnicodeScript.Arabic), // Arabic Presentation Forms-A + new Range(0xFE70, 0xFEFF, UnicodeScript.Arabic), // Arabic Presentation Forms-B + + // Devanagari + new Range(0x0900, 0x097F, UnicodeScript.Devanagari), // Devanagari + + // Thai + new Range(0x0E00, 0x0E7F, UnicodeScript.Thai), // Thai + + // Currency + new Range(0x20A0, 0x20CF, UnicodeScript.Currency), // Currency Symbols + + // Math + new Range(0x2200, 0x22FF, UnicodeScript.Math), // Mathematical Operators + new Range(0x27C0, 0x27EF, UnicodeScript.Math), // Miscellaneous Mathematical Symbols-A + new Range(0x2980, 0x29FF, UnicodeScript.Math), // Miscellaneous Mathematical Symbols-B + new Range(0x2A00, 0x2AFF, UnicodeScript.Math), // Supplemental Mathematical Operators + new Range(0x1D400, 0x1D7FF, UnicodeScript.Math), // Mathematical Alphanumeric Symbols + + // Symbol (box drawing, geometric shapes, dingbats, misc symbols) + new Range(0x2500, 0x257F, UnicodeScript.Symbol), // Box Drawing + new Range(0x2580, 0x259F, UnicodeScript.Symbol), // Block Elements + new Range(0x25A0, 0x25FF, UnicodeScript.Symbol), // Geometric Shapes + new Range(0x2700, 0x27BF, UnicodeScript.Symbol), // Dingbats + + // CJK Han (Unified Ideographs) + new Range(0x3400, 0x4DBF, UnicodeScript.Han), // CJK Unified Ideographs Extension A + new Range(0x4E00, 0x9FFF, UnicodeScript.Han), // CJK Unified Ideographs + new Range(0xF900, 0xFAFF, UnicodeScript.Han), // CJK Compatibility Ideographs + new Range(0x20000, 0x2A6DF, UnicodeScript.Han), // CJK Unified Ideographs Extension B + new Range(0x2A700, 0x2B73F, UnicodeScript.Han), // CJK Unified Ideographs Extension C + new Range(0x2B740, 0x2B81F, UnicodeScript.Han), // CJK Unified Ideographs Extension D + new Range(0x2B820, 0x2CEAF, UnicodeScript.Han), // CJK Unified Ideographs Extension E + + // Japanese Hiragana + new Range(0x3040, 0x309F, UnicodeScript.Hiragana), // Hiragana + + // Japanese Katakana + new Range(0x30A0, 0x30FF, UnicodeScript.Katakana), // Katakana + new Range(0x31F0, 0x31FF, UnicodeScript.Katakana), // Katakana Phonetic Extensions + + // Korean Hangul + new Range(0x1100, 0x11FF, UnicodeScript.Hangul), // Hangul Jamo + new Range(0x3130, 0x318F, UnicodeScript.Hangul), // Hangul Compatibility Jamo + new Range(0xAC00, 0xD7AF, UnicodeScript.Hangul), // Hangul Syllables + new Range(0xD7B0, 0xD7FF, UnicodeScript.Hangul), // Hangul Jamo Extended-B + + // Emoji + new Range(0x1F300, 0x1F5FF, UnicodeScript.Emoji), // Miscellaneous Symbols and Pictographs + new Range(0x1F600, 0x1F64F, UnicodeScript.Emoji), // Emoticons + new Range(0x1F680, 0x1F6FF, UnicodeScript.Emoji), // Transport and Map Symbols + new Range(0x1F700, 0x1F77F, UnicodeScript.Emoji), // Alchemical Symbols + new Range(0x1F780, 0x1F7FF, UnicodeScript.Emoji), // Geometric Shapes Extended + new Range(0x1F800, 0x1F8FF, UnicodeScript.Emoji), // Supplemental Arrows-C + new Range(0x1F900, 0x1F9FF, UnicodeScript.Emoji), // Supplemental Symbols and Pictographs + new Range(0x1FA00, 0x1FA6F, UnicodeScript.Emoji), // Chess Symbols + new Range(0x1FA70, 0x1FAFF, UnicodeScript.Emoji) // Symbols and Pictographs Extended-A + }; + + // Search table: copy of _sourceRanges sorted by Start. Built once at type init. + private static readonly Range[] _sortedRanges = BuildSortedRanges(); + + private static Range[] BuildSortedRanges() + { + var copy = new Range[_sourceRanges.Length]; + Array.Copy(_sourceRanges, copy, _sourceRanges.Length); + Array.Sort(copy, (a, b) => a.Start.CompareTo(b.Start)); + return copy; + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs b/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs index 2768dd391..b17f1550c 100644 --- a/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs +++ b/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 02/27/2026 EPPlus Software AB Initial implementation 05/06/2026 EPPlus Software AB Property-based transactional configuration + 05/20/2026 EPPlus Software AB Added per-script glyph fallback configuration *************************************************************************************************/ using System.Collections.Generic; @@ -52,6 +53,23 @@ public interface IEpplusFontConfiguration /// IDictionary FontFallbacks { get; } + /// + /// Replaces the per-script glyph fallback chain for the given Unicode script. + /// When the primary font (and any font-level fallbacks) lack a glyph for a character + /// that belongs to , EPPlus walks this chain in order and + /// uses the first font that contains the glyph. + /// + /// Setting a chain via this method fully replaces the built-in default for that script. + /// The caller becomes responsible for providing a complete chain β€” there is no merge + /// with the default. Pass an empty array to disable fallback for the script entirely. + /// + /// Note: Emoji and Math glyphs are always served by EPPlus's bundled Noto Emoji and + /// Noto Math fonts respectively, regardless of this configuration. + /// + /// The Unicode script to configure. + /// Ordered list of font names to try. + void SetScriptFallback(UnicodeScript script, params string[] fallbackFontNames); + /// /// Restores all settings to factory defaults: /// @@ -59,6 +77,7 @@ public interface IEpplusFontConfiguration /// Sets to true. /// Restores the default (with Archivo Narrow built-in fallback). /// Clears . + /// Restores the default per-script glyph fallback chains. /// /// void Reset(); diff --git a/src/EPPlus.Interfaces/Fonts/UnicodeScript.cs b/src/EPPlus.Interfaces/Fonts/UnicodeScript.cs new file mode 100644 index 000000000..8da5e3c22 --- /dev/null +++ b/src/EPPlus.Interfaces/Fonts/UnicodeScript.cs @@ -0,0 +1,74 @@ +ο»Ώ/************************************************************************************************* + 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 + ************************************************************************************************* + 05/20/2026 EPPlus Software AB Initial implementation + *************************************************************************************************/ +namespace OfficeOpenXml.Interfaces.Fonts +{ + /// + /// Unicode scripts used for per-script glyph-level fallback. Each value corresponds + /// to a Unicode block or group of blocks that share a writing system, and is the key + /// for configuring fallback fonts via + /// EpplusFontConfiguration.SetScriptFallback(UnicodeScript, params string[]). + /// + /// Only scripts that are realistic in Office documents are included. Adding more is + /// straightforward but requires updating UnicodeScriptClassifier as well. + /// + public enum UnicodeScript + { + /// The character does not belong to any of the supported scripts. + Unknown = 0, + + /// Latin script (ASCII, Latin-1, Latin Extended). Covers English, most European languages. + Latin, + + /// Cyrillic script. Covers Russian, Ukrainian, Bulgarian, Serbian, and other Slavic languages. + Cyrillic, + + /// Greek script. Covers Modern and Polytonic Greek. + Greek, + + /// Arabic script. Covers Arabic, Persian, Urdu, and related languages. + Arabic, + + /// Hebrew script. + Hebrew, + + /// Thai script. + Thai, + + /// Devanagari script. Covers Hindi, Marathi, Nepali, and many other Indian languages. + Devanagari, + + /// CJK Unified Ideographs (Han characters). Used in Chinese, Japanese, Korean, Vietnamese. + Han, + + /// Japanese Hiragana syllabary. + Hiragana, + + /// Japanese Katakana syllabary. + Katakana, + + /// Korean Hangul script (syllables and Jamo). + Hangul, + + /// Emoji and pictographic symbols (U+1F300-U+1FAFF and related blocks). + Emoji, + + /// Mathematical symbols and operators (U+2200-U+22FF, U+27C0-U+27EF, U+2A00-U+2AFF). + Math, + + /// Currency symbols (U+20A0-U+20CF). + Currency, + + /// Box drawing, block elements, and miscellaneous symbols used in tables and diagrams. + Symbol + } +} \ No newline at end of file