diff --git a/src/EPPlus.Compression/AssemblyInfo.cs b/src/EPPlus.Compression/AssemblyInfo.cs index cbb79fc75f..41efa7646a 100644 --- a/src/EPPlus.Compression/AssemblyInfo.cs +++ b/src/EPPlus.Compression/AssemblyInfo.cs @@ -2,4 +2,5 @@ using System.Security; [assembly: InternalsVisibleTo("EPPlus, PublicKey=00240000048000009400000006020000002400005253413100040000010001002981343969ed86fe604c56a84c61e33109424ef07bb458ff12e9533c11ea23ac8ef7e014b2a2de4ceb5f7528f963c755fe9b32f09cc35d21de94319d2a952a6e663cd46d6d98465998c77b52093d4f17cdc20ec054751244696f08afa6f4417d85267b147b73b6a3f5e9015b9dfd3dcc3328ce63df53a7c08a5544c1526ea5a5")] +[assembly: InternalsVisibleTo("EPPlus.Fonts.OpenType, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f506ded61f2e8f2c2a377e72c15f7de76a83bd0e1dc4425e95db0e9fd6cb4703d2ab8d9e666fe1f7738fca0b5695025d7de7292c0cc0b7ea3a5948f42681521523b7e94bb6827dfc7b102dd9b8b60216c86b1b462a37f155d14ab8a97987652e4691ceb1f2455df986c0bdf73dd3ce0de3327a057fbf6560db070c1444f313be")] [assembly: AllowPartiallyTrustedCallers] \ No newline at end of file diff --git a/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs b/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs index a92705daff..82e84d8d5b 100644 --- a/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs +++ b/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs @@ -160,7 +160,11 @@ public void SpaceCase() var layout = SystemFolderEngine.GetTextLayoutEngineForFont(mf); var output = layout.WrapText(text, 11f, 39.4); - var shaper = OpenTypeFonts.GetShaperForFont(mf); + var engine = new OpenTypeFontEngine(cfg => + { + cfg.SearchSystemDirectories = true; + }); + var shaper = engine.GetShaperForFont(mf); var shapes2 = shaper.ShapeLight("nec rhoncus"); var width= shapes2.GetWidthInPoints(11f); //var shaper = OpenTypeFonts.GetShaperForFont(mf); @@ -198,10 +202,7 @@ public void LoremIpsumTesting() var engine = new OpenTypeFontEngine(x => x.SearchSystemDirectories = true); var layout = engine.GetTextLayoutEngineForFont(mf); - - //var wrappedStrings = layout.WrapRichText(new List() { text }, new List() { mf }, 39.4f); - - var shaper = OpenTypeFonts.GetShaperForFont(mf); + var shaper = engine.GetShaperForFont(mf); var wrappedStrings = layout.WrapText(text, 11f, 39.4); diff --git a/src/EPPlus.Export.Pdf.Tests/FontTests.cs b/src/EPPlus.Export.Pdf.Tests/FontTests.cs index c30f7b9b79..6f93e8d5f3 100644 --- a/src/EPPlus.Export.Pdf.Tests/FontTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/FontTests.cs @@ -101,11 +101,13 @@ public void AddFontData_Embedded_FontResourcePointsAtType0Dict() { var settings = CreateSettings(engine, true); var dictionaries = CreateDictionariesWithSingleFont(settings, engine); + var docSettings = PdfDocumentSettings.From(settings); var excelPdf = new ExcelPdf(); excelPdf.SetPageSettingsForTest(settings); excelPdf.SetDocumentSettingsForTest(PdfDocumentSettings.From(settings)); excelPdf.SetDictionariesForTest(dictionaries); + excelPdf.SetDocumentSettingsForTest(docSettings); excelPdf.AddFontData(); var fontResource = dictionaries.GetFont(settings, TestFontName, FontSubFamily.Regular); @@ -150,11 +152,13 @@ public void AddFontData_Embedded_DoesNotEmitSimpleFontObject() { var settings = CreateSettings(engine, true); var dictionaries = CreateDictionariesWithSingleFont(settings, engine); + var docSettings = PdfDocumentSettings.From(settings); var excelPdf = new ExcelPdf(); excelPdf.SetPageSettingsForTest(settings); excelPdf.SetDocumentSettingsForTest(PdfDocumentSettings.From(settings)); excelPdf.SetDictionariesForTest(dictionaries); + excelPdf.SetDocumentSettingsForTest(docSettings); excelPdf.AddFontData(); foreach (var obj in excelPdf._document) diff --git a/src/EPPlus.Export.Pdf.Tests/Helpers/FontDirectoriesTestHelper.cs b/src/EPPlus.Export.Pdf.Tests/Helpers/FontDirectoriesTestHelper.cs index 8422410727..c3ce396604 100644 --- a/src/EPPlus.Export.Pdf.Tests/Helpers/FontDirectoriesTestHelper.cs +++ b/src/EPPlus.Export.Pdf.Tests/Helpers/FontDirectoriesTestHelper.cs @@ -77,7 +77,6 @@ public static void ClassInitialize(TestContext testContext) _testOutputAvailable = false; } - OpenTypeFonts.ClearFontCache(); _initialized = true; } } diff --git a/src/EPPlus.Export.Pdf.Tests/Helpers/FontTestHelper.cs b/src/EPPlus.Export.Pdf.Tests/Helpers/FontTestHelper.cs index 01f117c973..4668da7234 100644 --- a/src/EPPlus.Export.Pdf.Tests/Helpers/FontTestHelper.cs +++ b/src/EPPlus.Export.Pdf.Tests/Helpers/FontTestHelper.cs @@ -83,11 +83,11 @@ public static void AssertFontValid( /// Folders to search for fonts /// Serialized subset bytes public static byte[] SubsetAndSerialize( + OpenTypeFontEngine engine, string fontName, - string text, - List fontFolders) + string text) { - var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); + var font = engine.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(text); return subset.Serialize(); } @@ -96,11 +96,11 @@ public static byte[] SubsetAndSerialize( /// Creates a subset and serializes it to bytes (char array overload) /// public static byte[] SubsetAndSerialize( + OpenTypeFontEngine engine, string fontName, - char[] chars, - List fontFolders) + char[] chars) { - var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); + var font = engine.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(chars); return subset.Serialize(); } @@ -113,11 +113,11 @@ public static byte[] SubsetAndSerialize( /// Folders to search for fonts /// Parsed subset font (validated) public static OpenTypeFont RoundtripSubset( + OpenTypeFontEngine engine, string fontName, - string text, - List fontFolders) + string text) { - var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); + var font = engine.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(text); var bytes = subset.Serialize(); @@ -132,11 +132,11 @@ public static OpenTypeFont RoundtripSubset( /// Performs a full roundtrip with char array /// public static OpenTypeFont RoundtripSubset( + OpenTypeFontEngine engine, string fontName, - char[] chars, - List fontFolders) + char[] chars) { - var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); + var font = engine.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(chars); var bytes = subset.Serialize(); diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheBenchmarks.cs deleted file mode 100644 index b51dbe68ac..0000000000 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheBenchmarks.cs +++ /dev/null @@ -1,57 +0,0 @@ -using BenchmarkDotNet.Attributes; -using OfficeOpenXml.Interfaces.Fonts; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EPPlus.Fonts.OpenType.Benchmarks -{ - /// - /// Separate benchmark class to measure cache performance without ClearCache in IterationSetup - /// - [MemoryDiagnoser] - [SimpleJob(warmupCount: 3, iterationCount: 5)] - public class FontCacheBenchmarks - { - private List _fontFolders; - - [GlobalSetup] - public void Setup() - { - var fontsPath = Path.Combine(System.AppContext.BaseDirectory, "Fonts"); - - if (!Directory.Exists(fontsPath)) - { - throw new DirectoryNotFoundException($"Fonts directory not found: {fontsPath}"); - } - - _fontFolders = new List { fontsPath }; - - // Pre-load font into cache - OpenTypeFonts.ClearFontCache(); - OpenTypeFonts.LoadFont("Roboto"); - } - - [Benchmark] - public OpenTypeFont Load_FromCache_SingleThread() - { - // This should be extremely fast - just cache lookup - return OpenTypeFonts.LoadFont("Roboto"); - } - - [Benchmark] - public OpenTypeFont[] Load_FromCache_MultipleFonts() - { - // Simulates loading multiple font styles (like for a document) - return new[] - { - OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular), - OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Bold), - OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Italic), - OpenTypeFonts.LoadFont("Roboto", FontSubFamily.BoldItalic) - }; - } - } -} diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheClearingBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheClearingBenchmarks.cs deleted file mode 100644 index 29622a3ca5..0000000000 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/FontCacheClearingBenchmarks.cs +++ /dev/null @@ -1,49 +0,0 @@ -using BenchmarkDotNet.Attributes; -using EPPlus.Fonts.OpenType; -using OfficeOpenXml.Interfaces.Fonts; - -/// -/// Benchmarks for repeated cache clearing scenarios -/// -[MemoryDiagnoser] -[SimpleJob(warmupCount: 3, iterationCount: 5)] -public class FontCacheClearingBenchmarks -{ - private List _fontFolders; - - [GlobalSetup] - public void Setup() - { - var fontsPath = Path.Combine(System.AppContext.BaseDirectory, "Fonts"); - - if (!Directory.Exists(fontsPath)) - { - throw new DirectoryNotFoundException($"Fonts directory not found: {fontsPath}"); - } - - _fontFolders = new List { fontsPath }; - } - - [Benchmark] - public OpenTypeFont Load_Clear_Load_Pattern() - { - // Simulates pattern where cache is cleared between operations - OpenTypeFonts.ClearFontCache(); - var font1 = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); - - OpenTypeFonts.ClearFontCache(); - var font2 = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); - - return font2; - } - - [Benchmark] - public OpenTypeFont Load_Reuse_Pattern() - { - // Simulates pattern where cache is NOT cleared (optimal) - var font1 = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); - var font2 = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); - - return font2; - } -} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/FontLoadingBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/FontLoadingBenchmarks.cs index 703d995c9f..37bee9b8c9 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/FontLoadingBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/FontLoadingBenchmarks.cs @@ -1,16 +1,4 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 01/20/2025 EPPlus Software AB Initial implementation - *************************************************************************************************/ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using EPPlus.Fonts.OpenType; using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; @@ -24,6 +12,12 @@ public class FontLoadingBenchmarks { private List _fontFolders; + // Kept across iterations: its font cache is warm after the first load. + private OpenTypeFontEngine _warmEngine; + + // Rebuilt before every cold iteration by IterationSetup. + private OpenTypeFontEngine _coldEngine; + [GlobalSetup] public void Setup() { @@ -35,41 +29,68 @@ public void Setup() } _fontFolders = new List { fontsPath }; + + _warmEngine = CreateEngine(); + _warmEngine.LoadFont("Roboto", FontSubFamily.Regular); } - [Benchmark] - public OpenTypeFont Load_Roboto_Regular_ColdCache() + [GlobalCleanup] + public void Cleanup() { - OpenTypeFonts.ClearFontCache(); // Clear INNE i benchmark - return OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); + _warmEngine?.Dispose(); + _coldEngine?.Dispose(); } - [Benchmark] - public OpenTypeFont Load_Roboto_Regular_WarmCache() + private OpenTypeFontEngine CreateEngine() { - // Load UTAN att cleara - använder cache från GlobalSetup eller warmup - return OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); + return new OpenTypeFontEngine(cfg => + { + foreach (var folder in _fontFolders) + { + cfg.FontDirectories.Add(folder); + } + // The benchmark measures loading the test fonts, not whatever happens to be + // installed on the machine running it. + cfg.SearchSystemDirectories = false; + }); + } + + [IterationSetup(Target = nameof(Load_ColdCache))] + public void ColdSetup() + { + _coldEngine?.Dispose(); + _coldEngine = CreateEngine(); } [Benchmark] - public OpenTypeFont Load_Roboto_Bold_ColdCache() + [Arguments(FontSubFamily.Regular)] + [Arguments(FontSubFamily.Bold)] + [Arguments(FontSubFamily.Italic)] + [Arguments(FontSubFamily.BoldItalic)] + public OpenTypeFont Load_ColdCache(FontSubFamily subFamily) { - OpenTypeFonts.ClearFontCache(); - return OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Bold); + return _coldEngine.LoadFont("Roboto", subFamily); } [Benchmark] - public OpenTypeFont Load_Roboto_Italic_ColdCache() + public OpenTypeFont Load_Roboto_Regular_WarmCache() { - OpenTypeFonts.ClearFontCache(); - return OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Italic); + return _warmEngine.LoadFont("Roboto", FontSubFamily.Regular); } [Benchmark] - public OpenTypeFont Load_Roboto_BoldItalic_ColdCache() + public OpenTypeFont[] Load_FromCache_AllSubFamilies() { - OpenTypeFonts.ClearFontCache(); - return OpenTypeFonts.LoadFont("Roboto", FontSubFamily.BoldItalic); + // Four distinct cache keys in sequence — the pattern a document with all four + // styles produces. Unlike the single-font warm benchmark, this measures four + // separate lookups rather than one repeated. + return new[] + { + _warmEngine.LoadFont("Roboto", FontSubFamily.Regular), + _warmEngine.LoadFont("Roboto", FontSubFamily.Bold), + _warmEngine.LoadFont("Roboto", FontSubFamily.Italic), + _warmEngine.LoadFont("Roboto", FontSubFamily.BoldItalic) + }; } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs index f1181423e7..7eadc55bc3 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs @@ -71,7 +71,7 @@ public void Setup() } Console.WriteLine("\nLoading Roboto Regular..."); - var font = OpenTypeFonts.LoadFont(FontFamily, FontSubFamily.Regular); + var font = fontEngine.LoadFont(FontFamily, FontSubFamily.Regular); Console.WriteLine(string.Format("Loaded: {0} {1} ({2} glyphs)", font.FullName, font.SubFamily, font.GlyfTable.Glyphs.Count)); diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/SubsettingBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/SubsettingBenchmarks.cs index 985233598d..ae9bc613ee 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/SubsettingBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/SubsettingBenchmarks.cs @@ -36,7 +36,12 @@ public void Setup() } _fontFolders = new List { fontsPath }; - _roboto = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); + var fontEngine = new OpenTypeFontEngine(cfg => + { + cfg.FontDirectories.Add(fontsPath); + cfg.SearchSystemDirectories = false; + }); + _roboto = fontEngine.LoadFont("Roboto", FontSubFamily.Regular); } [Benchmark] diff --git a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs index eb9bda6daa..32f5551e6e 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs @@ -21,8 +21,11 @@ public void TestLineFragmentAbstraction() var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); var fragments = GetTextFragments(); - - var layout = OpenTypeFonts.GetTextLayoutEngineForFont(fragments[0].Font); + var engine = new OpenTypeFontEngine(cfg => + { + cfg.SearchSystemDirectories = true; + }); + var layout = engine.GetTextLayoutEngineForFont(fragments[0].Font); var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); var wrappedCollection = layout.WrapRichTextLineCollection(fragments, maxSizePoints); @@ -37,8 +40,11 @@ public void TestLineFragmentSeeWhatLinesUseWhatRichText() var fragments = GetTextFragments(); fragments[4].RichTextOptions.FontColor = Color.DarkRed; - - var layout = OpenTypeFonts.GetTextLayoutEngineForFont(fragments[0].Font); + var engine = new OpenTypeFontEngine(cfg => + { + cfg.SearchSystemDirectories = true; + }); + var layout = engine.GetTextLayoutEngineForFont(fragments[0].Font); var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); var wrappedCollection = layout.WrapRichTextLineCollection(fragments, maxSizePoints); diff --git a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs index b713ae797c..5d937b3bfd 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs @@ -203,7 +203,7 @@ public void DiagnoseScriptFallback() }); // Diagnostik 1: vad sätter engine för Han efter konstruktion? - var chain = engine.GetScriptFallback(UnicodeScript.Han); + var chain = engine.FontStore.GetScriptFallback(UnicodeScript.Han); System.Console.WriteLine($"[DIAG] Han chain: [{string.Join(", ", chain ?? new string[0])}]"); // Diagnostik 2: shape och se vad providern returnerar diff --git a/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontDirectoriesTestHelper.cs b/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontDirectoriesTestHelper.cs index ea662b6419..5d89cf8f21 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontDirectoriesTestHelper.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontDirectoriesTestHelper.cs @@ -76,7 +76,6 @@ public static void ClassInitialize(TestContext testContext) _testOutputAvailable = false; } - OpenTypeFonts.ClearFontCache(); _initialized = true; } } diff --git a/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontTestHelper.cs b/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontTestHelper.cs index 650b81516f..2346762a69 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontTestHelper.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Helpers/FontTestHelper.cs @@ -82,11 +82,11 @@ public static void AssertFontValid( /// Folders to search for fonts /// Serialized subset bytes public static byte[] SubsetAndSerialize( + OpenTypeFontEngine engine, string fontName, - string text, - List fontFolders) + string text) { - var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); + var font = engine.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(text); return subset.Serialize(); } @@ -95,11 +95,11 @@ public static byte[] SubsetAndSerialize( /// Creates a subset and serializes it to bytes (char array overload) /// public static byte[] SubsetAndSerialize( + OpenTypeFontEngine engine, string fontName, - char[] chars, - List fontFolders) + char[] chars) { - var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); + var font = engine.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(chars); return subset.Serialize(); } @@ -112,11 +112,11 @@ public static byte[] SubsetAndSerialize( /// Folders to search for fonts /// Parsed subset font (validated) public static OpenTypeFont RoundtripSubset( + OpenTypeFontEngine engine, string fontName, - string text, - List fontFolders) + string text) { - var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); + var font = engine.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(text); var bytes = subset.Serialize(); @@ -131,11 +131,11 @@ public static OpenTypeFont RoundtripSubset( /// Performs a full roundtrip with char array /// public static OpenTypeFont RoundtripSubset( + OpenTypeFontEngine engine, string fontName, - char[] chars, - List fontFolders) + char[] chars) { - var font = OpenTypeFonts.LoadFont(fontName, FontSubFamily.Regular); + var font = engine.LoadFont(fontName, FontSubFamily.Regular); var subset = font.CreateSubset(chars); var bytes = subset.Serialize(); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs index cfc24d12d4..c8a5bddecb 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs @@ -51,8 +51,11 @@ public void TestParagraphs() Assert.AreEqual(lstOfRichText[0], styleRuns[0]); Assert.AreEqual(lstOfRichText[1], styleRuns[1]); - - var layout = OpenTypeFonts.GetTextLayoutEngineForFont(font); + var engine = new OpenTypeFontEngine(cfg => + { + cfg.SearchSystemDirectories = true; + }); + var layout = engine.GetTextLayoutEngineForFont(font); var wrappedLines = layout.WrapRichTextLines(fragments, 225d); var wrappedLinesPara = paragraph.Wrap(225d); @@ -300,6 +303,53 @@ public void EnsureWrappingRichTextAndGettingLineSpacing() Assert.AreEqual(rtLst[2], (IRichTextFormatEssential)lines[2].LineFragments[0].OriginalTextFragment.RichTextOptions); } + + [TestMethod] + public void EnsureWrappingRichTextAndGettingLineSpacing_OfficeFonts() + { + List txtLst = new List() { "Hi ", "I am rich ", "But I am Even Richer " }; + var rt = new RichTextFormatBase(txtLst[0], "Aptos Narrow", 12f); + var rtSecond = new RichTextFormatBase(txtLst[1], "Times New Roman", 11f); + var rtThird = new RichTextFormatBase(txtLst[2], "Arial", 18f); + + rtThird.Italic = true; + rtThird.Bold = true; + + List rtLst = new List() { rt, rtSecond, rtThird }; + + var fontEngine = new OpenTypeFontEngine(cfg => + { + cfg.SearchSystemDirectories = true; + cfg.MetricsFallback = MetricsFallbackMode.WhenFontMissing; + }); + + var paragraph = new LayoutSystem(fontEngine, rtLst); + + var lines = paragraph.Wrap(92.976377953d); + + //Assert correct wrapping + Assert.AreEqual("Hi I am rich But", lines[0].Text); + Assert.AreEqual("I am Even", lines[1].Text); + Assert.AreEqual("Richer", lines[2].Text); + + //Assert line segments correct count + Assert.AreEqual(3, lines[0].LineFragments.Count); + Assert.AreEqual(3, lines[0].InternalLineFragments.Count); + Assert.AreEqual(1, lines[1].LineFragments.Count); + Assert.AreEqual(1, lines[1].InternalLineFragments.Count); + Assert.AreEqual(1, lines[2].LineFragments.Count); + Assert.AreEqual(1, lines[2].InternalLineFragments.Count); + + //Assert correct fragment in correct spot + Assert.AreEqual(rtLst[0], (IRichTextFormatEssential)lines[0].LineFragments[0].OriginalTextFragment.RichTextOptions); + Assert.AreEqual(rtLst[1], (IRichTextFormatEssential)lines[0].LineFragments[1].OriginalTextFragment.RichTextOptions); + Assert.AreEqual(rtLst[2], (IRichTextFormatEssential)lines[0].LineFragments[2].OriginalTextFragment.RichTextOptions); + + Assert.AreEqual(rtLst[2], (IRichTextFormatEssential)lines[1].LineFragments[0].OriginalTextFragment.RichTextOptions); + Assert.AreEqual(rtLst[2], (IRichTextFormatEssential)lines[2].LineFragments[0].OriginalTextFragment.RichTextOptions); + } + + [TestMethod] public void TestGetSection() { diff --git a/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs index 22e0d83e2e..81f1725eaf 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs @@ -144,7 +144,7 @@ public void ReadSixFonts() [TestMethod] public void ReadAllOTFFonts() { - List allFontsList = OpenTypeFonts.GetAllBaseFontData(FontFolders, true, Scanner.FontFormat.Otf); + var allFontsList = FontDiscovery.GetAllBaseFontData(FontFolders, true, Scanner.FontFormat.Otf); List dataHolder = new List(); @@ -170,7 +170,7 @@ public void ReadAllOTFFonts() [TestMethod] public void ReadAllTestTTFFontsAndVerifyUseIsOkay() { - List allFontsList = OpenTypeFonts.GetAllBaseFontData(FontFolders, false, Scanner.FontFormat.Ttf); + var allFontsList = FontDiscovery.GetAllBaseFontData(FontFolders, false, Scanner.FontFormat.Ttf); List dataHolder = new List(); @@ -196,7 +196,7 @@ public void ReadAllTestTTFFontsAndVerifyUseIsOkay() [TestMethod, Ignore("This test takes a long time and should not run in quick regression tests")] public void ReadAllTTFFonts() { - List allFontsList = OpenTypeFonts.GetAllBaseFontData(FontFolders, true, Scanner.FontFormat.Ttf); + var allFontsList = FontDiscovery.GetAllBaseFontData(FontFolders, true, Scanner.FontFormat.Otf); List dataHolder = new List(); @@ -224,7 +224,7 @@ public void ReadAllFonts() { var sw = new Stopwatch(); sw.Start(); - List allFontsList = OpenTypeFonts.GetAllBaseFontData(FontFolders, true); + var allFontsList = FontDiscovery.GetAllBaseFontData(FontFolders, true, Scanner.FontFormat.Otf); sw.Stop(); Trace.WriteLine(sw.ElapsedMilliseconds); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Serialization/GPosSerializationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Serialization/GPosSerializationTests.cs index 8633c1ad54..2966165736 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Serialization/GPosSerializationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Serialization/GPosSerializationTests.cs @@ -82,7 +82,7 @@ private class SinglePosValue [TestMethod] public void Diagnose_SerializedFontOffsets() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); Debug.WriteLine("=== ORIGINAL TABLE RECORDS ==="); foreach (var kvp in font.TableRecords) @@ -129,7 +129,7 @@ public void Diagnose_SerializedFontOffsets() [TestMethod] public void SerializeGpos_StructurePreserved() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var originalGpos = font.GposTable; var serializer = new OpenTypeFontSerializer(font); @@ -167,7 +167,7 @@ public void SerializeGpos_StructurePreserved() [TestMethod] public void SerializeGpos_FeatureTagsPreserved() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var originalTags = new List(); foreach (var feature in font.GposTable.FeatureList.FeatureRecords) @@ -196,7 +196,7 @@ public void SerializeGpos_FeatureTagsPreserved() [TestMethod] public void SerializeGpos_ScriptTagsPreserved() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var originalTags = new List(); foreach (var script in font.GposTable.ScriptList.ScriptRecords) @@ -229,7 +229,7 @@ public void SerializeGpos_ScriptTagsPreserved() [TestMethod] public void SerializeGpos_PairPos_KerningValuesPreserved() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var originalKerning = CollectKerningPairs(font); Debug.WriteLine(string.Format("Original font has {0} kerning pairs", originalKerning.Count)); @@ -265,7 +265,7 @@ public void SerializeGpos_PairPos_KerningValuesPreserved() [TestMethod] public void SerializeGpos_PairPos_SpecificPairsVerified() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); ushort fGlyph, eGlyph, aGlyph, vGlyph; font.CmapTable.TryGetGlyphId('f', out fGlyph); @@ -316,7 +316,7 @@ public void SerializeGpos_PairPos_SpecificPairsVerified() [TestMethod] public void SerializeGpos_PairPos_ValueFormatPreserved() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var origLookup = FindFirstLookupOfType(font.GposTable, 2); var origSubtable = origLookup.SubTables[0] as PairPosSubTableFormat1; @@ -342,7 +342,7 @@ public void SerializeGpos_PairPos_ValueFormatPreserved() [TestMethod] public void SerializeGpos_SinglePos_ValuesPreserved() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var singlePosLookup = FindFirstLookupOfType(font.GposTable, 1); if (singlePosLookup == null) @@ -387,7 +387,7 @@ public void SerializeGpos_SinglePos_ValuesPreserved() [TestMethod] public void SerializeGpos_MarkToBase_StructurePreserved() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var markToBaseLookup = FindFirstLookupOfType(font.GposTable, 4); if (markToBaseLookup == null) @@ -420,7 +420,7 @@ public void SerializeGpos_MarkToBase_StructurePreserved() [TestMethod] public void SerializeGpos_MarkToBase_AnchorPointsPreserved() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var markToBaseLookup = FindFirstLookupOfType(font.GposTable, 4); if (markToBaseLookup == null) @@ -482,7 +482,7 @@ public void SerializeGpos_MarkToBase_AnchorPointsPreserved() [TestMethod] public void SerializeGpos_FeatureLookupIndices_AreValid() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); @@ -506,7 +506,7 @@ public void SerializeGpos_FeatureLookupIndices_AreValid() [TestMethod] public void Diagnose_GposTableOffset() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); Debug.WriteLine("=== TABLE RECORDS ==="); foreach (var kvp in font.TableRecords) @@ -526,7 +526,7 @@ public void Diagnose_GposTableOffset() [TestMethod] public void SerializeGpos_LangSysFeatureIndices_AreValid() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); @@ -567,7 +567,7 @@ public void SerializeGpos_LangSysFeatureIndices_AreValid() [TestMethod] public void SerializeGpos_CoverageGlyphIds_AreValid() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var serializer = new OpenTypeFontSerializer(font); var bytes = serializer.Serialize(); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Serialization/KernSerializationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Serialization/KernSerializationTests.cs index 517675118a..61c0b94174 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Serialization/KernSerializationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Serialization/KernSerializationTests.cs @@ -20,7 +20,7 @@ public void SerializeKernTable() var ffi = FontScannerV2.FindBestMatch(@"c:\windows\fonts", "Arial", FontSubFamily.Regular); var originalBytes = ffi.GetTableBytes("kern"); - var font = OpenTypeFonts.LoadFont("Arial"); + var font = SystemFontsEngine.LoadFont("Arial"); var kernBytes = font?.KernTable.Serialize(font); Assert.AreEqual(originalBytes.Length, kernBytes?.Length); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs index c75f92cad8..3b487aae8e 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs @@ -118,7 +118,7 @@ public void Subset_MultipleChars_ShouldWork() [TestMethod] public void Subset_RoundtripHelper_ShouldWork() { - var parsedFont = FontTestHelper.RoundtripSubset("Roboto", "test", FontFolders); + var parsedFont = FontTestHelper.RoundtripSubset(TestFolderEngine, "Roboto", "test"); SaveFontForCurrentTest(parsedFont); @@ -129,7 +129,7 @@ public void Subset_RoundtripHelper_ShouldWork() [TestMethod] public void Check_Original_Roboto_Ligatures() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); font.CmapTable.TryGetGlyphId('f', out ushort fGlyph); font.CmapTable.TryGetGlyphId('i', out ushort iGlyph); @@ -168,7 +168,7 @@ public void Check_Original_Roboto_Ligatures() [TestMethod] public void Subset_Ligatures_ShouldStillWork() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); Debug.WriteLine("=== ORIGINAL ROBOTO ==="); if (font.GsubTable != null) @@ -373,7 +373,7 @@ public void Subset_WithGposKerning_ShouldPreservePositioning() [TestMethod] public void Subset_WithGposSingleAdjustment_ShouldPreserve() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var chars = new[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'A', 'B', 'C', 'D', 'E', ' ' @@ -405,7 +405,7 @@ public void Subset_WithGposSingleAdjustment_ShouldPreserve() [TestMethod] public void Subset_WithGposMarkToBase_ShouldPreserveAccents() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var chars = new[] { 'e', 'a', 'o', 'u', 'i', 'n', 'é', 'à', 'ö', 'ü', 'ñ', ' ', diff --git a/src/EPPlus.Fonts.OpenType.Tests/TestAssemblySetup.cs b/src/EPPlus.Fonts.OpenType.Tests/TestAssemblySetup.cs deleted file mode 100644 index e9891b1000..0000000000 --- a/src/EPPlus.Fonts.OpenType.Tests/TestAssemblySetup.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EPPlus.Fonts.OpenType.Tests -{ - [TestClass] - public class TestAssemblySetup - { - [AssemblyInitialize] - public static void AssemblyInit(TestContext context) - { - OpenTypeFonts.ClearFontCache(); - } - } -} diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs index 337e76e8c0..6dab819653 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs @@ -19,7 +19,6 @@ public class MarkToBaseTests : FontTestBase [TestInitialize] public void TestSetup() { - OpenTypeFonts.ClearFontCache(); } diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs index 80382fce9d..ab868d5dc2 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs @@ -812,7 +812,7 @@ public void Discovery_CheckFontsForSingleAdjustment() { try { - var font = OpenTypeFonts.LoadFont(fontName, subFamily); + var font = SystemFontsEngine.LoadFont(fontName, subFamily); if (font.GposTable == null) { diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs index c2817363b0..2c15ddbdae 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/VerticalTextShapingTests.cs @@ -82,7 +82,7 @@ public void ShapeVertical_CjkText_TotalAdvanceHeightIsPositive() public void ShapeVertical_EmptyString_ReturnsEmptyGlyphArray() { // Arrange - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(TestFolderEngine, font); // Act @@ -98,7 +98,7 @@ public void ShapeVertical_EmptyString_ReturnsEmptyGlyphArray() public void ShapeVertical_ClusterIndexMatchesCharacterPosition() { // Arrange - var font = OpenTypeFonts.LoadFont("BIZ UDGothic"); + var font = TestFolderEngine.LoadFont("BIZ UDGothic"); var shaper = new TextShaper(TestFolderEngine, font); var text = "ABC"; diff --git a/src/EPPlus.Fonts.OpenType.Tests/Validation/CmapTableValidationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Validation/CmapTableValidationTests.cs index 6ab5269792..ec80b55949 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Validation/CmapTableValidationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Validation/CmapTableValidationTests.cs @@ -23,7 +23,7 @@ public static void Initialize(TestContext testContext) [TestMethod] public void CmapTableValidation_Test() { - var font = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular); + var font = TestFolderEngine.LoadFont("Roboto", FontSubFamily.Regular); var validator = new CmapTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.CmapTable, context); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Validation/GlyphTableValidationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Validation/GlyphTableValidationTests.cs index 06d4ea8794..186234da91 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Validation/GlyphTableValidationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Validation/GlyphTableValidationTests.cs @@ -19,7 +19,7 @@ public class GlyphTableValidationTests : FontTestBase [TestMethod] public void LocaTableValidation_Test() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var validator = new LocaTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.LocaTable, context); @@ -29,7 +29,7 @@ public void LocaTableValidation_Test() [TestMethod] public void HmtxTableValidation_Test() { - var font = OpenTypeFonts.LoadFont("Roboto"); + var font = TestFolderEngine.LoadFont("Roboto"); var validator = new HmtxTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.HmtxTable, context); @@ -43,7 +43,7 @@ public void HmtxTableValidation_Test() [DataRow("Mulish", FontSubFamily.Regular)] public void GlyfTableValidation_Test(string fontName, FontSubFamily subFamily) { - var font = OpenTypeFonts.LoadFont(fontName, subFamily); + var font = TestFolderEngine.LoadFont(fontName, subFamily); var validator = new GlyfTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.GlyfTable, context); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Validation/GsubTableValidationTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Validation/GsubTableValidationTests.cs index 4c37fee533..4e3482ef6e 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Validation/GsubTableValidationTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Validation/GsubTableValidationTests.cs @@ -16,7 +16,7 @@ public class GsubTableValidationTests : FontTestBase [DataRow("NotoEmoji")] public void GsubTableValidation_Test(string fontName) { - var font = OpenTypeFonts.LoadFont(fontName); + var font = TestFolderEngine.LoadFont(fontName); var validator = new GsubTableValidator(); var context = new FontValidationContext(font); var result = validator.Validate(font.GsubTable, context); diff --git a/src/EPPlus.Fonts.OpenType/AssemblyInfo.cs b/src/EPPlus.Fonts.OpenType/AssemblyInfo.cs index 9217a433f0..aae4703231 100644 --- a/src/EPPlus.Fonts.OpenType/AssemblyInfo.cs +++ b/src/EPPlus.Fonts.OpenType/AssemblyInfo.cs @@ -2,5 +2,7 @@ using System.Security; [assembly: AllowPartiallyTrustedCallers] +[assembly: InternalsVisibleTo("EPPlus, PublicKey=00240000048000009400000006020000002400005253413100040000010001002981343969ed86fe604c56a84c61e33109424ef07bb458ff12e9533c11ea23ac8ef7e014b2a2de4ceb5f7528f963c755fe9b32f09cc35d21de94319d2a952a6e663cd46d6d98465998c77b52093d4f17cdc20ec054751244696f08afa6f4417d85267b147b73b6a3f5e9015b9dfd3dcc3328ce63df53a7c08a5544c1526ea5a5")] [assembly: InternalsVisibleTo("EPPlus.Fonts.OpenType.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e919e6cd771194b1b1ade084355488193b5e639a2ffed859ad956760b1489eab4e21c1949bf4e947cac2a430f232d15484062cb8314340b7db4d67b49b2e4c270bc02c457e06ee7dd4e6ecf09277543e696525f8d31d2228a1fe205aaa824573d08214c12d9ed7c73b5658d98131efd0ab69f9847c22f20b712f479f2d0032d6")] [assembly: InternalsVisibleTo("EPPlus.Export.Pdf.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100dd3a3466a88cbf5d374fe992cec433c48022414fe96608933e8e36782001213dd31bc454dc6f962a54a3a76cfb9e03a32cd4c658ecd49d1a98709971a080ab92d5c5b65346155f8d6422db4ffbf662f78913996a9a8b78ee11ff3cda7e585208cd4468fb3201f15bbb1dfc45c120703c9d6ad495bb9de66893ae5ab5ac8f40dc")] +[assembly: InternalsVisibleTo("EPPlusTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001001dd11308ec93a6ebcec727e183a8972dc6f95c23ecc34aa04f40cbfc9c17b08b4a0ea5c00dcd203bace44d15a30ce8796e38176ae88e960ceff9cc439ab938738ba0e603e3d155fc298799b391c004fc0eb4393dd254ce25db341eb43303e4c488c9500e126f1288594f0710ec7d642e9c72e76dd860649f1c48249c00e31fba")] diff --git a/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs b/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs index 3640404e4c..9306126e46 100644 --- a/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs +++ b/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs @@ -12,6 +12,7 @@ Date Author Change 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.FontCache; using EPPlus.Fonts.OpenType.FontResolver; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Interfaces.Fonts; @@ -40,6 +41,8 @@ public class DefaultFontProvider : IFontProvider private readonly OpenTypeFontEngine _engine; private readonly OpenTypeFont _primaryFont; + private readonly IFontSource _fontSource; + // Embedded fallbacks, lazy-loaded on first use. private readonly LazyFallbackFont _notoEmoji; private readonly LazyFallbackFont _notoMath; @@ -70,13 +73,27 @@ public 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) + : this(engine == null ? null : engine.FontStore, primaryFont) { + // The initializer runs first, so the null guard has to be in the expression above. + // This check exists only so the exception names 'engine' rather than 'fontSource' — + // the caller passed an engine and should be told about an engine. if (engine == null) throw new ArgumentNullException("engine"); + } + + /// + /// Creates a font provider over a font source. Used by the engine, which passes its own + /// store rather than itself — a glyph provider has no business reaching a shaper factory. + /// + internal DefaultFontProvider(IFontSource fontSource, OpenTypeFont primaryFont) + { + if (fontSource == null) + throw new ArgumentNullException("fontSource"); if (primaryFont == null) throw new ArgumentNullException("primaryFont"); - _engine = engine; + _fontSource = fontSource; _primaryFont = primaryFont; _notoEmoji = new LazyFallbackFont(EmbeddedFonts.LoadNotoEmoji); _notoMath = new LazyFallbackFont(EmbeddedFonts.LoadNotoMath); @@ -143,23 +160,6 @@ public IEnumerable GetAllFonts() // Internal helpers // ----------------------------------------------------------------------------------------- - /// - /// Resolves a shaper for a different font through this provider's engine. Used by the - /// layout engine to shape rich-text fragments that switch typeface, so the lookup goes - /// through the same engine that created this provider — not the global OpenTypeFonts - /// singleton. Kept internal: the engine dependency stays encapsulated here rather than - /// leaking onto IFontProvider. - /// - internal ITextShaper GetShaperForFont(IFontFormatBase font) - { - return _engine.GetShaperForFont(font); - } - - internal ITextShaper GetShaperForFont(MeasurementFont font) - { - return _engine.GetShaperForFont(font); - } - /// /// Tries to find the glyph in a lazy-loaded embedded fallback font (Noto Emoji / Math). /// @@ -236,7 +236,7 @@ private List ResolveScriptChain(UnicodeScript script) { var result = new List(); - var chainNames = _engine.GetScriptFallback(script); + var chainNames = _fontSource.GetScriptFallback(script); if (chainNames == null || chainNames.Length == 0) return result; @@ -248,13 +248,13 @@ private List ResolveScriptChain(UnicodeScript script) // 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); + var availability = _fontSource.GetFontAvailability(fontName, FontSubFamily.Regular); if (availability != FontAvailability.Exact) continue; try { - var font = _engine.LoadFont(fontName, FontSubFamily.Regular); + var font = _fontSource.LoadFont(fontName, FontSubFamily.Regular); if (font != null) result.Add(font); } diff --git a/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj b/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj index 11a02b7336..d6fe61859a 100644 --- a/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj +++ b/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj @@ -69,10 +69,12 @@ + PreserveNewest + @@ -92,6 +94,7 @@ + diff --git a/src/EPPlus.Fonts.OpenType/EmbeddedFonts.cs b/src/EPPlus.Fonts.OpenType/EmbeddedFonts.cs index 5c9d5cffc0..7ac8cb9702 100644 --- a/src/EPPlus.Fonts.OpenType/EmbeddedFonts.cs +++ b/src/EPPlus.Fonts.OpenType/EmbeddedFonts.cs @@ -68,7 +68,8 @@ private static OpenTypeFont LoadCached(string resourceName) "This is a bug in EPPlus.Fonts.OpenType - please report it."); } - font = OpenTypeFonts.GetFromBytes(bytes: ReadStreamFully(stream)); + font = new OpenTypeFont(fontBytes: ReadStreamFully(stream)); + font.EnsureFullyLoaded(); _cache[resourceName] = font; return font; } diff --git a/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs b/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs index 4aa7e14946..cb38f651aa 100644 --- a/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs +++ b/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs @@ -20,10 +20,18 @@ namespace EPPlus.Fonts.OpenType.FontResolver { /// /// Concrete implementation of . - /// Managed exclusively by — not instantiated by user code. - /// Mutations are intended to happen inside an OpenTypeFonts.Configure callback, - /// after which reads the resulting state as a snapshot and - /// rebuilds the resolver. + /// Created by — not instantiated by user code. + /// Mutations are intended to happen inside the configuration callback passed to the engine + /// constructor, or to ExcelWorkbook.ConfigureFonts, which forwards to it. + /// + /// The engine keeps a reference to this instance rather than copying it, and reads different + /// properties at different times: and + /// are read once while the resolver is built, so + /// changing them after the callback returns has no effect, whereas + /// , the per-script chains and + /// are read on each font resolution and so do take effect. + /// Callers should not rely on either behaviour; treat the configuration as fixed once the + /// callback returns and create a new engine to change it. /// internal class EpplusFontConfiguration : IEpplusFontConfiguration { @@ -39,6 +47,7 @@ internal class EpplusFontConfiguration : IEpplusFontConfiguration public EpplusFontConfiguration() { SearchSystemDirectories = true; + MetricsFallback = MetricsFallbackMode.WhenFontMissing; ApplyDefaultScriptFallbacks(); } @@ -75,6 +84,9 @@ public IDictionary FontFallbacks get { return _fontFallbacks; } } + /// + public MetricsFallbackMode MetricsFallback { get; set; } + /// public void SetScriptFallback(UnicodeScript script, params string[] fallbackFontNames) { @@ -98,6 +110,7 @@ public void Reset() FontResolver = null; _fontFallbacks.Clear(); _scriptFallbacks.Clear(); + MetricsFallback = MetricsFallbackMode.WhenFontMissing; ApplyDefaultScriptFallbacks(); } diff --git a/src/EPPlus.Fonts.OpenType/FontCache/OpenTypeFontCache.cs b/src/EPPlus.Fonts.OpenType/FontCache/OpenTypeFontCache.cs index c24af917c8..d088eadab3 100644 --- a/src/EPPlus.Fonts.OpenType/FontCache/OpenTypeFontCache.cs +++ b/src/EPPlus.Fonts.OpenType/FontCache/OpenTypeFontCache.cs @@ -68,6 +68,24 @@ public void BeginCache(string cacheKey) } } + /// + /// Removes a not-yet-loaded placeholder entry. Called when loading failed, so that + /// later lookups for the same key fail fast instead of spending the full Monitor.Wait + /// timeout waiting for a load that will never complete. + /// + public void RemoveIfNotLoaded(string cacheKey) + { + lock (_syncRoot) + { + CachedOpenTypeFont cached; + if (_cache.TryGetValue(cacheKey, out cached) && !cached.IsLoaded) + { + _cache.Remove(cacheKey); + Monitor.PulseAll(_syncRoot); + } + } + } + /// /// Adds or updates a fully loaded font using a prebuilt cache key. /// Signals all waiting threads that the font is now available. diff --git a/src/EPPlus.Fonts.OpenType/FontCache/ShaperCache.cs b/src/EPPlus.Fonts.OpenType/FontCache/ShaperCache.cs new file mode 100644 index 0000000000..672ccdf100 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/FontCache/ShaperCache.cs @@ -0,0 +1,98 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 09/02/2026 EPPlus Software AB Extracted from OpenTypeFontEngine + *************************************************************************************************/ +using EPPlus.Fonts.OpenType.TextShaping; +using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; +using System.Collections.Generic; + +namespace EPPlus.Fonts.OpenType.FontCache +{ + /// + /// Per-thread cache of shapers for one engine. Storage only — it holds no policy and never + /// decides which kind of shaper a request should get. + /// + /// The cache is per-thread because is stateful per call: font + /// tracking is reset at the start of every Shape, ShapeLight and ExtractCharWidths, and read + /// back afterwards through GetUsedFonts. Sharing one instance across threads would interleave + /// that state. + /// + /// Rendering and measurement shapers are kept in separate dictionaries. A measurement request + /// may store a metrics-only shaper, and a shared dictionary keyed only on font name and + /// subfamily would let a later rendering request find it. + /// + internal class ShaperCache + { + /// + /// Keyed on the cache instance rather than the engine. Keying on the engine would leave + /// this class depending on the one it was extracted from, and the engine already owns + /// exactly one of these. + /// + [System.ThreadStatic] + private static Dictionary _threadLocal; + + private class Entries + { + internal readonly Dictionary Rendering = + new Dictionary(); + + internal readonly Dictionary Measurement = + new Dictionary(); + } + + internal bool TryGetRendering(string key, out TextShaper shaper) + { + return GetOrCreateEntries().Rendering.TryGetValue(key, out shaper); + } + + internal void AddRendering(string key, TextShaper shaper) + { + GetOrCreateEntries().Rendering[key] = shaper; + } + + internal bool TryGetMeasurement(string key, out ITextShaper shaper) + { + return GetOrCreateEntries().Measurement.TryGetValue(key, out shaper); + } + + internal void AddMeasurement(string key, ITextShaper shaper) + { + GetOrCreateEntries().Measurement[key] = shaper; + } + + /// + /// Clears this cache for the calling thread only. Other threads see no entry for this + /// cache on their next access and rebuild from scratch. + /// + internal void ClearCurrentThread() + { + if (_threadLocal != null) + _threadLocal.Remove(this); + } + + private Entries GetOrCreateEntries() + { + // [ThreadStatic] field initializers only run on the primary thread. Every other + // thread sees null and must initialize on first use. + if (_threadLocal == null) + _threadLocal = new Dictionary(); + + Entries entries; + if (!_threadLocal.TryGetValue(this, out entries)) + { + entries = new Entries(); + _threadLocal[this] = entries; + } + return entries; + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/FontDiscovery.cs b/src/EPPlus.Fonts.OpenType/FontDiscovery.cs new file mode 100644 index 0000000000..5ee7c96a39 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/FontDiscovery.cs @@ -0,0 +1,91 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 09/02/2026 EPPlus Software AB Extracted from OpenTypeFontEngine + *************************************************************************************************/ +using EPPlus.Fonts.OpenType.FontResolver; +using EPPlus.Fonts.OpenType.Scanner; +using OfficeOpenXml.Interfaces.Fonts; +using System; +using System.Collections.Generic; +using System.IO; + +namespace EPPlus.Fonts.OpenType +{ + /// + /// Diagnostic and discovery API over the font file system. Independent of any engine's + /// configuration: the directories to search are passed in, not read from a configuration, + /// which is why this never belonged on the engine. + /// + public static class FontDiscovery + { + /// + /// Returns all available font faces as fully loaded instances. + /// Skips corrupt or unreadable fonts but writes diagnostics for each failure. + /// Not cached, and may take significant time to complete. + /// + public static List GetAllBaseFontData( + List fontDirectories, + bool searchSystemDirectories = true, + FontFormat? formatTarget = null) + { + var locations = DefaultFontLocations.GetLocationsCollection(fontDirectories, searchSystemDirectories); + var faces = FontScannerV2.EnumerateAllFaces(locations); + + var result = new List(faces.Count); + var failures = 0; + + foreach (var face in faces) + { + if (formatTarget.HasValue && !MatchesFormat(face.FilePath, formatTarget.Value)) + continue; + + try + { + var font = new OpenTypeFont(File.ReadAllBytes(face.FilePath)); + font.EnsureFullyLoaded(); + result.Add(font); + } + catch (Exception ex) + { + failures++; + System.Diagnostics.Debug.WriteLine( + string.Format("[FontDiscovery] Failed to load font: {0} => {1}: {2}", + face.FilePath, ex.GetType().Name, ex.Message)); + } + } + + if (failures > 0) + { + System.Diagnostics.Debug.WriteLine( + string.Format("[FontDiscovery] {0} font(s) failed to load.", failures)); + } + + return result; + } + + private static bool MatchesFormat(string filePath, FontFormat target) + { + string ext = Path.GetExtension(filePath); + if (string.IsNullOrEmpty(ext)) + { + // No extension: format is undetermined, so do not filter it out. + return true; + } + + ext = ext.ToLowerInvariant(); + var format = (ext == ".otf" || ext == ".cff") + ? FontFormat.Otf + : FontFormat.Ttf; + + return format == target; + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/FontStore.cs b/src/EPPlus.Fonts.OpenType/FontStore.cs new file mode 100644 index 0000000000..35e2e6e2ad --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/FontStore.cs @@ -0,0 +1,189 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 09/02/2026 EPPlus Software AB Extracted from OpenTypeFontEngine + *************************************************************************************************/ +using EPPlus.Fonts.OpenType.FontResolver; +using OfficeOpenXml.Interfaces.Fonts; +using System; +using System.Collections.Generic; + +namespace EPPlus.Fonts.OpenType.FontCache +{ + /// + /// Resolves, parses and caches fonts for one . + /// Owns the resolver, the parsed-font cache and the per-font locks. + /// + /// One instance per engine: two engines never share parsed fonts, because their resolver + /// configurations may produce different fonts for the same name. + /// + internal class FontStore : IFontSource + { + private readonly object _syncRoot = new object(); + private readonly Dictionary _fontLocks = new Dictionary(); + private readonly OpenTypeFontCache _fontCache = new OpenTypeFontCache(); + private readonly IFontResolver _resolver; + private readonly EpplusFontConfiguration _configuration; + + private bool _disposed; + + internal FontStore(IFontResolver resolver, EpplusFontConfiguration configuration) + { + if (resolver == null) + throw new ArgumentNullException("resolver"); + if (configuration == null) + throw new ArgumentNullException("configuration"); + + _resolver = resolver; + _configuration = configuration; + } + + // ----------------------------------------------------------------------------------------- + // Font loading + // ----------------------------------------------------------------------------------------- + + /// + /// Loads a font by name and subfamily, with thread-safe caching. + /// Returns null if the font cannot be resolved. + /// + internal OpenTypeFont LoadFont(string fontName, FontSubFamily subFamily, bool ignoreCache) + { + ThrowIfDisposed(); + + if (ignoreCache) + return ResolveAndCreate(_resolver, fontName, subFamily); + + string lockKey = BuildCacheKey(fontName, subFamily); + object fontLock; + lock (_syncRoot) + { + if (!_fontLocks.TryGetValue(lockKey, out fontLock)) + { + fontLock = new object(); + _fontLocks[lockKey] = fontLock; + } + } + + lock (fontLock) + { + var cached = _fontCache.GetFromCache(lockKey); + if (cached != null && cached.Font != null && cached.IsLoaded) + { + cached.Font.EnsureFullyLoaded(); + return cached.Font; + } + + _fontCache.BeginCache(lockKey); + + var font = ResolveAndCreate(_resolver, fontName, subFamily); + if (font == null) + { + // BeginCache left a not-loaded placeholder. Nothing will ever complete it, + // so remove it — otherwise every later GetFromCache for this key spends the + // full two-second Monitor.Wait timeout before giving up. + _fontCache.RemoveIfNotLoaded(lockKey); + return null; + } + + font.EnsureFullyLoaded(); + font.IsReadOnly = true; + _fontCache.AddToCache(font, lockKey); + return font; + } + } + + /// + public OpenTypeFont LoadFont(string fontName, FontSubFamily subFamily) + { + return LoadFont(fontName, subFamily, false); + } + + // ----------------------------------------------------------------------------------------- + // Availability and configuration + // ----------------------------------------------------------------------------------------- + + /// + /// Checks whether a font is available in the configured font system. + /// + /// If the resolver implements the call delegates + /// to it. Otherwise it probes via , which can only + /// distinguish found from not found — never , and + /// never NotFound at all for a resolver that substitutes internally. + /// + public FontAvailability GetFontAvailability(string fontName, FontSubFamily subFamily) + { + ThrowIfDisposed(); + if (fontName == null) + throw new ArgumentNullException("fontName"); + + var provider = _resolver as IFontAvailabilityProvider; + if (provider != null) + return provider.GetFontAvailability(fontName, subFamily); + + return _resolver.ResolveFont(fontName, subFamily) != null + ? FontAvailability.Exact + : FontAvailability.NotFound; + } + + /// + public string[] GetScriptFallback(UnicodeScript script) + { + return _configuration.GetScriptFallback(script); + } + + // ----------------------------------------------------------------------------------------- + // Lifecycle + // ----------------------------------------------------------------------------------------- + + internal void Clear() + { + lock (_syncRoot) + { + _fontCache.Clear(); + _fontLocks.Clear(); + } + } + + /// + /// Called by the engine from Dispose. A holds this store + /// directly, so without a flag here it could keep loading fonts after the engine that owns + /// it was disposed — the engine's own disposed check no longer covers every path in. + /// + internal void MarkDisposed() + { + _disposed = true; + Clear(); + } + + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException("OpenTypeFontEngine"); + } + + // ----------------------------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------------------------- + + internal static string BuildCacheKey(string fontName, FontSubFamily subFamily) + { + return string.Format("{0}_{1}", fontName, subFamily); + } + + private static OpenTypeFont ResolveAndCreate(IFontResolver resolver, string fontName, FontSubFamily subFamily) + { + var bytes = resolver.ResolveFont(fontName, subFamily); + if (bytes == null) + return null; + + return new OpenTypeFont(bytes); + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/FontSubFamilyConverter.cs b/src/EPPlus.Fonts.OpenType/FontSubFamilyConverter.cs new file mode 100644 index 0000000000..d617fa125f --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/FontSubFamilyConverter.cs @@ -0,0 +1,56 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 09/02/2026 EPPlus Software AB Extracted from OpenTypeFontEngine + *************************************************************************************************/ +using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; + +namespace EPPlus.Fonts.OpenType +{ + /// + /// Maps between and . + /// Neither type has anything to do with the font engine, so the mapping does not live there. + /// + public static class FontSubFamilyConverter + { + public static FontSubFamily ToSubFamily(MeasurementFontStyles style) + { + bool bold = (style & MeasurementFontStyles.Bold) == MeasurementFontStyles.Bold; + bool italic = (style & MeasurementFontStyles.Italic) == MeasurementFontStyles.Italic; + + if (bold && italic) + return FontSubFamily.BoldItalic; + if (bold) + return FontSubFamily.Bold; + if (italic) + return FontSubFamily.Italic; + + return FontSubFamily.Regular; + } + + public static MeasurementFontStyles ToStyles(FontSubFamily subFamily) + { + switch (subFamily) + { + case FontSubFamily.Bold: + return MeasurementFontStyles.Bold; + case FontSubFamily.Italic: + return MeasurementFontStyles.Italic; + case FontSubFamily.BoldItalic: + return MeasurementFontStyles.Bold | MeasurementFontStyles.Italic; + default: + // MeasurementFontStyles.Regular if that member exists; otherwise the + // zero value, which is what Regular means for a flags enum. + return default(MeasurementFontStyles); + } + } + } +} \ No newline at end of file diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/FontMetricsClass.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontMetricsClass.cs similarity index 89% rename from src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/FontMetricsClass.cs rename to src/EPPlus.Fonts.OpenType/GenericFontWidths/FontMetricsClass.cs index e39dd343d5..2dd88f2280 100644 --- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/FontMetricsClass.cs +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontMetricsClass.cs @@ -10,12 +10,7 @@ Date Author Change ************************************************************************************************* 12/26/2021 EPPlus Software AB EPPlus 6.0 *************************************************************************************************/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts.GenericMeasurements +namespace EPPlus.Fonts.OpenType.GenericFontWidths { internal enum FontMetricsClass : byte { diff --git a/src/EPPlus/Core/Worksheet/Fonts/FontMetricsFamilies.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontMetricsFamilies.cs similarity index 91% rename from src/EPPlus/Core/Worksheet/Fonts/FontMetricsFamilies.cs rename to src/EPPlus.Fonts.OpenType/GenericFontWidths/FontMetricsFamilies.cs index cdf6c5a516..358d2af819 100644 --- a/src/EPPlus/Core/Worksheet/Fonts/FontMetricsFamilies.cs +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontMetricsFamilies.cs @@ -10,12 +10,7 @@ Date Author Change ************************************************************************************************* 12/26/2021 EPPlus Software AB EPPlus 6.0 *************************************************************************************************/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts +namespace EPPlus.Fonts.OpenType.GenericFontWidths { internal enum FontMetricsFamilies : ushort { diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/FontScaleFactor.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontScaleFactor.cs similarity index 93% rename from src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/FontScaleFactor.cs rename to src/EPPlus.Fonts.OpenType/GenericFontWidths/FontScaleFactor.cs index 119b1ef398..82de4a5ac1 100644 --- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/FontScaleFactor.cs +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontScaleFactor.cs @@ -10,12 +10,7 @@ Date Author Change ************************************************************************************************* 12/26/2021 EPPlus Software AB EPPlus 6.0 *************************************************************************************************/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace OfficeOpenXml.Core.Worksheet.Fonts.GenericFontMetrics +namespace EPPlus.Fonts.OpenType.GenericFontWidths { internal class FontScaleFactor { diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/FontScaleFactors.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontScaleFactors.cs similarity index 97% rename from src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/FontScaleFactors.cs rename to src/EPPlus.Fonts.OpenType/GenericFontWidths/FontScaleFactors.cs index e43a7fe128..728e7d7c79 100644 --- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/FontScaleFactors.cs +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontScaleFactors.cs @@ -10,14 +10,9 @@ Date Author Change ************************************************************************************************* 12/26/2021 EPPlus Software AB EPPlus 6.0 *************************************************************************************************/ -using OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts.GenericMeasurements; -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts; -namespace OfficeOpenXml.Core.Worksheet.Fonts.GenericFontMetrics +namespace EPPlus.Fonts.OpenType.GenericFontWidths { /// /// The binary files created for text measurements of various font families just contains @@ -40,7 +35,7 @@ public FontScaleFactors() private static uint GetKey(FontMetricsFamilies family, FontSubFamilies subFamily) { - return GenericFontMetricsTextMeasurer.GetKey(family, subFamily); + return GenericTextMeasurerKey.GetKey(family, subFamily); } private static FontScaleFactor CSF(float s, float m, float l) diff --git a/src/EPPlus/FontSize.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontSize.cs similarity index 97% rename from src/EPPlus/FontSize.cs rename to src/EPPlus.Fonts.OpenType/GenericFontWidths/FontSize.cs index 0cc5186660..229c664fd8 100644 --- a/src/EPPlus/FontSize.cs +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontSize.cs @@ -1,4 +1,4 @@ -/************************************************************************************************* +/************************************************************************************************* 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 @@ -16,10 +16,8 @@ Date Author Change using System.Reflection; using System.IO; using System.Text; -using OfficeOpenXml.Utils; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; -namespace OfficeOpenXml +namespace EPPlus.Fonts.OpenType.GenericFontWidths { /// /// A collection of fonts and there size in pixels used when determining auto widths for columns. @@ -36,8 +34,8 @@ public class FontSize /// public const string NonExistingFont = "Arial"; internal static bool _isLoaded = false; - internal static object _lockObj=new object(); - internal static MemoryStream _fontStream=null; + internal static object _lockObj = new object(); + internal static MemoryStream _fontStream = null; /// /// Dictionary containing Font Width in pixels. /// You can add your own fonts and sizes here. @@ -140,9 +138,9 @@ private static double GetWidthHeight(string fontName, float fontSize, bool width } if (min > -1) { - if(fontSize > 72) + if (fontSize > 72) { - return Convert.ToDouble((int)(font[min] / min * fontSize)); + return Convert.ToDouble((int)(font[min] / min * fontSize)); } return font[min]; } @@ -165,7 +163,7 @@ internal static Dictionary GetFontSize(string fontName, bool width } else { - if (_isLoaded==false) + if (_isLoaded == false) { LoadAllFontsFromResource(); if (fontColl.ContainsKey(fontName)) @@ -194,18 +192,18 @@ public static void LoadAllFontsFromResource() /// /// The name of the font. /// If false the stream is loading the font is kept open to load other fonts faster. It true the font-stream is disposed on exit. - public static void LoadFontsFromResource(string fontName, bool disposeStream=true) - { + public static void LoadFontsFromResource(string fontName, bool disposeStream = true) + { lock (_lockObj) { if (_isLoaded) return; - if(_fontStream!=null) + if (_fontStream != null) { ReadFontSize(_fontStream, fontName); _isLoaded = string.IsNullOrEmpty(fontName); } var assembly = Assembly.GetExecutingAssembly(); - var stream = assembly.GetManifestResourceStream("OfficeOpenXml.resources.fontsize.zip"); + var stream = assembly.GetManifestResourceStream("EPPlus.Fonts.OpenType.Resources.fontsize.zip"); using (stream) { @@ -219,7 +217,7 @@ public static void LoadFontsFromResource(string fontName, bool disposeStream=tru var br = new BinaryReader(zipStream); if (string.IsNullOrEmpty(fontName)) { - using (var ms = EPPlusMemoryManager.GetStream(br.ReadBytes((int)entry.UncompressedSize))) + using (var ms = new MemoryStream(br.ReadBytes((int)entry.UncompressedSize))) { ReadFontSize(ms, fontName); } @@ -227,20 +225,20 @@ public static void LoadFontsFromResource(string fontName, bool disposeStream=tru } else { - _fontStream = EPPlusMemoryManager.GetStream(br.ReadBytes((int)entry.UncompressedSize)); + _fontStream =new MemoryStream(br.ReadBytes((int)entry.UncompressedSize)); ReadFontSize(_fontStream, fontName); } } } } } - if (disposeStream && _fontStream!=null) + if (disposeStream && _fontStream != null) { _fontStream.Dispose(); _fontStream = null; } } - } + } private static void ReadFontSize(MemoryStream stream, string fontName) { var br = new BinaryReader(stream); @@ -248,7 +246,7 @@ private static void ReadFontSize(MemoryStream stream, string fontName) //Read all font names first var fonts = new Dictionary(); var fno = 0; - while (fno(); br.BaseStream.Position = dataPos; var length = br.ReadUInt16(); - var pos = 0; + var pos = 0; while (pos < length) { var s = br.ReadUInt16(); diff --git a/src/EPPlus/Core/Worksheet/Fonts/FontSubFamilies.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontSubFamilies.cs similarity index 87% rename from src/EPPlus/Core/Worksheet/Fonts/FontSubFamilies.cs rename to src/EPPlus.Fonts.OpenType/GenericFontWidths/FontSubFamilies.cs index e7142d552d..c7ea357bc4 100644 --- a/src/EPPlus/Core/Worksheet/Fonts/FontSubFamilies.cs +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/FontSubFamilies.cs @@ -10,12 +10,7 @@ Date Author Change ************************************************************************************************* 12/26/2021 EPPlus Software AB EPPlus 6.0 *************************************************************************************************/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts +namespace EPPlus.Fonts.OpenType.GenericFontWidths { internal enum FontSubFamilies : ushort { diff --git a/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericFontMetricsCache.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericFontMetricsCache.cs new file mode 100644 index 0000000000..a5069755d5 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericFontMetricsCache.cs @@ -0,0 +1,122 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 09/01/2026 EPPlus Software AB Initial implementation + *************************************************************************************************/ +using System.Collections.Generic; + +namespace EPPlus.Fonts.OpenType.GenericFontWidths +{ + /// + /// Process wide cache of the serialized font metrics, and the single place that decides + /// which font key a request resolves to. + /// + /// Fonts are loaded on first use rather than all at once. The set of available keys is read + /// from the archive's file names, so IsValidFont and ResolveFontKey can answer without + /// decompressing anything; only fonts that are actually measured get parsed. A workbook + /// using two fonts holds a few kB here rather than the whole library. + /// + internal static class GenericFontMetricsCache + { + private static readonly Dictionary _loaded = + new Dictionary(); + private static HashSet _availableKeys; + private static readonly object _syncRoot = new object(); + + private static HashSet AvailableKeys + { + get + { + if (_availableKeys == null) + { + lock (_syncRoot) + { + if (_availableKeys == null) + { + _availableKeys = GenericFontMetricsLoader.LoadAvailableFontKeys(); + } + } + } + return _availableKeys; + } + } + + /// + /// Returns the metrics for a font key, or null when the archive holds no such font. + /// + internal static SerializedFontMetrics GetMetrics(uint fontKey) + { + SerializedFontMetrics metrics; + lock (_syncRoot) + { + if (_loaded.TryGetValue(fontKey, out metrics)) + { + return metrics; + } + } + + if (!AvailableKeys.Contains(fontKey)) return null; + + // Parsed outside the lock; a duplicate parse under contention is cheaper than + // holding the lock across the decompression, and the result is identical either way. + var parsed = GenericFontMetricsLoader.LoadFontMetrics(fontKey); + if (parsed == null) return null; + + lock (_syncRoot) + { + if (_loaded.TryGetValue(fontKey, out metrics)) + { + return metrics; + } + _loaded[fontKey] = parsed; + return parsed; + } + } + + /// + /// True when the archive contains metrics for the font key. Does not load them. + /// + internal static bool IsValidFont(uint fontKey) + { + return AvailableKeys.Contains(fontKey); + } + + /// + /// Resolves the key that should actually be measured against, falling back to the + /// Regular subfamily of the same family when the requested subfamily has no metrics. + /// Returns uint.MaxValue when the family is unknown entirely. + /// + /// Not every family ships all four subfamilies. Windows has no Arial Black Bold, no + /// Impact Italic, no Calibri Light Bold and no Tahoma Italic, among others - fifteen + /// combinations in total. Those used to be generated anyway, by taking the Regular + /// font's advance widths and writing them out under the requested subfamily. Now those + /// files are simply absent, and the substitution happens here where it is visible + /// instead of being baked into the data. + /// + /// The fallback deliberately does not widen anything for Bold. The generator's old + /// attempt at that had no effect on the reported widths, so falling straight through to + /// Regular reproduces the previous behaviour exactly. + /// + internal static uint ResolveFontKey(uint requestedKey) + { + if (requestedKey == uint.MaxValue) return uint.MaxValue; + if (AvailableKeys.Contains(requestedKey)) return requestedKey; + + // The low 16 bits hold the subfamily; clearing them gives the Regular variant. + var regularKey = requestedKey & 0xFFFF0000; + if (regularKey != requestedKey && AvailableKeys.Contains(regularKey)) + { + return regularKey; + } + + return uint.MaxValue; + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericFontMetricsLoader.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericFontMetricsLoader.cs new file mode 100644 index 0000000000..f6767b8bae --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericFontMetricsLoader.cs @@ -0,0 +1,153 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 12/26/2021 EPPlus Software AB EPPlus 6.0 + 09/01/2026 EPPlus Software AB Per-font loading + *************************************************************************************************/ +using OfficeOpenXml.Packaging.Ionic.Zip; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace EPPlus.Fonts.OpenType.GenericFontWidths +{ + /// + /// Loads serialized font metrics from the resources/TextMetrics.zip archive. + /// + internal static class GenericFontMetricsLoader + { + private const string ResourceName = "EPPlus.Fonts.OpenType.Resources.TextMetrics.zip"; + + /// + /// Loads every font in the archive. + /// + internal static Dictionary LoadFontMetrics() + { + var fonts = new Dictionary(); + using (var stream = GetResourceStream()) + { + var zipStream = new ZipInputStream(stream); + ZipEntry entry; + while ((entry = zipStream.GetNextEntry()) != null) + { + if (entry.IsDirectory || Path.GetExtension(entry.FileName) != ".fmtr") continue; + + var metrics = ReadEntry(zipStream, entry); + fonts[metrics.GetKey()] = metrics; + } + } + return fonts; + } + + /// + /// The font keys present in the archive, taken from the file names without reading or + /// decompressing any of them. Lets the caller answer "does this font exist" without + /// paying for the metrics of fonts nobody asked about. + /// + internal static HashSet LoadAvailableFontKeys() + { + var keys = new HashSet(); + using (var stream = GetResourceStream()) + { + var zipStream = new ZipInputStream(stream); + ZipEntry entry; + while ((entry = zipStream.GetNextEntry()) != null) + { + if (entry.IsDirectory || Path.GetExtension(entry.FileName) != ".fmtr") continue; + + uint key; + if (TryGetKeyFromFileName(entry.FileName, out key)) + { + keys.Add(key); + } + } + } + return keys; + } + + /// + /// Loads a single font. Returns null when the archive holds no such font. + /// + /// Most workbooks use one or two fonts, so loading all 101 on first measurement is + /// almost entirely wasted. The file name is the font key, so the right entry can be + /// found without decompressing the others. + /// + internal static SerializedFontMetrics LoadFontMetrics(uint fontKey) + { + using (var stream = GetResourceStream()) + { + var zipStream = new ZipInputStream(stream); + ZipEntry entry; + while ((entry = zipStream.GetNextEntry()) != null) + { + if (entry.IsDirectory || Path.GetExtension(entry.FileName) != ".fmtr") continue; + + uint key; + if (!TryGetKeyFromFileName(entry.FileName, out key) || key != fontKey) continue; + + return ReadEntry(zipStream, entry); + } + } + return null; + } + + private static Stream GetResourceStream() + { + var assembly = Assembly.GetExecutingAssembly(); + var stream = assembly.GetManifestResourceStream(ResourceName); + if (stream == null) + { + throw new InvalidOperationException("Embedded resource not found: " + ResourceName); + } + return stream; + } + + private static bool TryGetKeyFromFileName(string fileName, out uint key) + { + var name = Path.GetFileNameWithoutExtension(fileName); + return uint.TryParse(name, out key); + } + + private static SerializedFontMetrics ReadEntry(ZipInputStream zipStream, ZipEntry entry) + { + var bytes = ReadExactly(zipStream, (int)entry.UncompressedSize); + using (var ms = new MemoryStream(bytes)) + { + return GenericFontMetricsSerializer.Deserialize(ms); + } + } + + /// + /// Reads exactly count bytes. + /// + /// The previous code called Read once and assigned the result to an unused variable. A + /// decompressing stream is allowed to return fewer bytes than asked for, and when it + /// did the shortfall showed up as a truncated font file rather than an error. + /// + private static byte[] ReadExactly(Stream stream, int count) + { + var buffer = new byte[count]; + var offset = 0; + while (offset < count) + { + var read = stream.Read(buffer, offset, count - offset); + if (read <= 0) + { + throw new EndOfStreamException( + string.Format("Expected {0} bytes of font metrics but the stream ended after {1}.", + count, offset)); + } + offset += read; + } + return buffer; + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericFontMetricsSerializer.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericFontMetricsSerializer.cs new file mode 100644 index 0000000000..9a8691af75 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericFontMetricsSerializer.cs @@ -0,0 +1,116 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 12/26/2021 EPPlus Software AB EPPlus 6.0 + 09/01/2026 EPPlus Software AB Keep ranges instead of expanding them + *************************************************************************************************/ +using System; +using System.IO; +using System.Text; + +namespace EPPlus.Fonts.OpenType.GenericFontWidths +{ + internal static class GenericFontMetricsSerializer + { + public static readonly Encoding FileEncoding = Encoding.UTF8; + + /// + /// Highest .fmtr version this reader understands. + /// + /// The version field has been written since the format was introduced but never + /// checked. Rejecting what we cannot read matters as soon as a version 2 exists, + /// because the alternative is reading its extra fields as character data. + /// + internal const ushort MaxSupportedVersion = 2; + + public static SerializedFontMetrics Deserialize(Stream stream) + { + using (var reader = new BinaryReader(stream, FileEncoding)) + { + var metrics = new SerializedFontMetrics(); + metrics.Version = reader.ReadUInt16(); + if (metrics.Version > MaxSupportedVersion) + { + throw new InvalidDataException( + string.Format("Unsupported font metrics version {0}. This build reads up to version {1}.", + metrics.Version, MaxSupportedVersion)); + } + + metrics.Family = (FontMetricsFamilies)reader.ReadUInt16(); + metrics.SubFamily = (FontSubFamilies)reader.ReadUInt16(); + metrics.LineHeight1em = reader.ReadSingle(); + metrics.DefaultWidthClass = (FontMetricsClass)reader.ReadByte(); + + var nClassWidths = reader.ReadUInt16(); + if (nClassWidths == 0) + { + ReadVerticalMetrics(reader, metrics); + metrics.Seal(); + return metrics; + } + + for (var x = 0; x < nClassWidths; x++) + { + var cls = (FontMetricsClass)reader.ReadByte(); + metrics.SetClassWidth(cls, reader.ReadSingle()); + } + + var nClasses = reader.ReadUInt16(); + for (var x = 0; x < nClasses; x++) + { + var cls = (FontMetricsClass)reader.ReadByte(); + + var nRanges = reader.ReadUInt16(); + for (var rngIx = 0; rngIx < nRanges; rngIx++) + { + var start = reader.ReadUInt16(); + var end = reader.ReadUInt16(); + // Kept as a range. The previous version expanded it here into one + // dictionary entry per character in the span. + metrics.AddRange(start, end, cls); + } + + var nCharactersInClass = reader.ReadUInt16(); + for (var y = 0; y < nCharactersInClass; y++) + { + metrics.AddCharacter(reader.ReadUInt16(), cls); + } + } + + ReadVerticalMetrics(reader, metrics); + metrics.Seal(); + return metrics; + } + } + + /// + /// Reads the version 2 vertical metrics, which sit at the end of the file after the + /// class section. Placing them last keeps the version 1 part of a version 2 file byte + /// identical to a version 1 file. + /// + /// The line height is derived from the three rather than read from its own field. All + /// three go through the generator's per-value adjustment, so a separately written total + /// would no longer equal their sum and the baseline could end up outside the line box. + /// + private static void ReadVerticalMetrics(BinaryReader reader, SerializedFontMetrics metrics) + { + if (metrics.Version < 2) + { + metrics.ApproximateVerticalMetricsFromLineHeight(); + return; + } + + metrics.Ascender1em = reader.ReadSingle(); + metrics.Descender1em = reader.ReadSingle(); + metrics.LineGap1em = reader.ReadSingle(); + metrics.LineHeight1em = metrics.Ascender1em + metrics.Descender1em + metrics.LineGap1em; + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericTextMeasurerKey.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericTextMeasurerKey.cs new file mode 100644 index 0000000000..bed6bf8a32 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/GenericTextMeasurerKey.cs @@ -0,0 +1,83 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 12/26/2021 EPPlus Software AB EPPlus 6.0 + 09/01/2026 EPPlus Software AB Added ResolveKey with subfamily fallback + *************************************************************************************************/ +using OfficeOpenXml.Interfaces.Drawing.Text; +using System; + +namespace EPPlus.Fonts.OpenType.GenericFontWidths +{ + public static class GenericTextMeasurerKey + { + internal static uint GetKey(FontMetricsFamilies family, FontSubFamilies subFamily) + { + var k1 = (ushort)family; + var k2 = (ushort)subFamily; + return (uint)((k1 << 16) | ((k2) & 0xffff)); + } + + internal static uint GetKey(string fontFamily, MeasurementFontStyles fontStyle) + { + var enumName = fontFamily.Replace(" ", string.Empty); + var values = Enum.GetValues(typeof(FontMetricsFamilies)); + var supported = false; + foreach (var enumVal in values) + { + if (enumVal.ToString() == enumName) + { + supported = true; + break; + } + } + if (!supported) return uint.MaxValue; + var family = (FontMetricsFamilies)Enum.Parse(typeof(FontMetricsFamilies), enumName); + var subFamily = FontSubFamilies.Regular; + switch (fontStyle) + { + case MeasurementFontStyles.Bold: + subFamily = FontSubFamilies.Bold; + break; + case MeasurementFontStyles.Italic: + subFamily = FontSubFamilies.Italic; + break; + case MeasurementFontStyles.Italic | MeasurementFontStyles.Bold: + subFamily = FontSubFamilies.BoldItalic; + break; + default: + break; + } + return GetKey(family, subFamily); + } + + /// + /// Like , but returns a key that + /// actually has metrics behind it: if the requested subfamily is missing, the Regular + /// subfamily of the same family is used. Returns uint.MaxValue when neither exists. + /// + /// Callers that go on to measure should use this rather than GetKey. See + /// for why the substitution is + /// needed and why it does not adjust anything for Bold. + /// + internal static uint ResolveKey(string fontFamily, MeasurementFontStyles fontStyle) + { + return GenericFontMetricsCache.ResolveFontKey(GetKey(fontFamily, fontStyle)); + } + + /// + /// Overload for callers that already have the enum values. + /// + internal static uint ResolveKey(FontMetricsFamilies family, FontSubFamilies subFamily) + { + return GenericFontMetricsCache.ResolveFontKey(GetKey(family, subFamily)); + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/GenericFontWidths/SerializedFontMetrics.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/SerializedFontMetrics.cs new file mode 100644 index 0000000000..3da8ef7022 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/SerializedFontMetrics.cs @@ -0,0 +1,353 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 12/26/2021 EPPlus Software AB EPPlus 6.0 + 09/01/2026 EPPlus Software AB Compact character mapping + *************************************************************************************************/ +using System; +using System.Collections.Generic; + +namespace EPPlus.Fonts.OpenType.GenericFontWidths +{ + /// + /// Font metrics deserialized from a .fmtr file. + /// + /// The character to width class mapping used to be a Dictionary<char, FontMetricsClass> + /// with one entry per character. The .fmtr files are only 222 kB uncompressed precisely + /// because they store ranges, and expanding those ranges into dictionary entries on load + /// threw that away: 138 617 entries across the library at roughly 16 bytes each came to + /// 2.12 MB. Keeping the ranges as ranges brings that to about 335 kB. + /// + /// Lookup is a direct index for U+0020 to U+00FF, which covers nearly all real cell + /// content, and a binary search over the sorted ranges and single characters beyond that. + /// The Latin-1 table costs 224 bytes per font and keeps the common path at the same cost as + /// the old hash lookup. + /// + /// Build with AddRange and AddCharacter, then call Seal before use. + /// + internal class SerializedFontMetrics + { + private const int LatinFirst = 0x20; + private const int LatinLast = 0xFF; + private const byte NotMapped = 0xFF; + + /// + /// Number of width classes the format allows, and the size of the class width table. + /// + private const int MaxClasses = 32; + + private readonly float[] _classWidths = new float[MaxClasses]; + private readonly bool[] _classWidthSet = new bool[MaxClasses]; + + // Populated while building, released by Seal. + private List _buildRangeStart = new List(); + private List _buildRangeEnd = new List(); + private List _buildRangeClass = new List(); + private List _buildSingleChar = new List(); + private List _buildSingleClass = new List(); + + // Sorted by start / character. Parallel arrays rather than an array of structs so that + // the byte class does not get padded up to the alignment of the ushort. + private ushort[] _rangeStart; + private ushort[] _rangeEnd; + private byte[] _rangeClass; + private ushort[] _singleChar; + private byte[] _singleClass; + + private byte[] _latin; + + public FontMetricsFamilies Family { get; set; } + + public FontSubFamilies SubFamily { get; set; } + + public ushort Version { get; set; } + + public uint FontKey { get; set; } + + /// + /// Baseline to baseline distance, in em scaled by 96/72 like the class widths. + /// + /// For version 2 files this is derived from Ascender1em, Descender1em and LineGap1em + /// rather than read from the file, so the three can never disagree with the total. The + /// version 1 field is still read for version 1 files. + /// + public float LineHeight1em { get; set; } + + /// + /// Distance from the baseline to the top of the line box, in the same unit as + /// LineHeight1em. + /// + /// Version 1 files do not carry this, so it is approximated from the line height. The + /// ascent share measured across the 101 shipped fonts runs from 0.7349 (Courier New) to + /// 0.8289 (Tahoma) with a median of 0.8047, so any single constant is out by up to 7% + /// of the font height - about 1.2px at 11pt. That spread is why version 2 exists. + /// + public float Ascender1em { get; set; } + + /// + /// Distance from the baseline down to the bottom of the line box, in the same unit as + /// LineHeight1em. Positive, unlike the OpenType convention, so that ascender plus + /// descender plus line gap is the line height with no sign to get wrong. + /// + public float Descender1em { get; set; } + + /// + /// Extra leading between lines, in the same unit as LineHeight1em. Zero for every font + /// currently shipped - none of them sets USE_TYPO_METRICS, so the generator falls back + /// to usWinAscent and usWinDescent, which span the full line box on their own. Carried + /// anyway so a font that does set it needs no further format change. + /// + public float LineGap1em { get; set; } + + /// + /// Ascent share used for version 1 files, which store only the total. + /// + internal const float Version1AscentRatio = 0.8047f; + + /// + /// Fills Ascender1em, Descender1em and LineGap1em for a version 1 file by splitting the + /// line height. Keeping the approximation here means consumers do not need to know + /// which version the metrics came from. + /// + internal void ApproximateVerticalMetricsFromLineHeight() + { + Ascender1em = LineHeight1em * Version1AscentRatio; + Descender1em = LineHeight1em - Ascender1em; + LineGap1em = 0f; + } + + public FontMetricsClass DefaultWidthClass { get; set; } + + /// + /// Width of the default class, resolved once by Seal. + /// + public float DefaultWidth { get; private set; } + + /// + /// True once Seal has run and the metrics are ready for lookup. + /// + public bool IsSealed + { + get { return _latin != null; } + } + + #region Building + + internal void SetClassWidth(FontMetricsClass cls, float width) + { + var ix = (int)cls; + if (ix < 0 || ix >= MaxClasses) return; + _classWidths[ix] = width; + _classWidthSet[ix] = true; + } + + internal void AddRange(ushort start, ushort end, FontMetricsClass cls) + { + _buildRangeStart.Add(start); + _buildRangeEnd.Add(end); + _buildRangeClass.Add((byte)cls); + } + + internal void AddCharacter(ushort character, FontMetricsClass cls) + { + _buildSingleChar.Add(character); + _buildSingleClass.Add((byte)cls); + } + + /// + /// Sorts and freezes the mapping. Must be called before any lookup. + /// + internal void Seal() + { + if (IsSealed) return; + + DefaultWidth = GetClassWidth(DefaultWidthClass); + + SortByKey(_buildRangeStart, _buildRangeEnd, _buildRangeClass); + _rangeStart = _buildRangeStart.ToArray(); + _rangeEnd = _buildRangeEnd.ToArray(); + _rangeClass = _buildRangeClass.ToArray(); + + SortByKey(_buildSingleChar, null, _buildSingleClass); + _singleChar = _buildSingleChar.ToArray(); + _singleClass = _buildSingleClass.ToArray(); + + _buildRangeStart = null; + _buildRangeEnd = null; + _buildRangeClass = null; + _buildSingleChar = null; + _buildSingleClass = null; + + // Resolve the Latin-1 block once so the common path needs no search. Done after the + // arrays are built so it goes through the same lookup and cannot disagree with it. + _latin = new byte[LatinLast - LatinFirst + 1]; + for (var c = LatinFirst; c <= LatinLast; c++) + { + byte cls; + _latin[c - LatinFirst] = TrySearch((ushort)c, out cls) ? cls : NotMapped; + } + } + + /// + /// Insertion sort over the parallel lists, keyed on the first. The lists arrive close to + /// sorted from the file, and the alternative - building index arrays and permuting - + /// allocates more than the sort saves at these sizes. + /// + private static void SortByKey(List keys, List second, List classes) + { + for (var i = 1; i < keys.Count; i++) + { + var key = keys[i]; + var sec = second == null ? (ushort)0 : second[i]; + var cls = classes[i]; + var j = i - 1; + while (j >= 0 && keys[j] > key) + { + keys[j + 1] = keys[j]; + if (second != null) second[j + 1] = second[j]; + classes[j + 1] = classes[j]; + j--; + } + keys[j + 1] = key; + if (second != null) second[j + 1] = sec; + classes[j + 1] = cls; + } + } + + #endregion + + #region Lookup + + /// + /// Width of a class, or the default width when the class has none. + /// + public float GetClassWidth(FontMetricsClass cls) + { + var ix = (int)cls; + if (ix >= 0 && ix < MaxClasses && _classWidthSet[ix]) + { + return _classWidths[ix]; + } + return 0f; + } + + /// + /// True when the character has an explicit width class in this font. + /// + public bool ContainsCharacter(char c) + { + FontMetricsClass cls; + return TryGetClass(c, out cls); + } + + /// + /// Resolves the width class of a character. + /// + public bool TryGetClass(char c, out FontMetricsClass cls) + { + if (c >= LatinFirst && c <= LatinLast) + { + var mapped = _latin[c - LatinFirst]; + if (mapped == NotMapped) + { + cls = DefaultWidthClass; + return false; + } + cls = (FontMetricsClass)mapped; + return true; + } + + byte found; + if (TrySearch(c, out found)) + { + cls = (FontMetricsClass)found; + return true; + } + cls = DefaultWidthClass; + return false; + } + + /// + /// Width of a character, falling back to the default class width. This is the whole hot + /// path in one call - it replaces a lookup in CharMetrics followed by a second one in + /// ClassWidths. + /// + public float GetCharacterWidth(char c) + { + FontMetricsClass cls; + if (TryGetClass(c, out cls)) + { + return GetClassWidth(cls); + } + return DefaultWidth; + } + + private bool TrySearch(ushort c, out byte cls) + { + // Singles first: they outnumber the ranges roughly twenty to one. + if (_singleChar.Length > 0) + { + var lo = 0; + var hi = _singleChar.Length - 1; + while (lo <= hi) + { + var mid = lo + ((hi - lo) >> 1); + var v = _singleChar[mid]; + if (v == c) + { + cls = _singleClass[mid]; + return true; + } + if (v < c) lo = mid + 1; else hi = mid - 1; + } + } + + if (_rangeStart.Length > 0) + { + var lo = 0; + var hi = _rangeStart.Length - 1; + while (lo <= hi) + { + var mid = lo + ((hi - lo) >> 1); + if (_rangeStart[mid] > c) + { + hi = mid - 1; + } + else if (_rangeEnd[mid] < c) + { + lo = mid + 1; + } + else + { + cls = _rangeClass[mid]; + return true; + } + } + } + + cls = 0; + return false; + } + + #endregion + + public uint GetKey() + { + return GetKey(Family, SubFamily); + } + + public static uint GetKey(FontMetricsFamilies family, FontSubFamilies subFamily) + { + var k1 = (ushort)family; + var k2 = (ushort)subFamily; + return (uint)((k1 << 16) | ((k2) & 0xffff)); + } + } +} \ No newline at end of file diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/UnicodeRanges.cs b/src/EPPlus.Fonts.OpenType/GenericFontWidths/UnicodeRanges.cs similarity index 97% rename from src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/UnicodeRanges.cs rename to src/EPPlus.Fonts.OpenType/GenericFontWidths/UnicodeRanges.cs index 00ba76b908..680d210295 100644 --- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/UnicodeRanges.cs +++ b/src/EPPlus.Fonts.OpenType/GenericFontWidths/UnicodeRanges.cs @@ -15,7 +15,7 @@ Date Author Change using System.Linq; using System.Text; -namespace OfficeOpenXml.Core.Worksheet.Fonts.GenericFontMetrics +namespace EPPlus.Fonts.OpenType.GenericFontWidths { internal class UniCodeRange { diff --git a/src/EPPlus.Fonts.OpenType/IFontSource.cs b/src/EPPlus.Fonts.OpenType/IFontSource.cs new file mode 100644 index 0000000000..384832610a --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/IFontSource.cs @@ -0,0 +1,50 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 09/02/2026 EPPlus Software AB Extracted from OpenTypeFontEngine + *************************************************************************************************/ +using OfficeOpenXml.Interfaces.Fonts; + +namespace EPPlus.Fonts.OpenType.FontCache +{ + /// + /// The font-loading surface a font provider depends on: resolution, availability, and the + /// configured per-script fallback chains. Nothing else. + /// + /// It exists so does not depend on + /// . A glyph provider has no business reaching the engine's + /// shaper factory, and while the engine reference was there it could. + /// + internal interface IFontSource + { + /// + /// The configured fallback chain for a Unicode script, or null if none is configured. + /// An empty array means fallback is explicitly disabled for that script. + /// + string[] GetScriptFallback(UnicodeScript script); + + /// + /// Whether the exact family and subfamily exist, the family exists in another subfamily, + /// or neither. + /// + FontAvailability GetFontAvailability(string fontName, FontSubFamily subFamily); + + /// + /// Resolves, parses and caches a font. Returns null only when the resolver returns null, + /// which requires a custom . + /// + /// Deliberately has no ignoreCache parameter, unlike the store's own overload. A script + /// fallback chain is looked up for every code point in that script, so bypassing the + /// cache there would be pathological. Leaving the parameter off makes that unavailable + /// rather than merely discouraged. + /// + OpenTypeFont LoadFont(string fontName, FontSubFamily subFamily); + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs index df5e09b552..7eee36454a 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs @@ -195,7 +195,7 @@ void Shaping(bool shapeLight = true) foreach (var styleRun in StyleRuns) { var inputFrag = InputFragments[styleRun.FragmentIndex]; - var shaper = _engine.GetTextShaper(inputFrag.RichTextOptions.Family, inputFrag.RichTextOptions.SubFamily); + var shaper = _engine.GetMeasurementShaper(inputFrag.RichTextOptions.Family, inputFrag.RichTextOptions.SubFamily); if (shapeLight) { @@ -228,7 +228,7 @@ void Shaping(bool shapeLight = true) var lastFragment = InputFragments[InputFragments.Count - 1]; var lastRun = StyleRuns[StyleRuns.Count - 1]; - var lastShaper = _engine.GetTextShaper(lastFragment.RichTextOptions.Family, lastFragment.RichTextOptions.SubFamily); + var lastShaper = _engine.GetMeasurementShaper(lastFragment.RichTextOptions.Family, lastFragment.RichTextOptions.SubFamily); var lastShapedGlyphs = lastShaper.ShapeLight(lastRun.Text); double[] lastCharWidths = new double[lastRun.Length + 1]; lastShapedGlyphs.FillCharWidths((float)lastFragment.RichTextOptions.Size, lastCharWidths, lastRun.Length + 1); @@ -251,7 +251,7 @@ public TextLineCollection Wrap(double maxWidth) return new TextLineCollection(); } var inputRt = InputFragments[0]; - var shaper = _engine.GetTextShaper(inputRt.RichTextOptions.Family, inputRt.RichTextOptions.SubFamily); + var shaper = _engine.GetMeasurementShaper(inputRt.RichTextOptions.Family, inputRt.RichTextOptions.SubFamily); var layoutEngine = new TextLayoutEngine(_engine, shaper); var wrappedLines = layoutEngine.WrapRichTextRuns(StyleRuns, maxWidth); diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs index 5b0d08037d..4022c58ac6 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs @@ -52,9 +52,12 @@ public partial class TextLayoutEngine : IDisposable /// /// Creates a TextLayoutEngine for single-font text wrapping. - /// Font resolution for rich-text fragments uses the globally configured resolver — to - /// search additional directories or install a custom resolver, use - /// . + /// + /// This overload has no font engine, so it cannot resolve per-fragment fonts: rich-text + /// fragments are all measured with the shaper given here, whatever font they specify. For + /// multi-font rich text, use the constructor that takes an , + /// or obtain an engine-backed instance from + /// . /// public TextLayoutEngine(ITextShaper shaper) { diff --git a/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs b/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs index 7518114408..0cf3acc98b 100644 --- a/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs +++ b/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs @@ -9,6 +9,7 @@ This software is licensed under PolyForm Noncommercial License 1.0.0 Date Author Change ************************************************************************************************* 05/13/2026 EPPlus Software AB Per-instance font engine. Replaces static OpenTypeFonts. + 09/02/2026 EPPlus Software AB Extracted FontStore and ShaperCache; added measurement shaper *************************************************************************************************/ using EPPlus.Fonts.OpenType.FontCache; using EPPlus.Fonts.OpenType.FontResolver; @@ -26,33 +27,21 @@ Date Author Change namespace EPPlus.Fonts.OpenType { /// - /// An OpenType font engine instance. Owns its own configuration, resolver, and font cache — - /// two engines do not share parsed fonts and can have different configurations simultaneously. - /// Scanner-level data (file system listings, per-file FontFaceInfo) is shared globally - /// because that data describes the filesystem and is identical regardless of engine. + /// An OpenType font engine instance. Produces shapers and layout engines, and owns the + /// configuration, the font store and the shaper cache for its own lifetime. Two engines do + /// not share parsed fonts and can have different configurations simultaneously. + /// /// Configuration is set at construction time and is immutable for the lifetime of the engine. /// To use a different configuration, create a new engine. + /// + /// The engine holds the policy for which kind of shaper a request gets; the store and the + /// cache hold no policy at all. /// public class OpenTypeFontEngine : IDisposable { - private readonly object _syncRoot = new object(); - private readonly Dictionary _fontLocks = new Dictionary(); - - // Active resolver. Set at construction; never replaced. - private readonly IFontResolver _fontResolver; - - // Configuration snapshot. Held to support GetFontAvailability and similar queries. private readonly EpplusFontConfiguration _configuration; - - // Per-engine cache of parsed fonts. - private readonly OpenTypeFontCache _fontCache = new OpenTypeFontCache(); - - // Thread-local TextShaper cache. Each thread gets its own dictionary, keyed by the engine - // instance to avoid collisions if multiple engines are used on the same thread. - // We use a ThreadStatic Dictionary> - // so each engine has its own per-thread shaper namespace. - [ThreadStatic] - private static Dictionary> _threadLocalShaperCaches; + private readonly FontStore _fontStore; + private readonly ShaperCache _shaperCache = new ShaperCache(); private bool _disposed; @@ -89,18 +78,16 @@ public OpenTypeFontEngine(Action configure) // If the user installed a custom resolver, use it as-is. Otherwise build a // DefaultFontResolver from the configuration. - var userResolver = _configuration.FontResolver; - if (userResolver != null) - { - _fontResolver = userResolver; - } - else + var resolver = _configuration.FontResolver; + if (resolver == null) { - _fontResolver = new DefaultFontResolver( + resolver = new DefaultFontResolver( fontDirectories: _configuration.FontDirectories, searchSystemDirectories: _configuration.SearchSystemDirectories, config: _configuration); } + + _fontStore = new FontStore(resolver, _configuration); } // ----------------------------------------------------------------------------------------- @@ -108,19 +95,41 @@ public OpenTypeFontEngine(Action configure) // ----------------------------------------------------------------------------------------- /// - /// When true, GetTextShaper throws if the requested font cannot be resolved to an exact match, - /// even though a fallback was found. Default is false: rendering trusts the fallback chain - /// (which always resolves to at least the embedded font) and never throws. Set to true only - /// for diagnostics or validation where a missing exact font should surface as an error. + /// When true, shaper resolution throws if the requested font cannot be resolved to an + /// exact match, even though a fallback was found. Default is false: rendering trusts the + /// fallback chain (which always resolves to at least the embedded font) and never throws. + /// Set to true only for diagnostics or validation where a missing exact font should + /// surface as an error. + /// + /// It also suppresses the metrics fallback on the measurement path, since silently + /// substituting serialized metrics would defeat the purpose of the mode. /// public bool RequireExactFont { get; set; } = false; - //public FontAvailability FallBackAvailablility = FontAvailability.Exact; /// - /// Gets a TextShaper for the given font, reusing a thread-local cached instance. - /// The underlying OpenTypeFont is shared within this engine (but not between engines), - /// while each thread gets its own TextShaper instance to avoid locking. + /// The font store backing this engine. Internal: it is how the engine hands an + /// to the providers it constructs, and how the public + /// constructor forwards to its internal one. + /// + internal FontStore FontStore + { + get { return _fontStore; } + } + + // ----------------------------------------------------------------------------------------- + // Shapers — this is the policy + // ----------------------------------------------------------------------------------------- + + /// + /// Gets a for the given font, reusing a thread-local cached + /// instance. The underlying OpenTypeFont is shared within this engine (but not between + /// engines), while each thread gets its own TextShaper to avoid locking. /// Returns null if the font cannot be resolved. + /// + /// The returned shaper is always backed by a real font file, so its output carries glyph + /// ids, glyph outlines and font references. Use this when the caller needs glyph data: + /// PDF export, subsetting, embedding. For measurement and line breaking use + /// instead. /// public TextShaper GetTextShaper(string fontName, FontSubFamily subFamily = FontSubFamily.Regular) { @@ -128,102 +137,103 @@ public TextShaper GetTextShaper(string fontName, FontSubFamily subFamily = FontS if (fontName == null) throw new ArgumentNullException("fontName"); - var perEngineMap = GetOrCreateThreadLocalShaperMap(); + return GetOrCreateRenderingShaper(FontStore.BuildCacheKey(fontName, subFamily), fontName, subFamily); + } - string key = BuildCacheKey(fontName, subFamily); + /// + /// Gets a shaper for measuring text and breaking lines. + /// + /// Unlike this may return a shaper that is not backed by a + /// font file. When the requested font cannot be resolved and font resolution has fallen + /// all the way through to the embedded last-resort font, the serialized font metrics + /// (.fmtr) for the requested family are used instead, when they exist. Measuring the + /// requested family from quantized metrics is closer to the truth than measuring a + /// different family exactly. + /// + /// The returned shaper may have no glyph ids, no kerning and no OpenType layout tables; + /// see . The narrower return type is deliberate — it + /// exposes no glyph-level API, so a glyph consumer cannot reach for this method by + /// accident. + /// + /// Returns null only when neither a font file nor serialized metrics can be found, which + /// requires a custom that returns null. + /// + public ITextShaper GetMeasurementShaper(string fontName, FontSubFamily subFamily = FontSubFamily.Regular) + { + ThrowIfDisposed(); + if (fontName == null) + throw new ArgumentNullException("fontName"); - TextShaper shaper; - if (!perEngineMap.TryGetValue(key, out shaper)) + var key = FontStore.BuildCacheKey(fontName, subFamily); + + ITextShaper cached; + if (_shaperCache.TryGetMeasurement(key, out cached)) { - var font = LoadFont(fontName, subFamily); - if (font == null) - return null; + return cached; + } - if (RequireExactFont) - { - var availability = GetFontAvailability(fontName, subFamily); - if (availability != FontAvailability.Exact) - { - throw new FileNotFoundException( - $"Could not find Font: {fontName} {subFamily}. Resolved via fallback to: {font.GetEnglishFontFamilyName()} {font.SubFamily}."); - } - } + var shaper = CreateMeasurementShaper(key, fontName, subFamily); - shaper = new TextShaper(this, font); - perEngineMap[key] = shaper; + // A null result is not cached. It only happens with a custom resolver that gives up, + // and caching it would make a later change in resolver state unobservable. + if (shaper != null) + { + _shaperCache.AddMeasurement(key, shaper); } return shaper; } - public TextLayoutEngine GetTextLayoutEngine(string fontName, FontSubFamily subFamily = FontSubFamily.Regular) - { - var shaper = GetTextShaper(fontName, subFamily); - return new TextLayoutEngine(this, shaper); - } - public TextLayoutEngine GetTextLayoutEngineForFont(IFontFormatBase font) - { - var shaper = GetShaperForFont(font); - return new TextLayoutEngine(this, shaper); - } - + /// + /// Gets a shaper for measuring text in the given font. May return a metrics-only shaper; + /// see . + /// public ITextShaper GetShaperForFont(IFontFormatBase font) { - return GetTextShaper(font.Family, font.SubFamily); - } + if (font == null) + throw new ArgumentNullException("font"); - public TextLayoutEngine GetTextLayoutEngineForFont(MeasurementFont font) - { - var shaper = GetShaperForFont(font); - return new TextLayoutEngine(this, shaper); + return GetMeasurementShaper(font.Family, font.SubFamily); } + /// + /// Gets a shaper for measuring text in the given font. May return a metrics-only shaper; + /// see . + /// public ITextShaper GetShaperForFont(MeasurementFont font) { - return GetTextShaper(font.FontFamily, GetFontSubFamily(font.Style)); + if (font == null) + throw new ArgumentNullException("font"); + + return GetMeasurementShaper(font.FontFamily, FontSubFamilyConverter.ToSubFamily(font.Style)); } - public static FontSubFamily GetFontSubFamily(MeasurementFontStyles style) - { - if ((style & (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic)) == - (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic)) - { - return FontSubFamily.BoldItalic; - } - else if ((style & MeasurementFontStyles.Bold) == MeasurementFontStyles.Bold) - { - return FontSubFamily.Bold; - } - else if ((style & MeasurementFontStyles.Italic) == MeasurementFontStyles.Italic) - { - return FontSubFamily.Italic; - } + // ----------------------------------------------------------------------------------------- + // Layout engines + // + // A TextLayoutEngine only ever measures, so all three of these take the measurement path. + // ----------------------------------------------------------------------------------------- - return FontSubFamily.Regular; + public TextLayoutEngine GetTextLayoutEngine(string fontName, FontSubFamily subFamily = FontSubFamily.Regular) + { + var shaper = GetMeasurementShaper(fontName, subFamily); + return new TextLayoutEngine(this, shaper); } - /// - /// Clears this engine's parsed-font cache, per-font locks, and the calling thread's - /// TextShaper cache for this engine. Does not affect scanner-level caches (which are - /// global and reflect filesystem state, not engine configuration). - /// - public void ClearFontCache() + public TextLayoutEngine GetTextLayoutEngineForFont(IFontFormatBase font) { - ThrowIfDisposed(); - lock (_syncRoot) - { - _fontCache.Clear(); - _fontLocks.Clear(); - } + var shaper = GetShaperForFont(font); + return new TextLayoutEngine(this, shaper); + } - // Clear this engine's shaper cache for the calling thread. - // Other threads' caches will be lazily rebuilt on next use. - if (_threadLocalShaperCaches != null) - _threadLocalShaperCaches.Remove(this); + public TextLayoutEngine GetTextLayoutEngineForFont(MeasurementFont font) + { + var shaper = GetShaperForFont(font); + return new TextLayoutEngine(this, shaper); } // ----------------------------------------------------------------------------------------- - // Font loading + // Font queries — delegating to the store // ----------------------------------------------------------------------------------------- /// @@ -236,106 +246,25 @@ public OpenTypeFont LoadFont( bool ignoreCache = false) { ThrowIfDisposed(); - - if (ignoreCache) - return ResolveAndCreate(_fontResolver, fontName, subFamily); - - string lockKey = BuildCacheKey(fontName, subFamily); - object fontLock; - lock (_syncRoot) - { - if (!_fontLocks.TryGetValue(lockKey, out fontLock)) - { - fontLock = new object(); - _fontLocks[lockKey] = fontLock; - } - } - - lock (fontLock) - { - var cached = _fontCache.GetFromCache(lockKey); - if (cached != null && cached.Font != null && cached.IsLoaded) - { - cached.Font.EnsureFullyLoaded(); - return cached.Font; - } - - _fontCache.BeginCache(lockKey); - - var font = ResolveAndCreate(_fontResolver, fontName, subFamily); - if (font == null) - return null; - - font.EnsureFullyLoaded(); - font.IsReadOnly = true; - _fontCache.AddToCache(font, lockKey); - return font; - } + return _fontStore.LoadFont(fontName, subFamily, ignoreCache); } /// - /// Returns all available font faces as fully loaded OpenTypeFont instances. - /// Skips corrupt or unreadable fonts, but logs detailed information for diagnostics. - /// This method is NOT cached and may take significant time to complete. - /// Note: takes fontDirectories as a parameter; this is a diagnostic / discovery API - /// independent of the engine's configured resolver. + /// Checks whether a font is available in this engine's configured font system. + /// See for the accuracy caveats. /// - public List GetAllBaseFontData( - List fontDirectories, - bool searchSystemDirectories = true, - FontFormat? formatTarget = null) + public FontAvailability GetFontAvailability( + string fontName, + FontSubFamily subFamily = FontSubFamily.Regular) { ThrowIfDisposed(); - - var locations = DefaultFontLocations.GetLocationsCollection(fontDirectories, searchSystemDirectories); - var faces = FontScannerV2.EnumerateAllFaces(locations); - - var result = new List(faces.Count); - var failures = 0; - - foreach (var face in faces) - { - if (formatTarget.HasValue) - { - string ext = Path.GetExtension(face.FilePath); - if (!string.IsNullOrEmpty(ext)) - { - ext = ext.ToLowerInvariant(); - var format = (ext == ".otf" || ext == ".cff") - ? FontFormat.Otf - : FontFormat.Ttf; - - if (format != formatTarget.Value) - continue; - } - } - - try - { - var font = new OpenTypeFont(File.ReadAllBytes(face.FilePath)); - font.EnsureFullyLoaded(); - result.Add(font); - } - catch (Exception ex) - { - failures++; - System.Diagnostics.Debug.WriteLine( - string.Format("[OpenTypeFontEngine] Failed to load font: {0} => {1}: {2}", - face.FilePath, ex.GetType().Name, ex.Message)); - } - } - - if (failures > 0) - System.Diagnostics.Debug.WriteLine( - string.Format("[OpenTypeFontEngine] {0} font(s) failed to load.", failures)); - - return result; + return _fontStore.GetFontAvailability(fontName, subFamily); } /// /// Creates an OpenTypeFont directly from raw font bytes. /// Font format (TTF/OTF) is detected automatically from the SFNT header. - /// Independent of engine configuration — does not consult the resolver or cache. + /// Independent of engine configuration — consults neither the resolver nor the cache. /// public OpenTypeFont GetFromBytes(byte[] bytes) { @@ -347,32 +276,46 @@ public OpenTypeFont GetFromBytes(byte[] bytes) return font; } + + // ----------------------------------------------------------------------------------------- + // Lifecycle + // ----------------------------------------------------------------------------------------- + /// - /// Checks whether a font is available in this engine's configured font system. - /// Returns if the exact family and subfamily exist, - /// if the family exists but not in the requested - /// subfamily, and otherwise. - /// - /// If the engine has a custom resolver that implements , - /// the call delegates to the resolver. Otherwise it probes via , - /// which can only distinguish "found" from "not found" — never . + /// Clears this engine's parsed-font cache, per-font locks, and the calling thread's + /// shaper cache for this engine. Does not affect scanner-level caches, which are global + /// and reflect filesystem state rather than engine configuration. /// - public FontAvailability GetFontAvailability( - string fontName, - FontSubFamily subFamily = FontSubFamily.Regular) + public void ClearFontCache() { ThrowIfDisposed(); - if (fontName == null) - throw new ArgumentNullException("fontName"); - var provider = _fontResolver as IFontAvailabilityProvider; - if (provider != null) - return provider.GetFontAvailability(fontName, subFamily); + // Shapers first. Each retained shaper holds a reference to a parsed font, so + // clearing the fonts first would leave live shapers pointing at fonts that are no + // longer in the cache and would be re-parsed on the next miss. + _shaperCache.ClearCurrentThread(); + _fontStore.Clear(); + } - // Fallback: probe via ResolveFont. Cannot distinguish FamilyOnly. - return _fontResolver.ResolveFont(fontName, subFamily) != null - ? FontAvailability.Exact - : FontAvailability.NotFound; + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + // Best-effort cleanup of this engine's shaper entries on the disposing thread. + // Entries on other threads are dropped when those threads next touch their map, + // since they will find no entry for this cache and rebuild. + _shaperCache.ClearCurrentThread(); + + // Marks the store disposed as well as clearing it, so a DefaultFontProvider that + // holds the store directly cannot keep loading fonts after this point. + _fontStore.MarkDisposed(); + } + + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException("OpenTypeFontEngine"); } internal FontEmbeddingDecision ResolveEmbeddingDecision(OpenTypeFont font) @@ -414,80 +357,128 @@ internal FontEmbeddingDecision ResolveEmbeddingDecision(OpenTypeFont font) } // ----------------------------------------------------------------------------------------- - // Internal helpers + // Shaper resolution — policy // ----------------------------------------------------------------------------------------- - /// - /// 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) + private TextShaper GetOrCreateRenderingShaper(string key, string fontName, FontSubFamily subFamily) { - return _configuration.GetScriptFallback(script); - } - - internal static string BuildCacheKey(string fontName, FontSubFamily subFamily) - { - return string.Format("{0}_{1}", fontName, subFamily); - } + TextShaper shaper; + if (_shaperCache.TryGetRendering(key, out shaper)) + { + return shaper; + } - private static OpenTypeFont ResolveAndCreate(IFontResolver resolver, string fontName, FontSubFamily subFamily) - { - var bytes = resolver.ResolveFont(fontName, subFamily); - if (bytes == null) + var font = _fontStore.LoadFont(fontName, subFamily, false); + if (font == null) return null; - return new OpenTypeFont(bytes); + ThrowIfNotExactWhenRequired(fontName, subFamily, font); + + shaper = CreateShaper(font); + _shaperCache.AddRendering(key, shaper); + return shaper; } - private Dictionary GetOrCreateThreadLocalShaperMap() + private ITextShaper CreateMeasurementShaper(string key, string fontName, FontSubFamily subFamily) { - // [ThreadStatic] field initializers only run on the primary thread. - // All other threads see null and must initialize on first use. - if (_threadLocalShaperCaches == null) - _threadLocalShaperCaches = new Dictionary>(); + var fallbackMode = _configuration.MetricsFallback; - Dictionary map; - if (!_threadLocalShaperCaches.TryGetValue(this, out map)) + // Always: short-circuit before LoadFont so no font file is opened at all. This is + // what makes measurement reproducible across machines — the result cannot depend on + // what happens to be installed. RequireExactFont wins, since a diagnostic mode that + // wants a missing font to throw must not be silenced by a metrics substitution. + if (fallbackMode == MetricsFallbackMode.Always && !RequireExactFont) { - map = new Dictionary(); - _threadLocalShaperCaches[this] = map; + GenericFontTextShaper alwaysShaper; + if (GenericFontTextShaper.TryCreate(fontName, FontSubFamilyConverter.ToStyles(subFamily), out alwaysShaper)) + { + return alwaysShaper; + } + // No metrics for this family. Fall through to normal resolution rather than + // failing — Always is a preference, not a constraint. } - return map; - } - internal static List GetLocationsCollection( - IEnumerable fontDirectories, - bool searchSystemDirectories) - { - return DefaultFontLocations.GetLocationsCollection(fontDirectories, searchSystemDirectories); + // If the rendering path already parsed this font on this thread, reuse that shaper. + // A real font is the better measurement source and the instance is identical. + TextShaper alreadyParsed; + if (_shaperCache.TryGetRendering(key, out alreadyParsed)) + { + return alreadyParsed; + } + + var font = _fontStore.LoadFont(fontName, subFamily, false); + + if (fallbackMode != MetricsFallbackMode.Disabled + && !RequireExactFont + && ResolvedToLastResort(fontName, font)) + { + GenericFontTextShaper metricsShaper; + if (GenericFontTextShaper.TryCreate(fontName, FontSubFamilyConverter.ToStyles(subFamily), out metricsShaper)) + { + return metricsShaper; + } + } + + if (font == null) + return null; + + ThrowIfNotExactWhenRequired(fontName, subFamily, font); + + var shaper = CreateShaper(font); + _shaperCache.AddRendering(key, shaper); + return shaper; } - // I OpenTypeFontEngine - private void ThrowIfDisposed() + /// + /// Builds the provider and shaper for a parsed font. The provider gets the store rather + /// than this engine, so nothing the engine creates holds a reference back to it. + /// + private TextShaper CreateShaper(OpenTypeFont font) { - if (_disposed) - throw new ObjectDisposedException("OpenTypeFontEngine"); + return new TextShaper(new DefaultFontProvider(_fontStore, font)); } - public void Dispose() + private void ThrowIfNotExactWhenRequired(string fontName, FontSubFamily subFamily, OpenTypeFont font) { - if (_disposed) return; - _disposed = true; + if (!RequireExactFont) + return; - lock (_syncRoot) + var availability = _fontStore.GetFontAvailability(fontName, subFamily); + if (availability != FontAvailability.Exact) { - _fontCache.Clear(); - _fontLocks.Clear(); + throw new FileNotFoundException( + $"Could not find Font: {fontName} {subFamily}. Resolved via fallback to: {font.GetEnglishFontFamilyName()} {font.SubFamily}."); } + } - // Best-effort cleanup of this engine's shaper map on the disposing thread. - // Maps on other threads will be cleaned up when those threads next touch their map, - // since they will see no entry for this engine and rebuild fresh. The lingering - // entries are small (empty dictionaries) and held only by the disposing-time references. - if (_threadLocalShaperCaches != null) - _threadLocalShaperCaches.Remove(this); + /// + /// True when font resolution produced the embedded last-resort font for a request that did + /// not ask for it, or produced nothing at all. + /// + /// Deliberately not expressed as GetFontAvailability returning NotFound. That reports only + /// on the requested family and knows nothing about the user-configured and built-in + /// fallback chains in DefaultFontResolver, so it reports NotFound even when a + /// metric-compatible substitute was found and used. A real substitute font is a better + /// measurement source than quantized metrics, so the metrics fallback must engage only + /// after those chains are exhausted — that is, at the point resolution gives up and loads + /// the embedded font. + /// + private static bool ResolvedToLastResort(string requestedFontName, OpenTypeFont resolvedFont) + { + // The caller asked for the last-resort font itself and got it. Not a fallback. + if (IsLastResortFamily(requestedFontName)) + return false; + + // A custom IFontResolver returned null. Nothing was resolved at all. + if (resolvedFont == null) + return true; + + return IsLastResortFamily(resolvedFont.GetEnglishFontFamilyName()); + } + + private static bool IsLastResortFamily(string fontName) + { + return string.Equals("archivo narrow", fontName, StringComparison.OrdinalIgnoreCase); } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs b/src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs deleted file mode 100644 index 4a74778c1d..0000000000 --- a/src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs +++ /dev/null @@ -1,176 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 - 01/10/2026 EPPlus Software AB Fix threading issue with global lock - 01/23/2026 EPPlus Software AB Improved thread-safety with per-font locking - 02/26/2026 EPPlus Software AB Moved caching from DefaultFontResolver to here - 02/27/2026 EPPlus Software AB Replaced Configure overloads with IEpplusFontConfiguration - 03/20/2026 EPPlus Software AB Added thread-local TextShaper cache - 05/06/2026 EPPlus Software AB Transactional Configure; single resolver, single cache key - 05/13/2026 EPPlus Software AB Reduced to a thin facade over a singleton OpenTypeFontEngine - *************************************************************************************************/ -using EPPlus.Fonts.OpenType.Integration; -using EPPlus.Fonts.OpenType.Scanner; -using EPPlus.Fonts.OpenType.TextShaping; -using OfficeOpenXml.Interfaces.Drawing.Text; -using OfficeOpenXml.Interfaces.Fonts; -using OfficeOpenXml.Interfaces.RichText; -using System; -using System.Collections.Generic; - -namespace EPPlus.Fonts.OpenType -{ - /// - /// Static facade for the OpenType font system. Delegates to a singleton - /// instance for backward compatibility with - /// callers that have not yet been migrated to per-instance engine usage. - /// - /// New code should prefer creating and owning an OpenTypeFontEngine directly. - /// - public static class OpenTypeFonts - { - private static readonly object _syncRoot = new object(); - private static OpenTypeFontEngine _default = new OpenTypeFontEngine(); - - // ----------------------------------------------------------------------------------------- - // Configuration (mutates the singleton engine) - // ----------------------------------------------------------------------------------------- - - /// - /// Reconfigures the singleton font engine used by this static facade. - /// Internally creates a new with the supplied - /// configuration and replaces the previous singleton. The previous engine is disposed, - /// invalidating any caches built against it. - /// - /// This method exists for source compatibility with callers that have not yet been - /// migrated to per-instance engine usage. New code should create its own - /// instead. - /// - public static void Configure(Action configure) - { - if (configure == null) - throw new ArgumentNullException("configure"); - - OpenTypeFontEngine oldEngine; - lock (_syncRoot) - { - oldEngine = _default; - _default = new OpenTypeFontEngine(configure); - } - - // Dispose the old engine outside the lock. Any caller still holding a reference - // to it (e.g. via GetTextShaper) will get an ObjectDisposedException on next use, - // which is the intended signal that configuration changed underneath them. - try { oldEngine.Dispose(); } catch { /* swallow - best effort */ } - } - - // ----------------------------------------------------------------------------------------- - // Delegating API - // ----------------------------------------------------------------------------------------- - - public static TextShaper GetTextShaper(string fontName, FontSubFamily subFamily = FontSubFamily.Regular) - { - return _default.GetTextShaper(fontName, subFamily); - } - - public static TextLayoutEngine GetTextLayoutEngine(string fontName, FontSubFamily subFamily = FontSubFamily.Regular) - { - return _default.GetTextLayoutEngine(fontName, subFamily); - } - - - public static TextLayoutEngine GetTextLayoutEngineForFont(IFontFormatBase font) - { - return _default.GetTextLayoutEngineForFont(font); - } - - public static TextLayoutEngine GetTextLayoutEngineForFont(MeasurementFont font) - { - return _default.GetTextLayoutEngineForFont(font); - } - - public static ITextShaper GetShaperForFont(MeasurementFont font) - { - return _default.GetShaperForFont(font); - } - - public static ITextShaper GetShaperForFont(IFontFormatBase font) - { - return _default.GetShaperForFont(font); - } - - public static FontSubFamily GetFontSubFamily(MeasurementFontStyles style) - { - return OpenTypeFontEngine.GetFontSubFamily(style); - } - - public static void ClearFontCache() - { - _default.ClearFontCache(); - } - - public static OpenTypeFont LoadFont( - string fontName, - FontSubFamily subFamily = FontSubFamily.Regular, - bool ignoreCache = false) - { - return _default.LoadFont(fontName, subFamily, ignoreCache); - } - - /// - /// Overload preserved for source compatibility with callers that pass fontDirectories. - /// The directories argument is IGNORED - to add directories permanently, configure the - /// engine via or create your own . - /// - [Obsolete("Pass font directories through OpenTypeFonts.Configure(cfg => cfg.FontDirectories.Add(...)) or use OpenTypeFontEngine directly. The directories argument is ignored.", false)] - public static OpenTypeFont LoadFont( - string fontName, - FontSubFamily subFamily, - IEnumerable fontDirectories, - bool searchSystemDirectories = true, - bool ignoreCache = false) - { - return _default.LoadFont(fontName, subFamily, ignoreCache); - } - - public static List GetAllBaseFontData( - List fontDirectories, - bool searchSystemDirectories = true, - FontFormat? formatTarget = null) - { - return _default.GetAllBaseFontData(fontDirectories, searchSystemDirectories, formatTarget); - } - - public static OpenTypeFont GetFromBytes(byte[] bytes) - { - return _default.GetFromBytes(bytes); - } - - public static FontAvailability GetFontAvailability( - string fontName, - FontSubFamily subFamily = FontSubFamily.Regular) - { - return _default.GetFontAvailability(fontName, subFamily); - } - - internal static string BuildCacheKey(string fontName, FontSubFamily subFamily) - { - return OpenTypeFontEngine.BuildCacheKey(fontName, subFamily); - } - - internal static List GetLocationsCollection( - IEnumerable fontDirectories, - bool searchSystemDirectories) - { - return OpenTypeFontEngine.GetLocationsCollection(fontDirectories, searchSystemDirectories); - } - } -} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetric_old2s.zip b/src/EPPlus.Fonts.OpenType/Resources/TextMetric_old2s.zip new file mode 100644 index 0000000000..43c9c65e66 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetric_old2s.zip differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics.zip b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics.zip new file mode 100644 index 0000000000..86515c1cc8 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics.zip differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/0.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/0.fmtr new file mode 100644 index 0000000000..e392211472 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/0.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1.fmtr new file mode 100644 index 0000000000..aff0d972c2 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1048576.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1048576.fmtr new file mode 100644 index 0000000000..d1ff789559 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1048576.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114112.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114112.fmtr new file mode 100644 index 0000000000..56eaf74521 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114112.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114113.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114113.fmtr new file mode 100644 index 0000000000..fc91661919 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114113.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114114.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114114.fmtr new file mode 100644 index 0000000000..0ea22635ff Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114114.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114115.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114115.fmtr new file mode 100644 index 0000000000..4fe4c64b42 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1114115.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179648.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179648.fmtr new file mode 100644 index 0000000000..f5695b853c Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179648.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179649.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179649.fmtr new file mode 100644 index 0000000000..f35d180bc4 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179649.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179650.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179650.fmtr new file mode 100644 index 0000000000..6e2a7638b0 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179650.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179651.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179651.fmtr new file mode 100644 index 0000000000..8535c7f7e5 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1179651.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1245184.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1245184.fmtr new file mode 100644 index 0000000000..751e695aad Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1245184.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1245185.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1245185.fmtr new file mode 100644 index 0000000000..9b49d11d66 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1245185.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131072.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131072.fmtr new file mode 100644 index 0000000000..3cd7822f14 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131072.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310720.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310720.fmtr new file mode 100644 index 0000000000..6a76ec3ca8 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310720.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310721.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310721.fmtr new file mode 100644 index 0000000000..7f0305fd2f Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310721.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310722.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310722.fmtr new file mode 100644 index 0000000000..4101e80c83 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310722.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310723.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310723.fmtr new file mode 100644 index 0000000000..2745ce7064 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1310723.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131073.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131073.fmtr new file mode 100644 index 0000000000..6b179e1216 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131073.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131074.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131074.fmtr new file mode 100644 index 0000000000..7713af7297 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131074.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131075.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131075.fmtr new file mode 100644 index 0000000000..d16e41344a Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/131075.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376256.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376256.fmtr new file mode 100644 index 0000000000..cb44f88dc2 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376256.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376257.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376257.fmtr new file mode 100644 index 0000000000..cb60249559 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376257.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376258.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376258.fmtr new file mode 100644 index 0000000000..82bb52c575 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376258.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376259.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376259.fmtr new file mode 100644 index 0000000000..9765a5628e Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1376259.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441792.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441792.fmtr new file mode 100644 index 0000000000..9456aa8627 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441792.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441793.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441793.fmtr new file mode 100644 index 0000000000..c88a6315db Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441793.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441794.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441794.fmtr new file mode 100644 index 0000000000..ee83d59904 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441794.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441795.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441795.fmtr new file mode 100644 index 0000000000..9d0e72de17 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1441795.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1507328.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1507328.fmtr new file mode 100644 index 0000000000..0f93acfb67 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1507328.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1507329.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1507329.fmtr new file mode 100644 index 0000000000..90ef467ed0 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1507329.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572864.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572864.fmtr new file mode 100644 index 0000000000..1f47edf3a2 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572864.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572865.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572865.fmtr new file mode 100644 index 0000000000..90c973f31b Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572865.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572866.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572866.fmtr new file mode 100644 index 0000000000..afa6e978b7 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572866.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572867.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572867.fmtr new file mode 100644 index 0000000000..1192f66681 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1572867.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638400.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638400.fmtr new file mode 100644 index 0000000000..c1413a4f63 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638400.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638401.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638401.fmtr new file mode 100644 index 0000000000..89743b351e Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638401.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638402.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638402.fmtr new file mode 100644 index 0000000000..f43ca8b157 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638402.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638403.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638403.fmtr new file mode 100644 index 0000000000..035f5b36b0 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1638403.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703936.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703936.fmtr new file mode 100644 index 0000000000..fe103b8a8a Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703936.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703937.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703937.fmtr new file mode 100644 index 0000000000..ec629cbde1 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703937.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703938.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703938.fmtr new file mode 100644 index 0000000000..2ec6d8ebfe Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703938.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703939.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703939.fmtr new file mode 100644 index 0000000000..7193fb3f62 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1703939.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769472.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769472.fmtr new file mode 100644 index 0000000000..4f57df534d Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769472.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769473.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769473.fmtr new file mode 100644 index 0000000000..f34940780e Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769473.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769474.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769474.fmtr new file mode 100644 index 0000000000..f3463bcb43 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769474.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769475.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769475.fmtr new file mode 100644 index 0000000000..8d6c071a09 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1769475.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835008.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835008.fmtr new file mode 100644 index 0000000000..0e7648d6ea Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835008.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835009.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835009.fmtr new file mode 100644 index 0000000000..2c934ff0a4 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835009.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835010.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835010.fmtr new file mode 100644 index 0000000000..3042604cff Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835010.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835011.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835011.fmtr new file mode 100644 index 0000000000..7c8c6cc2f7 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1835011.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1900544.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1900544.fmtr new file mode 100644 index 0000000000..3fccefca64 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1900544.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1900545.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1900545.fmtr new file mode 100644 index 0000000000..09cbb915c8 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/1900545.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196608.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196608.fmtr new file mode 100644 index 0000000000..bd35c0a96f Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196608.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196609.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196609.fmtr new file mode 100644 index 0000000000..c75bd3cbff Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196609.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196610.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196610.fmtr new file mode 100644 index 0000000000..b9baa07cab Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196610.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196611.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196611.fmtr new file mode 100644 index 0000000000..e73d015a95 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/196611.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/2.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/2.fmtr new file mode 100644 index 0000000000..3af5c8155b Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/2.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262144.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262144.fmtr new file mode 100644 index 0000000000..6cd5108861 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262144.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262145.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262145.fmtr new file mode 100644 index 0000000000..98413a1877 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262145.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262146.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262146.fmtr new file mode 100644 index 0000000000..1bfd6a026f Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262146.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262147.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262147.fmtr new file mode 100644 index 0000000000..7afcda303d Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/262147.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/3.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/3.fmtr new file mode 100644 index 0000000000..6650a6de54 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/3.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/327680.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/327680.fmtr new file mode 100644 index 0000000000..181c5857e3 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/327680.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/327682.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/327682.fmtr new file mode 100644 index 0000000000..66937d4d72 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/327682.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393216.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393216.fmtr new file mode 100644 index 0000000000..48966c12d1 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393216.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393217.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393217.fmtr new file mode 100644 index 0000000000..a0d41780ea Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393217.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393218.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393218.fmtr new file mode 100644 index 0000000000..084b45fccc Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393218.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393219.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393219.fmtr new file mode 100644 index 0000000000..838a45e51d Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/393219.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458752.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458752.fmtr new file mode 100644 index 0000000000..9eba416d4a Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458752.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458753.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458753.fmtr new file mode 100644 index 0000000000..c44f36f907 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458753.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458754.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458754.fmtr new file mode 100644 index 0000000000..6032594879 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458754.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458755.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458755.fmtr new file mode 100644 index 0000000000..25ea2fa020 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/458755.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589824.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589824.fmtr new file mode 100644 index 0000000000..a31ecb9d69 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589824.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589825.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589825.fmtr new file mode 100644 index 0000000000..7a1fa865ee Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589825.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589826.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589826.fmtr new file mode 100644 index 0000000000..c29731c5a5 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589826.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589827.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589827.fmtr new file mode 100644 index 0000000000..9bb86e9444 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/589827.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/65536.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/65536.fmtr new file mode 100644 index 0000000000..bb985c8c84 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/65536.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655360.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655360.fmtr new file mode 100644 index 0000000000..09ee5f8cff Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655360.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655361.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655361.fmtr new file mode 100644 index 0000000000..0d1b59a3dc Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655361.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655362.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655362.fmtr new file mode 100644 index 0000000000..da03576e91 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655362.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655363.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655363.fmtr new file mode 100644 index 0000000000..4e44d78518 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/655363.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720896.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720896.fmtr new file mode 100644 index 0000000000..de6778c973 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720896.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720897.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720897.fmtr new file mode 100644 index 0000000000..e7f8712020 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720897.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720898.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720898.fmtr new file mode 100644 index 0000000000..cd4a4a6c4c Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720898.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720899.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720899.fmtr new file mode 100644 index 0000000000..3484f02247 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/720899.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786432.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786432.fmtr new file mode 100644 index 0000000000..cf24646cfa Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786432.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786433.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786433.fmtr new file mode 100644 index 0000000000..66ed0cba27 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786433.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786434.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786434.fmtr new file mode 100644 index 0000000000..bc5b4bfb11 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786434.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786435.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786435.fmtr new file mode 100644 index 0000000000..e2a32334b0 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/786435.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/851968.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/851968.fmtr new file mode 100644 index 0000000000..7245b57e32 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/851968.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/851969.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/851969.fmtr new file mode 100644 index 0000000000..8fdf5981b4 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/851969.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/851970.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/851970.fmtr new file mode 100644 index 0000000000..1c5044bb70 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/851970.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917504.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917504.fmtr new file mode 100644 index 0000000000..a19fd16364 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917504.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917505.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917505.fmtr new file mode 100644 index 0000000000..85f39f0d3d Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917505.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917506.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917506.fmtr new file mode 100644 index 0000000000..aadfaba1a0 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917506.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917507.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917507.fmtr new file mode 100644 index 0000000000..649396cfe5 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/917507.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983040.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983040.fmtr new file mode 100644 index 0000000000..0ffd2b58ab Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983040.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983041.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983041.fmtr new file mode 100644 index 0000000000..f4c48ff656 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983041.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983042.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983042.fmtr new file mode 100644 index 0000000000..369e1a70ed Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983042.fmtr differ diff --git a/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983043.fmtr b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983043.fmtr new file mode 100644 index 0000000000..7f81900517 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics/983043.fmtr differ diff --git a/src/EPPlus/resources/TextMetrics.zip b/src/EPPlus.Fonts.OpenType/Resources/TextMetrics_old.zip similarity index 100% rename from src/EPPlus/resources/TextMetrics.zip rename to src/EPPlus.Fonts.OpenType/Resources/TextMetrics_old.zip diff --git a/src/EPPlus/resources/fontsize.zip b/src/EPPlus.Fonts.OpenType/Resources/fontsize.zip similarity index 100% rename from src/EPPlus/resources/fontsize.zip rename to src/EPPlus.Fonts.OpenType/Resources/fontsize.zip diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/GenericFontTextShaper.cs b/src/EPPlus.Fonts.OpenType/TextShaping/GenericFontTextShaper.cs new file mode 100644 index 0000000000..7d5a265a6a --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/TextShaping/GenericFontTextShaper.cs @@ -0,0 +1,522 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 08/31/2026 EPPlus Software AB Initial implementation + *************************************************************************************************/ +using EPPlus.Fonts.OpenType.GenericFontWidths; +using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; +using System; +using System.Collections.Generic; + +namespace EPPlus.Fonts.OpenType.TextShaping +{ + /// + /// An implementation backed by the serialized font metrics + /// (.fmtr files in Resources/TextMetrics.zip) instead of a real font file. + /// + /// This shaper requires no access to fonts installed on the system, which makes it + /// usable as a fallback for text measurement and line wrapping when the actual font + /// cannot be loaded. It is NOT a substitute for the OpenType based + /// when real shaping is required (PDF export), because the + /// underlying data contains no glyph outlines, no glyph ids and no OpenType layout + /// tables. + /// + /// Deliberate limitations, all of them consequences of the .fmtr format: + /// + /// No GSUB/GPOS. Ligatures, kerning, contextual alternates and mark positioning + /// are not applied. is accepted but ignored. + /// No glyph ids. carries the character code so + /// that shaped output remains diagnosable; it must not be used for subsetting or + /// embedding. + /// No font fallback. All glyphs report FontId 0. + /// Character advances are quantized into width classes (16 or 32 depending on the + /// font), so per character advances carry an error of up to half a class width. See + /// the remarks on . + /// Only the Basic Multilingual Plane is covered. The metrics are keyed on + /// , so characters above U+FFFF get the default width class. + /// + /// + /// The scale factors in are intentionally NOT applied. + /// Those exist to align AutoFitColumns with the Excel GUI and are not font metrics. + /// The same reasoning excludes the digit and East Asian scaling factors used by + /// GenericFontMetricsTextMeasurerBase. This shaper reports the metrics as they are. + /// + internal class GenericFontTextShaper : ITextShaper + { + /// + /// Units per em for the virtual font this shaper represents. The .fmtr data has no + /// units per em of its own, so a value is chosen here and all advances are expressed + /// relative to it. 1000 matches the fallback used by . + /// + private const ushort GENERIC_UNITS_PER_EM = 1000; + + /// + /// Widths in the .fmtr files are stored as pixels per point of font size, i.e. the + /// em value already multiplied by 96/72 by the font-labs exporter. This constant + /// reverses that so widths can be converted to design units. + /// + private const float PixelsPerPointToEm = 72f / 96f; + + private readonly SerializedFontMetrics _metrics; + private readonly uint _fontKey; + private readonly ushort _defaultAdvance; + private readonly ushort _lineHeightDesignUnits; + + /// + /// Creates a shaper for the supplied metrics. + /// + internal GenericFontTextShaper(SerializedFontMetrics metrics) + { + if (metrics == null) + { + throw new ArgumentNullException("metrics"); + } + + _metrics = metrics; + _fontKey = metrics.GetKey(); + + _defaultAdvance = ToDesignUnits(_metrics.DefaultWidth); + _lineHeightDesignUnits = ToDesignUnits(_metrics.LineHeight1em); + } + + /// + /// Attempts to create a shaper for a font family and style. Returns false when the + /// family has no serialized metrics, in which case the caller should fall back. + /// + internal static bool TryCreate(string fontFamily, MeasurementFontStyles style, out GenericFontTextShaper shaper) + { + shaper = null; + if (string.IsNullOrEmpty(fontFamily)) + { + return false; + } + + // ResolveKey rather than GetKey so a missing subfamily falls back to the family's + // Regular, matching what the measurer does. + var fontKey = GenericTextMeasurerKey.ResolveKey(fontFamily, style); + if (fontKey == uint.MaxValue) + { + return false; + } + + var metrics = GenericFontMetricsCache.GetMetrics(fontKey); + if (metrics == null) + { + return false; + } + + shaper = new GenericFontTextShaper(metrics); + return true; + } + + /// + /// The font key (family and subfamily) these metrics were loaded for. + /// + internal uint FontKey + { + get { return _fontKey; } + } + + public ushort UnitsPerEm + { + get { return GENERIC_UNITS_PER_EM; } + } + + /// + public bool HasGlyphIds + { + get { return false; } + } + + #region Horizontal shaping + + public ShapedText Shape(string text, ShapingOptions options = null) + { + if (string.IsNullOrEmpty(text)) + { + return new ShapedText + { + OriginalText = text ?? string.Empty, + Glyphs = new ShapedGlyph[0], + FontUnitsPerEm = new ushort[] { GENERIC_UNITS_PER_EM }, + FontLineHeights = new int[] { _lineHeightDesignUnits } + }; + } + + var glyphs = MapToGlyphs(text); + + return new ShapedText + { + OriginalText = text, + Glyphs = glyphs.ToArray(), + FontUnitsPerEm = new ushort[] { GENERIC_UNITS_PER_EM }, + FontLineHeights = new int[] { _lineHeightDesignUnits } + }; + } + + public ShapedLightText ShapeLight(string text, ShapingOptions options = null) + { + if (string.IsNullOrEmpty(text)) + { + return new ShapedLightText + { + Glyphs = new GlyphWidth[0], + FontUnitsPerEm = new ushort[] { GENERIC_UNITS_PER_EM } + }; + } + + var glyphs = MapToGlyphs(text); + var result = new GlyphWidth[glyphs.Count]; + for (var i = 0; i < glyphs.Count; i++) + { + var g = glyphs[i]; + result[i] = new GlyphWidth + { + XAdvance = (ushort)g.XAdvance, + ClusterIndex = g.ClusterIndex, + CharCount = g.CharCount, + FontId = g.FontId + }; + } + + return new ShapedLightText + { + Glyphs = result, + FontUnitsPerEm = new ushort[] { GENERIC_UNITS_PER_EM } + }; + } + + public ShapedText[] ShapeLines(string text, ShapingOptions options = null) + { + if (string.IsNullOrEmpty(text)) + { + return new ShapedText[0]; + } + + var lines = text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + var result = new ShapedText[lines.Length]; + for (var i = 0; i < lines.Length; i++) + { + result[i] = Shape(lines[i], options); + } + return result; + } + + /// + /// Maps characters to advances. One glyph per character, except that a valid + /// surrogate pair produces a single glyph with CharCount 2. + /// + private List MapToGlyphs(string text) + { + var glyphs = new List(text.Length); + + var i = 0; + while (i < text.Length) + { + int charCount; + ushort advance; + ushort glyphId; + + if (i < text.Length - 1 && char.IsHighSurrogate(text[i]) && char.IsLowSurrogate(text[i + 1])) + { + // Supplementary plane. The metrics are keyed on char and cover the BMP + // only, so there is nothing better available than the default width. + charCount = 2; + advance = _defaultAdvance; + glyphId = 0; + } + else if (char.IsSurrogate(text[i])) + { + // Lone surrogate. Zero width, mirroring .notdef handling in TextShaper. + charCount = 1; + advance = 0; + glyphId = 0; + } + else + { + charCount = 1; + advance = GetAdvance(text[i]); + glyphId = text[i]; + } + + glyphs.Add(new ShapedGlyph + { + GlyphId = glyphId, + BaseAdvance = (short)advance, + XAdvance = (short)advance, + YAdvance = 0, + XOffset = 0, + YOffset = 0, + ClusterIndex = (ushort)i, + CharCount = (byte)charCount, + FontId = 0 + }); + + i += charCount; + } + + return glyphs; + } + + /// + /// Resolves the advance width of a single BMP character, in design units. + /// + private ushort GetAdvance(char c) + { + // Control characters carry no width. GenericFontMetricsTextMeasurerBase makes the + // same exclusion; note that this also covers CR and LF, so a caller that shapes + // multi-line text through Shape() rather than ShapeLines() gets zero width line + // breaks instead of default width ones. + if (char.IsControl(c)) + { + return 0; + } + + if (IsEastAsianChar(c)) + { + return GetEastAsianAdvance(c); + } + + return ToDesignUnits(_metrics.GetCharacterWidth(c)); + } + + /// + /// East Asian characters are full width (one em) regardless of the font, with the + /// half width Katakana block at half an em. + /// + /// Unlike GenericFontMetricsTextMeasurerBase this applies neither the 1.13 Kanji + /// scaling factor nor the 1.05 bold factor. Both are Excel GUI calibration rather + /// than font metrics and belong in the measurer, not here. + /// + private static ushort GetEastAsianAdvance(char c) + { + var cc = (int)c; + // U+FF61 - U+FF9F, half width Katakana and punctuation. + if (cc >= 0xFF61 && cc <= 0xFF9F) + { + return (ushort)(GENERIC_UNITS_PER_EM / 2); + } + return GENERIC_UNITS_PER_EM; + } + + /// + /// Returns true when the character falls inside one of the Japanese/Kanji ranges. + /// Uses an explicit loop rather than LINQ; this runs once per character and the + /// LINQ version in GenericFontMetricsTextMeasurerBase allocates an enumerator and + /// a closure on every call. + /// + private static bool IsEastAsianChar(char c) + { + var cc = (int)c; + + // Cheap rejection of Latin and most of the BMP below the CJK blocks. + if (cc < 0x2E80) + { + return false; + } + + foreach (var range in UniCodeRange.JapaneseKanji) + { + if (range.IsInRange(cc)) + { + return true; + } + } + return false; + } + + /// + /// Converts a width from the .fmtr format (pixels per point of font size) to design + /// units of the virtual font. + /// + /// Rounding to whole design units adds an error below 0.001 em, which is negligible + /// next to the quantization already present in the source data: a class width step + /// is 0.114 em for the 16 class fonts (Calibri, Arial) and 0.063 - 0.075 em for the + /// 32 class fonts (Aptos Narrow, Segoe UI, Tahoma). At 11pt that is 1.67px and + /// 0.92 - 1.10px respectively, so a single character advance can be off by half of + /// that. The errors partly cancel across a string when measuring, but a caller that + /// positions character by character accumulates them. + /// + private static ushort ToDesignUnits(float fmtrWidth) + { + if (fmtrWidth <= 0f) + { + return 0; + } + var designUnits = Math.Round(fmtrWidth * PixelsPerPointToEm * GENERIC_UNITS_PER_EM, + MidpointRounding.AwayFromZero); + if (designUnits > ushort.MaxValue) + { + return ushort.MaxValue; + } + return (ushort)designUnits; + } + + #endregion + + #region Vertical shaping + + /// + /// Shapes text for vertical layout. The .fmtr format has no vertical metrics, so the + /// horizontal advance is used as the advance height. This mirrors what + /// does for fonts without a vmtx table, but it gives poor + /// stacking for narrow Latin characters and should be revisited if vertical text is + /// actually routed through this shaper. + /// + public ShapedVerticalText ShapeVertical(string text, ShapingOptions options = null) + { + if (string.IsNullOrEmpty(text)) + { + return new ShapedVerticalText + { + OriginalText = text ?? string.Empty, + Glyphs = new VerticalShapedGlyph[0] + }; + } + + var horizontal = MapToGlyphs(text); + var glyphs = new VerticalShapedGlyph[horizontal.Count]; + for (var i = 0; i < horizontal.Count; i++) + { + var g = horizontal[i]; + var advance = (ushort)g.XAdvance; + glyphs[i] = new VerticalShapedGlyph( + g.GlyphId, + advance, // advanceHeight, no vertical metrics available + 0, // topSideBearing + advance, // advanceWidth, used for centering + g.ClusterIndex, + g.CharCount, + 0); + } + + return new ShapedVerticalText + { + OriginalText = text, + Glyphs = glyphs + }; + } + + public VerticalGlyphHeight[] ShapeLightVertical(string text, ShapingOptions options = null) + { + if (string.IsNullOrEmpty(text)) + { + return new VerticalGlyphHeight[0]; + } + + var horizontal = MapToGlyphs(text); + var result = new VerticalGlyphHeight[horizontal.Count]; + for (var i = 0; i < horizontal.Count; i++) + { + var g = horizontal[i]; + result[i] = new VerticalGlyphHeight + { + YAdvance = (ushort)g.XAdvance, + ClusterIndex = g.ClusterIndex, + CharCount = g.CharCount + }; + } + return result; + } + + #endregion + + #region Character widths + + public double[] ExtractCharWidths(string text, float fontSize, ShapingOptions options) + { + if (string.IsNullOrEmpty(text)) + { + return new double[text == null ? 0 : text.Length]; + } + + var charWidths = new double[text.Length]; + ExtractCharWidthsCore(text, fontSize, charWidths); + return charWidths; + } + + public void ExtractCharWidths(string text, float fontSize, ShapingOptions options, double[] targetArray) + { + if (string.IsNullOrEmpty(text)) + { + return; + } + + if (targetArray == null || targetArray.Length < text.Length) + { + throw new ArgumentException( + string.Format("Target array must be at least as large as text length ({0})", text.Length), + "targetArray"); + } + + ExtractCharWidthsCore(text, fontSize, targetArray); + } + + private void ExtractCharWidthsCore(string text, float fontSize, double[] targetArray) + { + Array.Clear(targetArray, 0, text.Length); + + var scaleFactor = fontSize / (double)GENERIC_UNITS_PER_EM; + var glyphs = MapToGlyphs(text); + + foreach (var glyph in glyphs) + { + int charIndex = glyph.ClusterIndex; + if (charIndex >= 0 && charIndex < text.Length) + { + targetArray[charIndex] += glyph.XAdvance * scaleFactor; + } + } + } + + #endregion + + #region Font metrics + + public float GetLineHeightInPoints(float fontSize) + { + return _metrics.LineHeight1em * PixelsPerPointToEm * fontSize; + } + + /// + /// Total font height, ascent plus descent, excluding the line gap. This is the same + /// distinction TextShaper makes: GetLineHeightInPoints includes the leading between + /// lines, this does not. + /// + public float GetFontHeightInPoints(float fontSize) + { + return (_metrics.Ascender1em + _metrics.Descender1em) * PixelsPerPointToEm * fontSize; + } + + /// + /// Distance from the top of the line box down to the baseline. + /// + /// Exact for version 2 metrics. For version 1 files the value is split out of the line + /// height by a fixed ratio in SerializedFontMetrics, which is out by up to 7% of the + /// font height for the extremes of the shipped library. + /// + public float GetAscentInPoints(float fontSize) + { + return _metrics.Ascender1em * PixelsPerPointToEm * fontSize; + } + + /// + /// Distance from the baseline down to the bottom of the line box. See + /// for the version 1 caveat. + /// + public float GetDescentInPoints(float fontSize) + { + return _metrics.Descender1em * PixelsPerPointToEm * fontSize; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs index 3af30c604e..654c9baf82 100644 --- a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs +++ b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs @@ -55,6 +55,12 @@ public ushort UnitsPerEm } } + /// + public bool HasGlyphIds + { + get { return true; } + } + /// /// Creates a TextShaper with automatic emoji fallback (DefaultFontProvider). /// NOTE: In most cases, prefer OpenTypeFonts.GetTextShaper() over creating @@ -91,30 +97,6 @@ public TextShaper(IFontProvider fontProvider) _chainingContextualProcessor = new ChainingContextualProcessor(_primaryFont, _singleSubstitutionProcessor, _ligatureProcessor); } - /// - /// Resolves a shaper for a different font via this shaper's font provider. Returns null - /// when the provider is not engine-backed (e.g. a custom IFontProvider), in which case the - /// caller is expected to fall back. This lets a TextLayoutEngine shape multi-font rich text - /// through the engine that produced this shaper instead of the global singleton. - /// - internal ITextShaper GetShaperForFont(IFontFormatBase font) - { - var dfp = _fontProvider as DefaultFontProvider; - if (dfp != null) - return dfp.GetShaperForFont(font); - - return null; - } - - internal ITextShaper GetShaperForFont(MeasurementFont font) - { - var dfp = _fontProvider as DefaultFontProvider; - if (dfp != null) - return dfp.GetShaperForFont(font); - - return null; - } - #region Font Tracking API /// diff --git a/src/EPPlus.Fonts.OpenType/fonts.publickey b/src/EPPlus.Fonts.OpenType/fonts.publickey new file mode 100644 index 0000000000..3b162786a6 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType/fonts.publickey differ diff --git a/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs b/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs index ef0980b2bc..f2d3f2b3cd 100644 --- a/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs +++ b/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs @@ -81,6 +81,17 @@ public interface IEpplusFontConfiguration /// Restores the default per-script glyph fallback chains. /// /// + + /// + /// Whether text measurement may fall back to serialized font metrics when the requested + /// font is not available as a font file. Defaults to + /// . + /// + /// Measurement and line breaking only. Rendering and font embedding always require a real + /// font file, so PDF export is unaffected by this setting. + /// + MetricsFallbackMode MetricsFallback { get; set; } + void Reset(); /// diff --git a/src/EPPlus.Interfaces/Fonts/ITextShaper.cs b/src/EPPlus.Interfaces/Fonts/ITextShaper.cs index c8933f6413..5b6746f22f 100644 --- a/src/EPPlus.Interfaces/Fonts/ITextShaper.cs +++ b/src/EPPlus.Interfaces/Fonts/ITextShaper.cs @@ -92,5 +92,16 @@ public interface ITextShaper /// Gets the font's units per em (for manual conversions). /// ushort UnitsPerEm { get; } + + /// + /// True when this shaper is backed by a real font file and its shaped output carries + /// usable glyph ids, glyph outlines and font references. + /// + /// False for metrics-only shapers, whose output is valid for measurement and line + /// breaking but carries no glyph identity. Consumers that subset, embed or otherwise + /// resolve glyphs must check this and refuse rather than emit glyph ids that do not + /// correspond to any font. + /// + bool HasGlyphIds { get; } } } \ No newline at end of file diff --git a/src/EPPlus.Interfaces/Fonts/MetricsFallbackMode.cs b/src/EPPlus.Interfaces/Fonts/MetricsFallbackMode.cs new file mode 100644 index 0000000000..0b4107432a --- /dev/null +++ b/src/EPPlus.Interfaces/Fonts/MetricsFallbackMode.cs @@ -0,0 +1,52 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 09/03/2026 EPPlus Software AB Metrics fallback configuration + *************************************************************************************************/ +namespace OfficeOpenXml.Interfaces.Fonts +{ + /// + /// Controls whether text measurement may fall back to serialized font metrics when the + /// requested font is not available as a font file. + /// + /// This affects measurement and line breaking only. Rendering and font embedding always + /// require a real font file and are unaffected by this setting — PDF export in particular + /// never uses serialized metrics, regardless of what is configured here. + /// + public enum MetricsFallbackMode + { + /// + /// Never use serialized metrics. Text in a font that cannot be resolved is measured with + /// the embedded last-resort font, which is condensed and therefore a poor width match for + /// most proportional fonts. + /// + Disabled = 0, + + /// + /// Default. Use serialized metrics when font resolution has fallen all the way through to + /// the embedded last-resort font and the requested family has metrics available. + /// + /// The user-configured and built-in fallback chains still take precedence: a real, + /// metric-compatible substitute font is a better measurement source than quantized + /// metrics, since it also carries kerning and OpenType layout tables. + /// + WhenFontMissing = 1, + + /// + /// Always measure from serialized metrics, ignoring installed fonts entirely. + /// + /// Measurement then does not depend on which fonts the machine has, so line breaking is + /// reproducible across machines and testable without a font fixture. The cost is that + /// kerning and OpenType substitutions are not applied and widths are quantized, so + /// measurements differ slightly from what a real font would give. + /// + Always = 2 + } +} \ No newline at end of file diff --git a/src/EPPlus/Core/AutofitHelper.cs b/src/EPPlus/Core/AutofitHelper.cs index 52a271741d..47b2d3d526 100644 --- a/src/EPPlus/Core/AutofitHelper.cs +++ b/src/EPPlus/Core/AutofitHelper.cs @@ -22,6 +22,7 @@ Date Author Change using OfficeOpenXml.Utils; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Fonts.OpenType; +using EPPlus.Fonts.OpenType.GenericFontWidths; namespace OfficeOpenXml.Core { diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/DefaultTextMeasurer.cs b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/DefaultTextMeasurer.cs index 3bf0fb8e54..b7809bd1b3 100644 --- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/DefaultTextMeasurer.cs +++ b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/DefaultTextMeasurer.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 12/26/2021 EPPlus Software AB EPPlus 6.0 *************************************************************************************************/ +using EPPlus.Fonts.OpenType.GenericFontWidths; using OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts; using OfficeOpenXml.Interfaces.Drawing.Text; using System; @@ -23,7 +24,7 @@ internal class DefaultTextMeasurer : GenericFontMetricsTextMeasurerBase { internal TextMeasurement Measure(string text, float size) { - var fontKey = GetKey(FontMetricsFamilies.Calibri, FontSubFamilies.Regular); + var fontKey = GenericTextMeasurerKey.GetKey(FontMetricsFamilies.Calibri, FontSubFamilies.Regular); return MeasureTextInternal(text, fontKey, MeasurementFontStyles.Regular, size); } } diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsLoader.cs b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsLoader.cs deleted file mode 100644 index d7b4c76528..0000000000 --- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsLoader.cs +++ /dev/null @@ -1,58 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 12/26/2021 EPPlus Software AB EPPlus 6.0 - *************************************************************************************************/ -using OfficeOpenXml.Packaging.Ionic.Zip; -using OfficeOpenXml.Utils; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts.GenericMeasurements -{ - /// - /// Loads serialized font metrics - /// - internal static class GenericFontMetricsLoader - { - /// - /// Loads all serialized font metrics from the resources/SerializedFonts.zip archive - /// - internal static Dictionary LoadFontMetrics() - { - var fonts = new Dictionary(); - var assembly = Assembly.GetExecutingAssembly(); - using (var stream = assembly.GetManifestResourceStream("OfficeOpenXml.resources.TextMetrics.zip")) - { - var zipStream = new ZipInputStream(stream); - ZipEntry entry; - while ((entry = zipStream.GetNextEntry()) != null) - { - if (!entry.IsDirectory && Path.GetExtension(entry.FileName) == ".fmtr") - { - var bytes = new byte[entry.UncompressedSize]; - var size = zipStream.Read(bytes, 0, (int)entry.UncompressedSize); - using (var ms = EPPlusMemoryManager.GetStream(bytes)) - { - var fnt = GenericFontMetricsSerializer.Deserialize(ms); - fonts.Add(fnt.GetKey(), fnt); - } - - } - } - } - return fonts; - } - } -} diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsSerializer.cs b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsSerializer.cs deleted file mode 100644 index ed4b7ebe2f..0000000000 --- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsSerializer.cs +++ /dev/null @@ -1,74 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 12/26/2021 EPPlus Software AB EPPlus 6.0 - *************************************************************************************************/ -using OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts.GenericMeasurements; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts.GenericMeasurements -{ - internal static class GenericFontMetricsSerializer - { - public static readonly Encoding FileEncoding = Encoding.UTF8; - - public static SerializedFontMetrics Deserialize(Stream stream) - { - using (var reader = new BinaryReader(stream, FileEncoding)) - { - var metrics = new SerializedFontMetrics(); - metrics.Version = reader.ReadUInt16(); - metrics.Family = (FontMetricsFamilies)reader.ReadUInt16(); - metrics.SubFamily = (FontSubFamilies)reader.ReadUInt16(); - metrics.LineHeight1em = reader.ReadSingle(); - metrics.DefaultWidthClass = (FontMetricsClass)reader.ReadByte(); - var nClassWidths = reader.ReadUInt16(); - if (nClassWidths == 0) - { - return metrics; - } - for (var x = 0; x < nClassWidths; x++) - { - var cls = (FontMetricsClass)reader.ReadByte(); - var width = reader.ReadSingle(); - metrics.ClassWidths[cls] = width; - } - var nClasses = reader.ReadUInt16(); - for (var x = 0; x < nClasses; x++) - { - var cls = (FontMetricsClass)reader.ReadByte(); - var nRanges = reader.ReadUInt16(); - for (var rngIx = 0; rngIx < nRanges; rngIx++) - { - var start = reader.ReadUInt16(); - var end = reader.ReadUInt16(); - for (var c = start; c <= end; c++) - { - metrics.CharMetrics[Convert.ToChar(c)] = cls; - } - } - var nCharactersInClass = reader.ReadUInt16(); - if (nCharactersInClass == 0) continue; - for (int y = 0; y < nCharactersInClass; y++) - { - var cCode = reader.ReadUInt16(); - var c = Convert.ToChar(cCode); - metrics.CharMetrics[c] = cls; - } - } - return metrics; - } - } - } -} diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurer.cs b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurer.cs index f72b29d464..0cf9daabda 100644 --- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurer.cs +++ b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurer.cs @@ -9,7 +9,9 @@ This software is licensed under PolyForm Noncommercial License 1.0.0 Date Author Change ************************************************************************************************* 12/26/2021 EPPlus Software AB EPPlus 6.0 + 09/01/2026 EPPlus Software AB Resolve missing subfamilies to Regular *************************************************************************************************/ +using EPPlus.Fonts.OpenType.GenericFontWidths; using OfficeOpenXml.Core.Worksheet.Fonts.GenericFontMetrics; using OfficeOpenXml.Interfaces.Drawing.Text; using System; @@ -24,23 +26,22 @@ internal class GenericFontMetricsTextMeasurer : GenericFontMetricsTextMeasurerBa /// Only CR, LF or CRLF should be considered. /// #pragma warning disable 618 - public bool MeasureWrappedTextCells + public bool MeasureWrappedTextCells { - get; - set; + get; + set; } #pragma warning restore 618 /// /// /// /// - public eWrappedTextAutofitMode WrappedTextAutofitMode - { - get; - set; + public eWrappedTextAutofitMode WrappedTextAutofitMode + { + get; + set; } - /// /// Measures the supplied text /// @@ -49,8 +50,14 @@ public eWrappedTextAutofitMode WrappedTextAutofitMode /// A public TextMeasurement MeasureText(string text, MeasurementFont font) { - var fontKey = GetKey(font.FontFamily, font.Style); - if (!IsValidFont(fontKey)) return TextMeasurement.Empty; + // ResolveKey rather than GetKey: fifteen family/subfamily combinations have no + // metrics because the font is not shipped by Windows at all, and those now fall + // back to the family's Regular instead of measuring as zero width. + var fontKey = GenericTextMeasurerKey.ResolveKey(font.FontFamily, font.Style); + if (fontKey == uint.MaxValue) return TextMeasurement.Empty; + + // The original style is still passed through: it drives the East Asian width + // handling, which does not depend on which file the metrics came from. return MeasureTextInternal(text, fontKey, font.Style, font.Size, WrappedTextAutofitMode); } @@ -61,31 +68,29 @@ public bool ValidForEnvironment() internal List MeasureIndividualCharacters(string text, MeasurementFont font, float ppi = 108.73578912433f) { - var fontKey = GetKey(font.FontFamily, font.Style); - if (IsValidFont(fontKey)) + var fontKey = GenericTextMeasurerKey.ResolveKey(font.FontFamily, font.Style); + if (fontKey == uint.MaxValue) { - return MeasureTextSpacingInternal(text, fontKey, font.Style, font.Size, ppi); - } - else - { - throw new InvalidOperationException("Font is not valid"); + throw new InvalidOperationException( + string.Format("No font metrics available for {0} {1}", font.FontFamily, font.Style)); } + return MeasureTextSpacingInternal(text, fontKey, font.Style, font.Size, ppi); } + internal uint MeasureIndividualCharacter(char c, MeasurementFont font, float ppi = 108.73578912433f) { - var fontKey = GetKey(font.FontFamily, font.Style); - if (IsValidFont(fontKey)) + var fontKey = GenericTextMeasurerKey.ResolveKey(font.FontFamily, font.Style); + if (fontKey == uint.MaxValue) { - float resolutionDifference = ppi / 96f; - float ptSize = font.Size * (72f / 96f); - float finalFactor = resolutionDifference * ptSize; - - return MeasureCharacter(c, fontKey, font.Style, ptSize, finalFactor); - } - else - { - throw new InvalidOperationException("Font is not valid"); + throw new InvalidOperationException( + string.Format("No font metrics available for {0} {1}", font.FontFamily, font.Style)); } + + float resolutionDifference = ppi / 96f; + float ptSize = font.Size * (72f / 96f); + float finalFactor = resolutionDifference * ptSize; + + return MeasureCharacter(c, fontKey, font.Style, ptSize, finalFactor); } } -} +} \ No newline at end of file diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurerBase.cs b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurerBase.cs index 09cde3fc8a..a012c9252b 100644 --- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurerBase.cs +++ b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurerBase.cs @@ -9,51 +9,53 @@ This software is licensed under PolyForm Noncommercial License 1.0.0 Date Author Change ************************************************************************************************* 12/26/2021 EPPlus Software AB EPPlus 6.0 + 09/01/2026 EPPlus Software AB Use shared cache and compact character lookup *************************************************************************************************/ -using OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts; -using OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts.GenericMeasurements; +using EPPlus.Fonts.OpenType.GenericFontWidths; using OfficeOpenXml.Interfaces.Drawing.Text; using System; using System.Collections.Generic; -using System.Drawing; -using System.Linq; namespace OfficeOpenXml.Core.Worksheet.Fonts.GenericFontMetrics { internal abstract class GenericFontMetricsTextMeasurerBase { - private FontScaleFactors _fontScaleFactors = new FontScaleFactors(); - private static Dictionary _fonts; - private static object _syncRoot = new object(); + /// + /// Shared rather than per instance. The table holds 116 entries and never changes, and + /// every measurer was allocating its own copy. + /// + private static readonly FontScaleFactors _fontScaleFactors = new FontScaleFactors(); public GenericFontMetricsTextMeasurerBase() { - Initialize(); + // Nothing to do. Metrics load on first use through GenericFontMetricsCache, which + // replaces the static dictionary this class used to populate eagerly - that read + // every font in the archive the first time anything was measured. } - private static void Initialize() + internal protected bool IsValidFont(uint fontKey) { - lock (_syncRoot) - { - if (_fonts == null) - { - _fonts = GenericFontMetricsLoader.LoadFontMetrics(); - } - } + return GenericFontMetricsCache.IsValidFont(fontKey); } - internal protected bool IsValidFont(uint fontKey) + private static SerializedFontMetrics GetFont(uint fontKey) { - return _fonts.ContainsKey(fontKey); + var font = GenericFontMetricsCache.GetMetrics(fontKey); + if (font == null) + { + throw new InvalidOperationException( + string.Format("No font metrics loaded for key {0}.", fontKey)); + } + return font; } internal protected TextMeasurement MeasureTextInternal(string text, uint fontKey, MeasurementFontStyles style, float size, eWrappedTextAutofitMode mode = eWrappedTextAutofitMode.Skip) { - if(text==null) + if (text == null) { - return new TextMeasurement(0, 0); + return new TextMeasurement(0, 0); } - var sFont = _fonts[fontKey]; + var sFont = GetFont(fontKey); // Width of the current segment (a "segment" is a line in SplitNewLine mode, // or a word in SplitWord mode). In FullText/Skip the whole text is one segment. @@ -66,7 +68,6 @@ internal protected TextMeasurement MeasureTextInternal(string text, uint fontKey for (var x = 0; x < text.Length; x++) { - var fnt = sFont; var c = text[x]; if (IsSegmentBoundary(c, mode)) @@ -74,15 +75,15 @@ internal protected TextMeasurement MeasureTextInternal(string text, uint fontKey // A CRLF pair is a single line break, not two. if (x > 0 && c == '\r' && text[x - 1] == '\n') { - continue; //CRLF should be handle - //d as one new line. + continue; } // A visible boundary character (hyphen) remains at the end of the // segment it terminates, so its own width is added before the break. - if (IsVisibleBoundary(c) && sFont.CharMetrics.ContainsKey(c)) + FontMetricsClass boundaryClass; + if (IsVisibleBoundary(c) && sFont.TryGetClass(c, out boundaryClass)) { - width += fnt.ClassWidths[sFont.CharMetrics[c]]; + width += sFont.GetClassWidth(boundaryClass); } // Close the current segment: keep it if it is the widest so far. @@ -95,9 +96,6 @@ internal protected TextMeasurement MeasureTextInternal(string text, uint fontKey // Start a new, empty segment. width = 0f; widthEA = 0f; - - // The boundary character itself is not part of the next segment. - // (Visible boundaries were already counted into the closed segment above.) continue; } @@ -108,15 +106,16 @@ internal protected TextMeasurement MeasureTextInternal(string text, uint fontKey } else { - if (sFont.CharMetrics.ContainsKey(c)) + FontMetricsClass cls; + if (sFont.TryGetClass(c, out cls)) { - var fw = fnt.ClassWidths[sFont.CharMetrics[c]]; + var fw = sFont.GetClassWidth(cls); if (Char.IsDigit(c)) fw *= FontScaleFactors.DigitsScalingFactor; width += fw; } else if (char.IsControl(c) == false) { - width += sFont.ClassWidths[fnt.DefaultWidthClass]; + width += sFont.DefaultWidth; } } } @@ -169,91 +168,18 @@ private static bool IsVisibleBoundary(char c) return c == '\u002D' || c == '\u2010'; } - static Dictionary AlphabetChars = new Dictionary - { - {'a', 0x06 }, - {'b', 0x07 }, - {'c', 0x05 }, - {'d', 0x07 }, - {'e', 0x06 }, - {'f', 0x04 }, - {'g', 0x07 }, - {'h', 0x07 }, - {'i', 0x03 }, - {'j', 0x03 }, - {'k', 0x06 }, - {'l', 0x03 }, - {'m', 0x09 }, - {'n', 0x07 }, - {'o', 0x07 }, - {'p', 0x07 }, - {'q', 0x07 }, - {'r', 0x04 }, - {'s', 0x05 }, - {'t', 0x04 }, - {'u', 0x07 }, - {'v', 0x05 }, - {'w', 0x09 }, - {'x', 0x05 }, - {'y', 0x05 }, - {'z', 0x05 }, - {'A', 0x07 }, - {'B', 0x06 }, - {'C', 0x07 }, - {'D', 0x08 }, - {'E', 0x06 }, - {'F', 0x06 }, - {'G', 0x08 }, - {'H', 0x08 }, - {'I', 0x03 }, - {'J', 0x04 }, - {'K', 0x06 }, - {'L', 0x05 }, - {'M', 0x0A }, - {'N', 0x08 }, - {'O', 0x09 }, - {'P', 0x06 }, - {'Q', 0x08 }, - {'R', 0x07 }, - {'S', 0x06 }, - {'T', 0x06 }, - {'U', 0x08 }, - {'V', 0x07 }, - {'W', 0x0B }, - {'X', 0x06 }, - {'Y', 0x05 }, - {'Z', 0x06 } - }; - internal List MeasureTextSpacingInternal(string text, uint fontKey, MeasurementFontStyles style, float size, float ppi = 108.73578912433f) { - var sFont = _fonts[fontKey]; - var chars = text.ToCharArray(); - - var spacingBuffer = new List(); - - var widthDefault = sFont.ClassWidths[sFont.DefaultWidthClass]; + var sFont = GetFont(fontKey); + var spacingBuffer = new List(text.Length); float resolutionDifference = ppi / 96f; float ptSize = size * (72f / 96f); - float finalFactor = resolutionDifference * ptSize; - for (var x = 0; x < chars.Length; x++) + for (var x = 0; x < text.Length; x++) { - var fnt = sFont; - var c = chars[x]; - - var fntClass = sFont.CharMetrics.ContainsKey(c) ? sFont.CharMetrics[c] : fnt.DefaultWidthClass; - float adjustmentFactor = 0.012f * ptSize * ((int)fntClass); - - float deviceUnits = fnt.ClassWidths[fntClass] * finalFactor - adjustmentFactor; - - var rounded = Math.Round(deviceUnits * 10, MidpointRounding.AwayFromZero); - var final = rounded / 10; - - uint simplifiedWidth = (uint)(Math.Round(deviceUnits, MidpointRounding.AwayFromZero)); - spacingBuffer.Add(simplifiedWidth); + spacingBuffer.Add(MeasureCharacterInternal(sFont, text[x], ptSize, finalFactor)); } return spacingBuffer; @@ -261,56 +187,21 @@ internal List MeasureTextSpacingInternal(string text, uint fontKey, Measur internal uint MeasureCharacter(char c, uint fontKey, MeasurementFontStyles style, float ptSize, float finalFactor) { - var sFont = _fonts[fontKey]; - var fnt = sFont; - - var fntClass = sFont.CharMetrics.ContainsKey(c) ? sFont.CharMetrics[c] : fnt.DefaultWidthClass; - float adjustmentFactor = 0.012f * ptSize * ((int)fntClass); - - float deviceUnits = fnt.ClassWidths[fntClass] * finalFactor - adjustmentFactor; - - uint simplifiedWidth = (uint)Math.Round(deviceUnits, MidpointRounding.AwayFromZero); - return simplifiedWidth; - } - - internal static uint GetKey(FontMetricsFamilies family, FontSubFamilies subFamily) - { - var k1 = (ushort)family; - var k2 = (ushort)subFamily; - return (uint)((k1 << 16) | ((k2) & 0xffff)); + return MeasureCharacterInternal(GetFont(fontKey), c, ptSize, finalFactor); } - internal static uint GetKey(string fontFamily, MeasurementFontStyles fontStyle) + private static uint MeasureCharacterInternal(SerializedFontMetrics sFont, char c, float ptSize, float finalFactor) { - var enumName = fontFamily.Replace(" ", string.Empty); - var values = Enum.GetValues(typeof(FontMetricsFamilies)); - var supported = false; - foreach (var enumVal in values) - { - if (enumVal.ToString() == enumName) - { - supported = true; - break; - } - } - if (!supported) return uint.MaxValue; - var family = (FontMetricsFamilies)Enum.Parse(typeof(FontMetricsFamilies), enumName); - var subFamily = FontSubFamilies.Regular; - switch (fontStyle) + FontMetricsClass fntClass; + if (!sFont.TryGetClass(c, out fntClass)) { - case MeasurementFontStyles.Bold: - subFamily = FontSubFamilies.Bold; - break; - case MeasurementFontStyles.Italic: - subFamily = FontSubFamilies.Italic; - break; - case MeasurementFontStyles.Italic | MeasurementFontStyles.Bold: - subFamily = FontSubFamilies.BoldItalic; - break; - default: - break; + fntClass = sFont.DefaultWidthClass; } - return GetKey(family, subFamily); + + float adjustmentFactor = 0.012f * ptSize * ((int)fntClass); + float deviceUnits = sFont.GetClassWidth(fntClass) * finalFactor - adjustmentFactor; + + return (uint)Math.Round(deviceUnits, MidpointRounding.AwayFromZero); } private static float GetEastAsianCharWidth(int cc, MeasurementFontStyles style) @@ -323,12 +214,23 @@ private static float GetEastAsianCharWidth(int cc, MeasurementFontStyles style) return emWidth * (96F / 72F) * FontScaleFactors.JapaneseKanjiDefaultScalingFactor; } + /// + /// Returns true when the character falls inside one of the Japanese/Kanji ranges. + /// + /// The LINQ version this replaces allocated an enumerator and a closure for every + /// character measured. The early return covers Latin and most of the BMP below the CJK + /// blocks, which is the overwhelming majority of cell content. + /// private static bool IsEastAsianChar(char c) { var cc = (int)c; + if (cc < 0x2E80) return false; - return UniCodeRange.JapaneseKanji.Any(x => x.IsInRange(cc)); + foreach (var range in UniCodeRange.JapaneseKanji) + { + if (range.IsInRange(cc)) return true; + } + return false; } - } -} +} \ No newline at end of file diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/SerializedFontMetrics.cs b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/SerializedFontMetrics.cs deleted file mode 100644 index 5533a66513..0000000000 --- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/SerializedFontMetrics.cs +++ /dev/null @@ -1,63 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 12/26/2021 EPPlus Software AB EPPlus 6.0 - *************************************************************************************************/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts.GenericMeasurements -{ - internal class SerializedFontMetrics - { - public SerializedFontMetrics() - { - ClassWidths = new Dictionary(); - CharMetrics = new Dictionary(); - } - - public FontMetricsFamilies Family { get; set; } - - public FontSubFamilies SubFamily { get; set; } - - public ushort Version { get; set; } - public uint FontKey { get; set; } - public float LineHeight1em { get; set; } - - public FontMetricsClass DefaultWidthClass { get; set; } - - public Dictionary ClassWidths - { - get; - private set; - } - - public Dictionary CharMetrics - { - get; - private set; - } - - public uint GetKey() - { - return GetKey(Family, SubFamily); - } - - public static uint GetKey(FontMetricsFamilies family, FontSubFamilies subFamily) - { - var k1 = (ushort)family; - var k2 = (ushort)subFamily; - return (uint)((k1 << 16) | ((k2) & 0xffff)); - } - - } -} diff --git a/src/EPPlus/Drawing/EMF/Records/EMR_EXTTEXTOUTW.cs b/src/EPPlus/Drawing/EMF/Records/EMR_EXTTEXTOUTW.cs index 15d68e229e..c650b3b6c4 100644 --- a/src/EPPlus/Drawing/EMF/Records/EMR_EXTTEXTOUTW.cs +++ b/src/EPPlus/Drawing/EMF/Records/EMR_EXTTEXTOUTW.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 01/01/2025 EPPlus Software AB Initial release EPPlus 8 *************************************************************************************************/ +using EPPlus.Fonts.OpenType.GenericFontWidths; using OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts.GenericMeasurements; using OfficeOpenXml.Core.Worksheet.Fonts.GenericFontMetrics; using OfficeOpenXml.Interfaces.Drawing.Text; @@ -116,7 +117,7 @@ internal EMR_EXTTEXTOUTW(BinaryReader br, uint TypeValue) : base(br, TypeValue) internal byte[] CalculateDxSpacing(string targetString) { var aMesurement = (GenericFontMetricsTextMeasurer)textSettings.GenericTextMeasurer; - aMesurement.MeasureTextInternal(targetString, GenericFontMetricsTextMeasurerBase.GetKey(Font.elw.mFont.FontFamily, Font.elw.mFont.Style), Font.elw.mFont.Style, Font.elw.mFont.Size); + aMesurement.MeasureTextInternal(targetString, GenericTextMeasurerKey.GetKey(Font.elw.mFont.FontFamily, Font.elw.mFont.Style), Font.elw.mFont.Style, Font.elw.mFont.Size); var values = aMesurement.MeasureIndividualCharacters(targetString, Font.elw.mFont, Ppi); var measurement = aMesurement.MeasureText(targetString, Font.elw.mFont); diff --git a/src/EPPlus/EPPlus.csproj b/src/EPPlus/EPPlus.csproj index 2fa1b01218..8e58351ffb 100644 --- a/src/EPPlus/EPPlus.csproj +++ b/src/EPPlus/EPPlus.csproj @@ -834,16 +834,12 @@ - - - - Never diff --git a/src/EPPlus/ExcelWorkbook.cs b/src/EPPlus/ExcelWorkbook.cs index 4bde6f69fb..524b9bde13 100644 --- a/src/EPPlus/ExcelWorkbook.cs +++ b/src/EPPlus/ExcelWorkbook.cs @@ -56,6 +56,7 @@ Date Author Change using System.Threading; using System.Threading.Tasks; using System.Xml; +using EPPlus.Fonts.OpenType.GenericFontWidths; namespace OfficeOpenXml { diff --git a/src/EPPlus/ExcelWorksheetView.cs b/src/EPPlus/ExcelWorksheetView.cs index ee462c909b..2071a59f8a 100644 --- a/src/EPPlus/ExcelWorksheetView.cs +++ b/src/EPPlus/ExcelWorksheetView.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using EPPlus.Fonts.OpenType.GenericFontWidths; using OfficeOpenXml.Drawing; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.Utils.EnumUtils; diff --git a/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs b/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs index 9261792bac..b29c508cab 100644 --- a/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs +++ b/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs @@ -59,10 +59,10 @@ public FontSubFamily GetSubFamilyFromNormalStyle //move this to a helper class o } } - public static double GetThemeFont0Width(ExcelWorksheet ws) + public static double GetThemeFont0Width(ExcelWorksheet ws, OpenTypeFontEngine engine) { var ns = ws.Workbook.Styles.GetNormalStyle(); - TextShaper shaper = OpenTypeFonts.GetTextShaper(ns.Style.Font.Name, FontSubFamily.Regular); + var shaper = engine.GetTextShaper(ns.Style.Font.Name, FontSubFamily.Regular); var shapedText = shaper.ShapeLight("0"); return shapedText.GetWidthInPoints(ns.Style.Font.Size); } diff --git a/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs b/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs index 4d42233ac7..083530b7ee 100644 --- a/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs +++ b/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs @@ -37,7 +37,7 @@ public static PdfCellCollection SetTextMap(PdfPageSettings pageSettings, PdfDict var tableStyleCache = new Dictionary(); var Range = pdfRange; var worksheet = Range.Range.Worksheet; - var ZeroCharWidth = pdfSheet.ZeroCharWidth = PdfWorksheet.GetThemeFont0Width(worksheet); + var ZeroCharWidth = pdfSheet.ZeroCharWidth = PdfWorksheet.GetThemeFont0Width(worksheet, pageSettings.FontEngine); int addedColumns = Range.ExtendColumns ? AddColumnsForNonWrappedText(pageSettings, worksheet, pdfSheet) : 0; var Map = new PdfCellCollection(Range.Range._fromRow, Range.Range._toRow, Range.Range._fromCol, Range.Range._toCol + addedColumns); pdfSheet.ToRow = pdfSheet.ToRow < Range.Range._toRow ? Range.Range._toRow : pdfSheet.ToRow; diff --git a/src/EPPlusTest/Core/Worksheet/AutofitWithSerializedFontMetricsTests.cs b/src/EPPlusTest/Core/Worksheet/AutofitWithSerializedFontMetricsTests.cs index 7e388c0deb..f08c29e808 100644 --- a/src/EPPlusTest/Core/Worksheet/AutofitWithSerializedFontMetricsTests.cs +++ b/src/EPPlusTest/Core/Worksheet/AutofitWithSerializedFontMetricsTests.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using EPPlus.Fonts.OpenType.GenericFontWidths; +using Microsoft.VisualStudio.TestTools.UnitTesting; using OfficeOpenXml; using OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts; using OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts.GenericMeasurements; @@ -14,6 +15,31 @@ namespace EPPlusTest.Core.Worksheet [TestClass] public class AutofitWithSerializedFontMetricsTests : TestBase { + private const float AutofitCorpusFontSize = 9f; + + private static readonly string[] AutofitCorpusHeaders = + { + "Narrow", "Wide", "Words", "Digits", "Punctuation", "Sentence", "Long mixed", "Short", "East Asian" + }; + + private static readonly string[,] AutofitCorpus = + { + // Narrow glyphs - the bottom width classes. + { "illij", "lililili", "iilltjfi lliftj", "jlitfi.ijltf ilfjti" }, + // Wide glyphs - the top width classes. + { "WMQ", "WWMMQQ", "MWQOGWM WQMOW", "WMOQGWM QWMOGW WMQOG" }, + // Ordinary words, the case that actually matters most. + { "Name", "Stockholm", "Invoice number", "Quarterly revenue report" }, + // Digits get their own scaling factor in the measurer. + { "23", "1234567", "1 234 567,89", "0123456789 0123456789" }, + { ".,!-", "-.,!?:;", "!!! ??? ...", "Hello, world! - (again); yes?" }, + { "One two", "The quick brown fox", "Jumps over the lazy dog", "Pack my box with five dozen jugs" }, + { "Ab1.", "Xy9! Zq2?", "Order 4711 - shipped, 12 pcs", "Ref: AB-1234/2026 (rev 3) - approved 12,5%" }, + { "A", "Hi", "OK", "End" }, + // Measured as full width regardless of font; the half width Katakana block is half. + { "日本語", "日本語のテキスト", "ハンカク カタカナ", "日本語とハンカクの混在テキスト" } + }; + [TestMethod] [DataRow("Calibri")] [DataRow("Aptos Narrow")] @@ -34,60 +60,70 @@ public class AutofitWithSerializedFontMetricsTests : TestBase [DataRow("Tw Cen MT")] [DataRow("Tw Cen MT Condensed")] [DataRow("Segoe UI")] + [DataRow("Tahoma")] public void AutofitWithSerializedFonts(string fontFamily) { + var columns = AutofitCorpus.GetLength(0); + var rows = AutofitCorpus.GetLength(1); + using (var package = new ExcelPackage()) { - for(var style = FontSubFamilies.Regular; style <= FontSubFamilies.BoldItalic; style++) + var measurements = package.Workbook.Worksheets.Add("Measurements"); + measurements.Cells[1, 1].Value = "Font"; + measurements.Cells[1, 2].Value = "Style"; + measurements.Cells[1, 3].Value = "Column"; + measurements.Cells[1, 4].Value = "Category"; + measurements.Cells[1, 5].Value = "Widest cell"; + measurements.Cells[1, 6].Value = "EPPlus width (chars)"; + measurements.Cells[1, 7].Value = "Excel width (chars)"; + measurements.Cells[1, 1, 1, 7].Style.Font.Bold = true; + var measurementRow = 2; + + for (var style = FontSubFamilies.Regular; style <= FontSubFamilies.BoldItalic; style++) { var sheet = package.Workbook.Worksheets.Add(style.ToString()); - var range = sheet.Cells[1, 1, 5, 10]; + var range = sheet.Cells[1, 1, rows + 1, columns]; range.Style.Font.Name = fontFamily; - range.Style.Font.Size = 9f; - range.Style.Font.Italic = (style == FontSubFamilies.Italic || style == FontSubFamilies.BoldItalic); - range.Style.Font.Bold = (style == FontSubFamilies.Bold || style == FontSubFamilies.BoldItalic); - var rnd = new Random(); - for (var col = 1; col < 10; col++) + range.Style.Font.Size = AutofitCorpusFontSize; + range.Style.Font.Italic = style == FontSubFamilies.Italic || style == FontSubFamilies.BoldItalic; + range.Style.Font.Bold = style == FontSubFamilies.Bold || style == FontSubFamilies.BoldItalic; + + for (var col = 0; col < columns; col++) { - for (var row = 1; row < 5; row++) + sheet.Cells[1, col + 1].Value = AutofitCorpusHeaders[col]; + for (var row = 0; row < rows; row++) { - var sb = new StringBuilder(); - var maxLength = 40 - (col * 2); - var nLetters = rnd.Next(4, maxLength); - for (var x = 0; x < nLetters; x++) - { - var n = 65; - if (x % 2 == 0) - { - n = rnd.Next(65, 90); - } - else if(x % 5 == 0) - { - var charArr = new int[] { (int)'.', (int)',', (int)'!', (int)'-' }; - var cix = rnd.Next(0, charArr.Length - 1); - n = charArr[cix]; - } - else if(x % 7 == 0) - { - n = (int)' '; - } - else - { - n = rnd.Next(97, 122); - } + sheet.Cells[row + 2, col + 1].Value = AutofitCorpus[col, row]; + } + } + + sheet.Columns[1, columns].AutoFit(); - sb.Append((char)n); + for (var col = 0; col < columns; col++) + { + var widest = string.Empty; + for (var row = 0; row < rows; row++) + { + if (AutofitCorpus[col, row].Length > widest.Length) + { + widest = AutofitCorpus[col, row]; } - sheet.Cells[row, col].Value = sb.ToString(); } + + measurements.Cells[measurementRow, 1].Value = fontFamily; + measurements.Cells[measurementRow, 2].Value = style.ToString(); + measurements.Cells[measurementRow, 3].Value = col + 1; + measurements.Cells[measurementRow, 4].Value = AutofitCorpusHeaders[col]; + measurements.Cells[measurementRow, 5].Value = widest; + measurements.Cells[measurementRow, 6].Value = Math.Round(sheet.Column(col + 1).Width, 2); + measurementRow++; } - var sw = new Stopwatch(); - sw.Start(); - sheet.Columns[1, 9].AutoFit(); - sw.Stop(); - var ms = sw.ElapsedMilliseconds; } - + + // Column 7 is left empty on purpose - fill it in from Excel after running + // Excel's own autofit on the same columns, so the two sit side by side. + measurements.Cells[1, 1, measurementRow - 1, 7].AutoFitColumns(); + SaveWorkbook($"Autofit_SerializedFont_{fontFamily.Replace(" ", string.Empty)}.xlsx", package); } } diff --git a/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs b/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs index 1eef5b295d..56ef51a530 100644 --- a/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs +++ b/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs @@ -83,8 +83,11 @@ public void WrapMultipleFragments_SpacedEndWord() ]; fonts.Add(mf2); - - var txtMeasurer = OpenTypeFonts.GetTextLayoutEngineForFont(mf2); + var engine = new OpenTypeFontEngine(cfg => + { + cfg.SearchSystemDirectories = true; + }); + var txtMeasurer = engine.GetTextLayoutEngineForFont(mf2); var maxWidth = 114d; var wrappedFragments = txtMeasurer.WrapRichText(txtRuns, fonts, maxWidth.PixelToPoint()); diff --git a/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs b/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs index 9a27e3c33d..0a5b460394 100644 --- a/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs @@ -53,7 +53,7 @@ public void ShouldSetWidthAndDefaultRowAndWidthClasses() var sheet = package.Workbook.Worksheets.Add("Test"); sheet.Cells["A1"].Value = "Name"; sheet.Cells["B1"].Value = "Age"; - sheet.Cells["A2"].Value = "John Doe"; + sheet.Cells["A2"].Value = "John Doe John Doe John Doe"; sheet.Cells["B2"].Value = 23; sheet.Cells["A1:A2"].AutoFitColumns(); var range = sheet.Cells["A1:C3"]; @@ -65,7 +65,7 @@ public void ShouldSetWidthAndDefaultRowAndWidthClasses() exporter.Settings.SetRowHeight = true; exporter.Settings.Culture = new CultureInfo("us-en"); var result = exporter.GetSinglePage(); - var expected = "
NameAge
John Doe23
"; + var expected = "
NameAge
John Doe John Doe John Doe23
"; Assert.AreEqual(expected, result); } } diff --git a/src/EPPlusTest/Issues/DrawingIssues.cs b/src/EPPlusTest/Issues/DrawingIssues.cs index 4282f09521..b8a9eda5de 100644 --- a/src/EPPlusTest/Issues/DrawingIssues.cs +++ b/src/EPPlusTest/Issues/DrawingIssues.cs @@ -9,6 +9,7 @@ using OfficeOpenXml.Drawing.Chart; using System.IO; using System.Drawing; +using EPPlus.Fonts.OpenType.GenericFontWidths; namespace EPPlusTest.Issues { [TestClass] diff --git a/src/EPPlusTest/Issues/LegacyTests/Issues.cs b/src/EPPlusTest/Issues/LegacyTests/Issues.cs index d75ab7d83f..352a7bf589 100644 --- a/src/EPPlusTest/Issues/LegacyTests/Issues.cs +++ b/src/EPPlusTest/Issues/LegacyTests/Issues.cs @@ -26,6 +26,7 @@ Date Author Change ******************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *******************************************************************************/ +using EPPlus.Fonts.OpenType.GenericFontWidths; using EPPlusTest.Properties; using EPPlusTest.Table; using FakeItEasy; diff --git a/src/EPPlusTest/tests.publickey b/src/EPPlusTest/tests.publickey new file mode 100644 index 0000000000..413c79d39e Binary files /dev/null and b/src/EPPlusTest/tests.publickey differ