Skip to content

Commit 0aa9fea

Browse files
committed
Fixes for subsetted fonts
1 parent ead3f20 commit 0aa9fea

12 files changed

Lines changed: 187 additions & 15 deletions

File tree

src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ public TextLayoutEngine(
6363
_spaceWidthCache = new Dictionary<float, double>();
6464
}
6565

66+
public double GetLineHeightInPoints(double fontSize)
67+
{
68+
return _shaper.GetLineHeightInPoints(fontSize);
69+
}
70+
71+
public double GetBaseLineInPoints(double fontSize)
72+
{
73+
return _shaper.GetBaseLineInPoints(fontSize);
74+
}
75+
76+
public double GetDescentInPoints(double fontSize)
77+
{
78+
return _shaper.GetDescentInPoints(fontSize);
79+
}
80+
6681
/// <summary>
6782
/// Gets a char width buffer with at least the specified capacity.
6883
/// Reuses existing buffer if large enough, otherwise rents larger one from pool.

src/EPPlus.Fonts.OpenType/OpenTypeFont.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,13 @@ public class OpenTypeFont
4747
private readonly object _syncRoot = new object();
4848

4949

50-
internal OpenTypeFont(FontFormat format)
50+
internal OpenTypeFont(FontFormat format, bool isSubset = false)
5151
{
5252
Format = format;
5353
_tableRecords = new Dictionary<string, TableRecord>();
5454
_localTableCache = new TableCache();
5555
_loaderCache = new TableLoaderCache();
56+
IsSubset = isSubset;
5657
}
5758

5859

@@ -419,6 +420,11 @@ public string SubFamily
419420
}
420421
}
421422

423+
public bool IsSubset
424+
{
425+
get; private set;
426+
}
427+
422428
public string GetEnglishFullFontFamilyName()
423429
{
424430
return GetNameString(NameRecordTypes.FullFontName);
@@ -589,6 +595,12 @@ public static bool TryParseEnum<T>(string value, out T result) where T : struct
589595

590596
internal Dictionary<string, byte[]> PreprocessedPaddedTables { get; } = new Dictionary<string, byte[]>();
591597

598+
/// <summary>
599+
/// For subset fonts: Maps original glyph IDs to new subset glyph IDs.
600+
/// Null for non-subset fonts.
601+
/// </summary>
602+
//public Dictionary<ushort, ushort> SubsetGlyphMapping { get; internal set; }
603+
592604

593605
/// <summary>
594606
/// Total length (in bytes) of the underlying font stream.

src/EPPlus.Fonts.OpenType/OpenTypeFontFactory.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,10 @@ public static OpenTypeFont CreateFromFace(FontFaceInfo face)
3434

3535
return new OpenTypeFont(fontData, format);
3636
}
37+
38+
public static OpenTypeFont CreateFromBytes(byte[] bytes, FontFormat format)
39+
{
40+
return new OpenTypeFont(bytes, format);
41+
}
3742
}
3843
}

src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,5 +279,10 @@ ex is NotSupportedException ||
279279

280280
return result;
281281
}
282+
283+
public static OpenTypeFont GetFromBytes(byte[] bytes, FontFormat format)
284+
{
285+
return OpenTypeFontFactory.CreateFromBytes(bytes, format);
286+
}
282287
}
283288
}

src/EPPlus.Fonts.OpenType/Subsetting/CmapSubsetProcessor.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Date Author Change
1313
using EPPlus.Fonts.OpenType.Tables;
1414
using EPPlus.Fonts.OpenType.Tables.Cmap;
1515
using EPPlus.Fonts.OpenType.Tables.Cmap.Mappings;
16+
using System;
1617
using System.Collections.Generic;
1718
using System.Linq;
1819

@@ -54,6 +55,8 @@ public void Rewrite(FontSubsettingContext context)
5455
// Build mapping: Unicode code point → NEW glyph ID in subset
5556
Dictionary<uint, ushort> cmapMapping = new Dictionary<uint, ushort>();
5657

58+
Console.WriteLine("\n=== CMAP REWRITE DEBUG ===");
59+
5760
foreach (uint codePoint in context.UsedCodePoints)
5861
{
5962
ushort oldGid;
@@ -64,6 +67,11 @@ public void Rewrite(FontSubsettingContext context)
6467
if (context.OldToNewGlyphId.TryGetValue(oldGid, out newGid))
6568
{
6669
cmapMapping[codePoint] = newGid;
70+
// Debug first few
71+
if (codePoint >= 'a' && codePoint <= 'c')
72+
{
73+
Console.WriteLine($" U+{codePoint:X4} ('{(char)codePoint}') : oldGID {oldGid:X4} -> newGID {newGid:X4}");
74+
}
6775
}
6876
else
6977
{

src/EPPlus.Fonts.OpenType/Subsetting/FontSubsettingContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public FontSubsettingContext(OpenTypeFont originalFont, IEnumerable<int> unicode
4545
if (originalFont == null) throw new ArgumentNullException("originalFont");
4646

4747
OriginalFont = originalFont;
48-
SubsetFont = new OpenTypeFont(originalFont.Format);
48+
SubsetFont = new OpenTypeFont(originalFont.Format, true);
4949

5050
SubsetFont.AddOrReplaceTable(originalFont.HeadTable.Clone());
5151
if (originalFont.NameTable != null)

src/EPPlus.Fonts.OpenType/Subsetting/GlyphAndLocaSubsetProcessor.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ Date Author Change
1313
using EPPlus.Fonts.OpenType.Tables.Glyph;
1414
using EPPlus.Fonts.OpenType.Tables.Head;
1515
using EPPlus.Fonts.OpenType.Tables.Loca;
16+
using System;
1617
using System.Collections.Generic;
18+
using System.IO;
1719

1820
namespace EPPlus.Fonts.OpenType.Subsetting
1921
{
@@ -36,7 +38,6 @@ public void Discover(FontSubsettingContext context)
3638

3739
public void Rewrite(FontSubsettingContext context)
3840
{
39-
// NewToOldGlyphId is sorted by new IDs (0, 1, 2...)
4041
var sortedOldIds = context.NewToOldGlyphId;
4142
List<Glyph> newGlyphs = new List<Glyph>(sortedOldIds.Count);
4243

@@ -58,20 +59,32 @@ public void Rewrite(FontSubsettingContext context)
5859
// 2. Save the new glyf table
5960
context.SubsetFont.AddOrReplaceTable(new GlyfTable(newGlyphs));
6061

61-
// 3. Build loca table with 4-byte alignment
62+
// 3. Build loca by ACTUALLY measuring serialized glyphs
6263
List<uint> offsets = new List<uint> { 0 };
63-
uint currentOffset = 0;
6464

65-
foreach (Glyph g in newGlyphs)
65+
using (var ms = new MemoryStream())
66+
using (var writer = new FontsBinaryWriter(ms))
6667
{
67-
int size = g.GetSize();
68-
uint paddedSize = (uint)((size + 3) & ~3); // Align to 4 bytes
69-
currentOffset += paddedSize;
70-
offsets.Add(currentOffset);
68+
foreach (Glyph g in newGlyphs)
69+
{
70+
long startPos = ms.Position;
71+
g.Serialize(writer);
72+
long endPos = ms.Position;
73+
74+
int writtenLength = (int)(endPos - startPos);
75+
int padding = (4 - (writtenLength % 4)) % 4;
76+
77+
for (int p = 0; p < padding; p++)
78+
writer.Write((byte)0);
79+
80+
offsets.Add((uint)ms.Position);
81+
}
82+
83+
Console.WriteLine($"Total glyf table size: {ms.Position} bytes, loca last offset: {offsets[offsets.Count - 1]}");
7184
}
7285

73-
// 4. Update head table format and add loca table
74-
bool useShortOffsets = currentOffset <= 131070;
86+
// 4. Update head and create loca
87+
bool useShortOffsets = offsets[offsets.Count - 1] <= 131070;
7588
context.SubsetFont.HeadTable.IndexToLocFormat = useShortOffsets
7689
? HeadTable.IndexToLocFormats.Offset16
7790
: HeadTable.IndexToLocFormats.Offset32;

src/EPPlus.Fonts.OpenType/Subsetting/SubsetFontBuilder.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ Date Author Change
1111
10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0
1212
*************************************************************************************************/
1313
using EPPlus.Fonts.OpenType.Subsetting.Processors;
14+
using System;
1415
using System.Collections.Generic;
16+
using System.Linq;
1517

1618
namespace EPPlus.Fonts.OpenType.Subsetting
1719
{
@@ -67,6 +69,8 @@ public OpenTypeFont CreateSubset(OpenTypeFont originalFont, IEnumerable<int> uni
6769

6870
context.SubsetFont.UsedCodePointsForSubset = new List<uint>(context.UsedCodePoints);
6971

72+
//context.SubsetFont.SubsetGlyphMapping = new Dictionary<ushort, ushort>(context.OldToNewGlyphId);
73+
7074
return context.SubsetFont;
7175
}
7276

@@ -76,12 +80,28 @@ private void BuildGlyphMapping(FontSubsettingContext context)
7680
var sortedGlyphs = new List<ushort>(context.IncludedGlyphs);
7781
sortedGlyphs.Sort();
7882

83+
Console.WriteLine($"\n=== BUILD GLYPH MAPPING DEBUG ===");
84+
Console.WriteLine($"Total included glyphs: {sortedGlyphs.Count}");
85+
Console.WriteLine($"First 10: {string.Join(", ", sortedGlyphs.Take(10).Select(g => $"{g:X4}").ToArray())}");
86+
Console.WriteLine($"Around 'a' (looking for 0x0045):");
87+
7988
for (ushort newId = 0; newId < sortedGlyphs.Count; newId++)
8089
{
8190
ushort oldId = sortedGlyphs[newId];
8291
context.OldToNewGlyphId[oldId] = newId;
8392
context.NewToOldGlyphId.Add(oldId);
93+
94+
if (oldId >= 0x0043 && oldId <= 0x0048)
95+
{
96+
Console.WriteLine($" oldGID {oldId:X4} -> newGID {newId:X4}");
97+
}
98+
}
99+
Console.WriteLine($"All {sortedGlyphs.Count} glyphs after sort:");
100+
for (int i = 0; i < Math.Min(50, sortedGlyphs.Count); i++)
101+
{
102+
Console.WriteLine($" [{i}] = oldGID {sortedGlyphs[i]:X4}");
84103
}
104+
Console.WriteLine($"==================================\n");
85105
}
86106
}
87107
}

src/EPPlus.Fonts.OpenType/Tables/Cmap/CmapTable.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ public bool ContainsChar(ushort charCode)
199199
}
200200

201201

202-
internal bool TryGetGlyphId(uint codePoint, out ushort glyphId)
202+
public bool TryGetGlyphId(uint codePoint, out ushort glyphId)
203203
{
204204
glyphId = 0;
205205

src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,5 +358,82 @@ private string GetEnglishName(NameRecordTypes type)
358358
return null;
359359
}
360360

361+
/// <summary>
362+
/// Returns the PostScript Name (nameID 6).
363+
/// Follows OpenType recommendations:
364+
/// 1. Prefer platform 3 (Windows) → UTF-16BE
365+
/// 2. Then platform 1 (Macintosh) → MacRoman
366+
/// 3. Then platform 0 (Unicode)
367+
/// If no PostScript name exists, fallback to a sanitized FullFontName.
368+
/// </summary>
369+
public string PostScriptName
370+
{
371+
get
372+
{
373+
// 1. Windows (platform 3) – most reliable
374+
var win = NameRecords
375+
.Where(r => r.RecordType == NameRecordTypes.PostScriptName && r.platformId == 3)
376+
.Select(r => r.Name)
377+
.FirstOrDefault(n => !string.IsNullOrEmpty(n));
378+
if (!string.IsNullOrEmpty(win))
379+
return SanitizePsName(win);
380+
381+
// 2. Unicode (platform 0)
382+
var uni = NameRecords
383+
.Where(r => r.RecordType == NameRecordTypes.PostScriptName && r.platformId == 0)
384+
.Select(r => r.Name)
385+
.FirstOrDefault(n => !string.IsNullOrEmpty(n));
386+
if (!string.IsNullOrEmpty(uni))
387+
return SanitizePsName(uni);
388+
389+
// 3. Macintosh (platform 1)
390+
var mac = NameRecords
391+
.Where(r => r.RecordType == NameRecordTypes.PostScriptName && r.platformId == 1)
392+
.Select(r => r.Name)
393+
.FirstOrDefault(n => !string.IsNullOrEmpty(n));
394+
if (!string.IsNullOrEmpty(mac))
395+
return SanitizePsName(mac);
396+
397+
// 4. If nameID 6 is missing – fallback to FullFontName (Windows)
398+
var full = GetFullFontName();
399+
if (!string.IsNullOrEmpty(full))
400+
return SanitizePsName(full);
401+
402+
// 5. Last fallback
403+
return "UnknownPSName";
404+
}
405+
}
406+
/// <summary>
407+
/// Sanitizes a name so it always becomes a valid PostScript-compatible font name.
408+
/// Removes illegal characters and replaces whitespace with hyphens.
409+
/// </summary>
410+
private static string SanitizePsName(string name)
411+
{
412+
if (string.IsNullOrEmpty(name))
413+
return "UnknownPSName";
414+
415+
var sb = new StringBuilder(name.Length);
416+
417+
foreach (char c in name)
418+
{
419+
if (char.IsWhiteSpace(c))
420+
{
421+
sb.Append('-');
422+
continue;
423+
}
424+
425+
// Valid ASCII range for PostScript names
426+
if (c >= 33 && c <= 126)
427+
{
428+
sb.Append(c);
429+
continue;
430+
}
431+
432+
// Skip invalid characters
433+
}
434+
435+
// If everything got stripped
436+
return sb.Length > 0 ? sb.ToString() : "UnknownPSName";
437+
}
361438
}
362439
}

0 commit comments

Comments
 (0)