Skip to content

Commit c87e239

Browse files
authored
Glyph-level fallback: route missing glyphs by Unicode script to bundled Noto Emoji/Math or named fallback fonts configured via SetScriptFallback. Fixed primary-font FontId invariant. (#2355)
1 parent 25ffc39 commit c87e239

27 files changed

Lines changed: 1072 additions & 293 deletions

src/EPPlus.Export.Pdf/PdfResources/PdfFontResource.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ internal class PdfFontResource : PdfResource
3636
internal int fontWidthObjectNumber = -1;
3737
internal int cidSetObjectNumber = -1;
3838
internal OpenTypeFont fontData;
39+
private OpenTypeFontEngine _fontEngine;
3940
private int firstChar = 32;
4041
private int lastChar = 255;
4142
private CIDSystemInfo cidSystemInfo = null;
@@ -55,13 +56,14 @@ public PdfFontResource(string fontName, FontSubFamily subFamily, int labelNumber
5556
: base("F", labelNumber)
5657
{
5758
this.fontName = fontName;
58-
fontData = OpenTypeFonts.LoadFont(fontName, subFamily, pageSettings.FontDirectories, pageSettings.SearchSystemDirectories);
59-
fontSubsetManager = new FontSubsetManager(fontData);
59+
_fontEngine = pageSettings.FontEngine;
60+
fontData = _fontEngine.LoadFont(fontName, subFamily);
61+
fontSubsetManager = new FontSubsetManager(pageSettings.FontEngine, fontData);
6062
}
6163

6264
internal static OpenTypeFont GetFontData(PdfPageSettings pageSettings, string fontName, FontSubFamily subFamily)
6365
{
64-
return OpenTypeFonts.LoadFont(fontName,subFamily, pageSettings.FontDirectories, pageSettings.SearchSystemDirectories);
66+
return pageSettings.FontEngine.LoadFont(fontName,subFamily);
6567
}
6668

6769
//Get font data from fontResources. If font does not exsist, add it to fontResources.
@@ -81,12 +83,6 @@ internal static OpenTypeFont GetFontResourceData(Dictionary<string, PdfFontResou
8183
return fontResources[FontData.FullFontName].fontData;
8284
}
8385

84-
internal void CreateSubset()
85-
{
86-
fontData = fontData.CreateSubset(Subset);
87-
Shaper = new TextShaper(fontData);
88-
}
89-
9086
//Get the Font Descriptor object to write in PDF.
9187
internal PdfFontDescriptor GetFontDescriptorObject(int objectNumber, int version = 0)
9288
{

src/EPPlus.Export.Pdf/PdfSettings/PdfPageSettings.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ Date Author Change
1111
27/11/2025 EPPlus Software AB EPPlus 9
1212
*************************************************************************************************/
1313
using EPPlus.Export.Pdf.PdfSettings.PdfPageSizes;
14+
using EPPlus.Fonts.OpenType;
1415
using System.Collections.Generic;
16+
using System.Linq;
1517

1618
namespace EPPlus.Export.Pdf.PdfSettings
1719
{
@@ -20,6 +22,34 @@ namespace EPPlus.Export.Pdf.PdfSettings
2022
/// </summary>
2123
public class PdfPageSettings
2224
{
25+
private OpenTypeFontEngine _fontEngine;
26+
public OpenTypeFontEngine FontEngine
27+
{
28+
get
29+
{
30+
if(_fontEngine == null)
31+
{
32+
_fontEngine = new OpenTypeFontEngine(x =>
33+
{
34+
if(FontDirectories != null && FontDirectories.Any())
35+
{
36+
foreach(var dir in FontDirectories)
37+
{
38+
if (!System.IO.Directory.Exists(dir))
39+
{
40+
throw new System.IO.DirectoryNotFoundException($"Font directory not found: {dir}");
41+
}
42+
x.FontDirectories.Add(dir);
43+
}
44+
x.SearchSystemDirectories = SearchSystemDirectories;
45+
46+
}
47+
48+
});
49+
}
50+
return _fontEngine;
51+
}
52+
}
2353
/// <summary>
2454
/// Add additional folders to search for fonts.
2555
/// </summary>

src/EPPlus.Fonts.OpenType.Benchmarks/ExtractCharWidthsBenchmark.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ public class ExtractCharWidthsBenchmark
1717
public void Setup()
1818
{
1919
var fontFolders = new List<string> { /* your font paths */ };
20-
var font = OpenTypeFonts.LoadFont("Calibri");
21-
_shaper = new TextShaper(font);
20+
var fontEngine = new OpenTypeFontEngine(x =>
21+
{
22+
x.SearchSystemDirectories = true;
23+
});
24+
var font = fontEngine.LoadFont("Calibri");
25+
_shaper = new TextShaper(fontEngine, font);
2226
_options = ShapingOptions.Default;
2327

2428
// Short: typical Excel cell

src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,14 @@ public void Setup()
5555

5656
// Configure the global font system to search the benchmark's local Fonts directory
5757
// exclusively. Must happen before any LoadFont call.
58-
OpenTypeFonts.Configure(cfg =>
58+
var fontEngine = new OpenTypeFontEngine(cfg =>
5959
{
6060
cfg.Reset();
6161
cfg.FontDirectories.Add(fontsPath);
6262
cfg.SearchSystemDirectories = false;
6363
});
6464

65+
6566
Console.WriteLine("\nAvailable Roboto fonts:");
6667
foreach (var file in Directory.GetFiles(fontsPath, "Roboto*.ttf"))
6768
{
@@ -74,7 +75,7 @@ public void Setup()
7475
Console.WriteLine(string.Format("Loaded: {0} {1} ({2} glyphs)",
7576
font.FullName, font.SubFamily, font.GlyfTable.Glyphs.Count));
7677

77-
var shaper = new TextShaper(font);
78+
var shaper = new TextShaper(fontEngine, font);
7879
_layoutEngine = new TextLayoutEngine(shaper);
7980

8081
Console.WriteLine("\nPre-warming font cache (Regular, Bold, Italic)...");

src/EPPlus.Fonts.OpenType.Benchmarks/TextLayoutEngineBenchmarks.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,14 @@ public List<string> New_Wrap_10Paragraphs_Sequential()
156156
[Benchmark]
157157
public double[] OnlyExtractWidths()
158158
{
159-
var font = OpenTypeFonts.LoadFont(FontFamily, FontSubFamily.Regular);
160-
var shaper = new TextShaper(font);
159+
var fontsPath = Path.Combine(AppContext.BaseDirectory, "Fonts");
160+
var fontEngine = new OpenTypeFontEngine(x =>
161+
{
162+
x.FontDirectories.Add(fontsPath);
163+
x.SearchSystemDirectories = false;
164+
});
165+
var font = fontEngine.LoadFont(FontFamily, FontSubFamily.Regular);
166+
var shaper = new TextShaper(fontEngine, font);
161167
return shaper.ExtractCharWidths(LoremIpsum20Para, FontSize, ShapingOptions.Default);
162168
}
163169

src/EPPlus.Fonts.OpenType.Benchmarks/TextShapingBenchmarks.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,27 @@ namespace EPPlus.Fonts.OpenType.Benchmarks
99
public class TextShapingBenchmarks
1010
{
1111
private OpenTypeFont _roboto;
12+
private OpenTypeFontEngine _engine;
1213
private TextShaper _shaper;
1314

1415
[GlobalSetup] // Runs once before all benchmarks
1516
public void Setup()
1617
{
1718
var fontsPath = Path.Combine(AppContext.BaseDirectory, "Fonts");
19+
_engine = new OpenTypeFontEngine(x =>
20+
{
21+
x.FontDirectories.Add(fontsPath);
22+
x.SearchSystemDirectories = false;
23+
});
1824

1925
if (!Directory.Exists(fontsPath))
2026
{
2127
throw new DirectoryNotFoundException($"Fonts directory not found: {fontsPath}");
2228
}
2329

2430
var fontFolders = new List<string> { fontsPath };
25-
_roboto = OpenTypeFonts.LoadFont("Roboto", FontSubFamily.Regular);
26-
_shaper = new TextShaper(_roboto);
31+
_roboto = _engine.LoadFont("Roboto", FontSubFamily.Regular);
32+
_shaper = new TextShaper(_engine, _roboto);
2733
}
2834

2935
[Benchmark]

0 commit comments

Comments
 (0)