Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 55 additions & 16 deletions src/EPPlus.Export.Pdf/Resources/PdfDictionaries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ Date Author Change
*************************************************************************************************
27/11/2025 EPPlus Software AB EPPlus 9
08/17/2026 EPPlus Software AB Canonical FontKey + resolve cache
08/20/2026 EPPlus Software AB Document-wide subsetting via DocumentFontSubsetBuilder
*************************************************************************************************/
using EPPlus.Export.Pdf.Settings;
using EPPlus.Fonts.OpenType;
using EPPlus.Fonts.OpenType.Integration;
using EPPlus.Fonts.OpenType.Subsetting;
using OfficeOpenXml.Interfaces.Fonts;
using System.Collections.Generic;
using System.Linq;
using EPPlus.Export.Pdf.Settings;

namespace EPPlus.Export.Pdf.Resources
{
Expand All @@ -27,6 +29,10 @@ internal class PdfDictionaries
internal readonly Dictionary<string, PdfShadingResource> Shadings = new Dictionary<string, PdfShadingResource>();
internal Dictionary<FontKey, IFontProvider> ShapedProviders = new Dictionary<FontKey, IFontProvider>();

// One document-wide subset builder, replacing the per-font FontSubsetManager. Owns all
// fallback resolution, embedding-restriction decisions, and shared subset construction.
private DocumentFontSubsetBuilder _subsetBuilder;

// Cache mapping a requested (family, subfamily) to the canonical FontKey of
// the loaded font. Case-insensitive on the requested family so casing in the
// source workbook resolves to the same key. Ensures the font is only loaded
Expand Down Expand Up @@ -61,30 +67,63 @@ internal FontKey ResolveFontKey(PdfPageSettings pageSettings, string family, Fon
return key;
}

public void AddFont(PdfPageSettings pageSettings, string FontName, FontSubFamily SubFamily, string Text)
// CHANGE 1: AddFont now only feeds the builder. It no longer creates a PdfFontResource —
// resources are created later, per ACTUAL font, during shaping. We still resolve the
// requested key so it is registered in _requestedToKey for later provider wiring.
public void AddFont(PdfPageSettings pageSettings, string fontName, FontSubFamily subFamily, string text)
{
var key = ResolveFontKey(pageSettings, FontName, SubFamily);
if (!Fonts.ContainsKey(key))
EnsureBuilder(pageSettings);
ResolveFontKey(pageSettings, fontName, subFamily); // register the requested key
_subsetBuilder.AddText(fontName, subFamily, text);
}

private void EnsureBuilder(PdfPageSettings pageSettings)
{
if (_subsetBuilder == null)
_subsetBuilder = new DocumentFontSubsetBuilder(pageSettings.FontEngine);
}

// CHANGE 2: new. Runs the single document-wide build, then wires one shaping provider per
// requested font. Call once, after all text is collected, before shaping. Replaces the
// old per-font CreateSubsettedProvider loop in PdfCatalog.
internal void BuildSubsets(PdfPageSettings pageSettings)
{
if (_subsetBuilder == null) return; // no text was collected
_subsetBuilder.Build();

foreach (var requestedKey in _requestedToKey.Values.Distinct())
{
int label = 1;
if (Fonts.Count > 0)
{
label = Fonts.Last().Value.labelNumber + 1;
}
Fonts.Add(key, new PdfFontResource(FontName, SubFamily, label, pageSettings));
var provider = _subsetBuilder.GetShapingProvider(requestedKey.Family, requestedKey.SubFamily);
if (provider != null)
ShapedProviders[requestedKey] = provider;
}
var manger = Fonts[key].fontSubsetManager;
manger.AddText(Text);
}

// CHANGE 3: GetFont is used by the renderer for METRICS only (glyph font selection is done
// per-glyph via FontIdMap). After skipping, the requested font may not be embedded, so we
// translate the requested font to the ACTUAL primary that renders it (the shaping
// provider's primary) and return that resource.
internal PdfFontResource GetFont(PdfPageSettings pageSettings, string fontName, FontSubFamily subFamily)
{
var key = ResolveFontKey(pageSettings, fontName, subFamily);
if (!Fonts.ContainsKey(key))
var requestedKey = ResolveFontKey(pageSettings, fontName, subFamily);

// Preferred path: translate requested -> actual via the shaping provider's primary.
IFontProvider provider;
if (ShapedProviders.TryGetValue(requestedKey, out provider) && provider.PrimaryFont != null)
{
throw new KeyNotFoundException("Font: " + key + " is missing from dictionary.");
var actual = provider.PrimaryFont;
var actualKey = new FontKey(actual.GetEnglishFontFamilyName(), actual.NameTable.GetSubfamilyEnum());
PdfFontResource viaProvider;
if (Fonts.TryGetValue(actualKey, out viaProvider))
return viaProvider;
}
return Fonts[key];

// Fallback: the requested font was embedded under its own identity (not skipped).
PdfFontResource direct;
if (Fonts.TryGetValue(requestedKey, out direct))
return direct;

throw new KeyNotFoundException("Font: " + requestedKey + " is missing from dictionary.");
}
}
}
10 changes: 5 additions & 5 deletions src/EPPlus.Export.Pdf/Resources/PdfFontResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ internal class PdfFontResource : PdfResource
internal HashSet<char> Subset = new HashSet<char>();
internal HashSet<ushort> Gids = new HashSet<ushort>();
internal Dictionary<ushort, string> charactermappings = new Dictionary<ushort, string>();
internal FontSubsetManager fontSubsetManager;

public PdfFontResource(string fontName, FontSubFamily subFamily, int labelNumber, PdfPageSettings pageSettings)
: base("F", labelNumber)
: base("F", labelNumber)
{
this.fontName = fontName;
_fontEngine = pageSettings.FontEngine;
fontData = _fontEngine.LoadFont(fontName, subFamily);
fontSubsetManager = new FontSubsetManager(pageSettings.FontEngine, fontData);
// fontData is assigned by the caller (ShapeText / GidsAndCharMap) to the actual, already-
// subsetted font. The resource must not load a whole font here — for fallback fonts (Noto
// Emoji, Archivo) a name-based load would be wrong or wasteful.
}

//Get the Font Descriptor object to write in PDF.
Expand Down Expand Up @@ -92,7 +92,7 @@ internal PdfFontDescriptor GetFontDescriptorObject(int objectNumber, int version
flag |= 1 << 5; // Nonsymbolic
if (fontData.GetEnglishFontFamilyName().ToLower().Contains("script") || fontData.GetEnglishFontFamilyName().ToLower().Contains("cursive"))
flag |= 1 << 3;
if (fontData.PostTable.italicAngle.RawValue != 0 || (fontData.Os2Table.fsSelection & Os2Table.FsSelectionFlags.Italic) != 0)
if (fontData.PostTable.italicAngle.RawValue != 0 || (fontData.Os2Table.fsSelection & FsSelectionFlags.Italic) != 0)
flag |= 1 << 6;
if (((ushort)fontData.Os2Table.fsSelection & 0x100) != 0)
flag |= 1 << 16;
Expand Down
4 changes: 2 additions & 2 deletions src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ public PdfScaling Scaling
internal string defaultFontName = "";

//DEBUG
internal bool Debug = false;
internal bool PrintAsText = false;
internal bool Debug = true;
internal bool PrintAsText = true;

public PdfPageSettings(OpenTypeFontEngine fontEngine)
{
Expand Down
133 changes: 0 additions & 133 deletions src/EPPlus.Fonts.OpenType.Tests/FontSubsetManagerTests.cs

This file was deleted.

23 changes: 13 additions & 10 deletions src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Date Author Change
*************************************************************************************************/
using EPPlus.Fonts.OpenType.FontResolver;
using EPPlus.Fonts.OpenType.Scanner;
using EPPlus.Fonts.OpenType.Tables.Os2;
using EPPlus.Fonts.OpenType.Tests.Helpers;
using OfficeOpenXml.Interfaces.Drawing.Text;
using OfficeOpenXml.Interfaces.Fonts;
Expand Down Expand Up @@ -60,7 +61,7 @@ public void ReadSourceSans3Otf()
struct LicenseDataHolder()
{
public string? FontName;
public ushort LicenseType;
public FsTypeFlags LicenseType;
public string? LTypeString;
}

Expand All @@ -71,20 +72,20 @@ struct LicenseDataHolder()
/// 4: Preview & Print embedding: the font may be embedded, and may be temporarily loaded on other systems for purposes of viewing or printing the document. Documents containing Preview & Print fonts must be opened "read-only"; no edits can be applied to the document.
/// 8: Editable embedding: the font may be embedded, and may be temporarily loaded on other systems. As with Preview & Print embedding, documents containing Editable fonts may be opened for reading. In addition, editing is permitted, including ability to format new text using the embedded font, and changes may be saved.
/// </summary>
string GetFsString(ushort fsId)
string GetFsString(FsTypeFlags fsType)
{
switch (fsId)
switch (fsType & (FsTypeFlags)Os2Table.FsTypeUsageMask)
{
case 0:
case FsTypeFlags.Installable:
return "Installable Embedding";
case 2:
case FsTypeFlags.RestrictedLicense:
return "Restricted Licence Embedding";
case 4:
case FsTypeFlags.PreviewPrint:
return "Preview & Print Embedding";
case 8:
case FsTypeFlags.Editable:
return "Editable Embedding";
default:
return $"UNKNOWN VALUE: '{fsId}' POTENTIALLY CORRUPT FONT";
return $"UNKNOWN VALUE: '{(ushort)fsType}' POTENTIALLY CORRUPT FONT";
}
}

Expand Down Expand Up @@ -160,7 +161,8 @@ public void ReadAllOTFFonts()
Assert.AreEqual(Scanner.FontFormat.Otf, allFontsList[i].Format);
}

var fontsThatCannotBeEmbedded = dataHolder.Where(x => x.LicenseType == 2);
var fontsThatCannotBeEmbedded = dataHolder.Where(
x => (x.LicenseType & (FsTypeFlags)Os2Table.FsTypeUsageMask) == FsTypeFlags.RestrictedLicense);

Assert.AreEqual(0, fontsThatCannotBeEmbedded.Count());
}
Expand Down Expand Up @@ -211,7 +213,8 @@ public void ReadAllTTFFonts()
Assert.AreEqual(Scanner.FontFormat.Ttf, allFontsList[i].Format);
}

var fontsThatCannotBeEmbedded = dataHolder.Where(x => x.LicenseType == 2);
var fontsThatCannotBeEmbedded = dataHolder.Where(
x => (x.LicenseType & (FsTypeFlags)Os2Table.FsTypeUsageMask) == FsTypeFlags.RestrictedLicense);

Assert.AreEqual(0, fontsThatCannotBeEmbedded.Count());
}
Expand Down
Loading
Loading