Skip to content

Commit 0bd13da

Browse files
committed
wip
1 parent 4db367a commit 0bd13da

12 files changed

Lines changed: 407 additions & 90 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using EPPlus.Fonts.OpenType.TextShaping;
2+
using OfficeOpenXml.Interfaces.Drawing.Text;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
10+
namespace EPPlus.Fonts.OpenType.Tests.TextShaping
11+
{
12+
[TestClass]
13+
public class MarkToBaseTests : FontTestBase
14+
{
15+
public override TestContext? TestContext { get; set; }
16+
17+
[TestMethod]
18+
public void MarkToBaseTest()
19+
{
20+
var font = OpenTypeFonts.GetFontData(null, "Roboto", FontSubFamily.Regular, true);
21+
var shaper = new TextShaper(font);
22+
23+
string test = "A\u0302\u0309";
24+
var shaped = shaper.Shape(test, ShapingOptions.Full);
25+
26+
Debug.WriteLine("Glyphs för 'Ẩn ặ':");
27+
foreach (var g in shaped.Glyphs)
28+
{
29+
Debug.WriteLine($"GID={g.GlyphId,-4} XAdv={g.XAdvance,-5} YOff={g.YOffset,-4}");
30+
}
31+
Assert.IsTrue(shaped.Glyphs.Any(x => x.YOffset > 0));
32+
}
33+
}
34+
}

src/EPPlus.Fonts.OpenType/OpenTypeFont.cs

Lines changed: 7 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Date Author Change
2727
using EPPlus.Fonts.OpenType.Tables.Name;
2828
using EPPlus.Fonts.OpenType.Tables.Os2;
2929
using EPPlus.Fonts.OpenType.Tables.Post;
30+
using EPPlus.Fonts.OpenType.Utils;
3031
using System;
3132
using System.Collections.Generic;
3233
using System.Linq;
@@ -512,19 +513,16 @@ public OpenTypeFont CreateSubset(IEnumerable<char> usedChars)
512513
if (usedChars == null)
513514
throw new ArgumentNullException(nameof(usedChars));
514515

515-
if (usedChars.Count() == 0)
516+
var charArray = usedChars.ToArray();
517+
if (charArray.Length == 0)
516518
throw new ArgumentException("Text cannot be empty", nameof(usedChars));
517519

518520
var subsetBuilder = new SubsetFontBuilder();
519521

520-
// Konvertera chars till Unicode code points
521-
var codePoints = usedChars
522-
.Select(c => (uint)c) // tvinga unsigned
523-
.Distinct()
524-
.Where(cp => cp <= 0x10FFFF) // validering
525-
.Select(cp => (int)cp);
522+
// Convert chars to Unicode code points (handling surrogate pairs)
523+
var codePoints = CharacterUtil.ExtractCodePointsFromChars(charArray);
526524

527-
// Skapa subset-font
525+
// Create subset font
528526
var newFont = subsetBuilder.CreateSubset(this, codePoints);
529527

530528
var postProcessor = new SubsetPostProcessor();
@@ -533,40 +531,6 @@ public OpenTypeFont CreateSubset(IEnumerable<char> usedChars)
533531
return newFont;
534532
}
535533

536-
public OpenTypeFont CreateSubset_Old(IEnumerable<char> usedChars)
537-
{
538-
// 1. Map chars to glyph IDs
539-
var glyphIds = new HashSet<ushort>();
540-
foreach (var ch in usedChars)
541-
{
542-
var glyphId = CmapTable.MapCharToGlyph(ch);
543-
if (glyphId >= 0)
544-
glyphIds.Add((ushort)glyphId);
545-
}
546-
glyphIds.Add(0); // Always include .notdef
547-
548-
// 2. Handle composite glyphs
549-
GlyfTable.ResolveCompositeGlyphs(glyphIds);
550-
551-
// 3. Create new font instance
552-
var subsetFont = new OpenTypeFont(_fontBytes, Format);
553-
554-
// 4. Copy and filter tables
555-
subsetFont.AddOrReplaceTable(HeadTable.Clone());
556-
subsetFont.AddOrReplaceTable(MaxpTable.Clone());
557-
subsetFont.MaxpTable.numGlyphs = (ushort)glyphIds.Count;
558-
559-
//subsetFont.ReplaceTable(TableNames.Glyf, GlyfTable.CreateSubset(glyphIds));
560-
//subsetFont.LocaTable = this.LocaTable.CreateSubset(glyphIds);
561-
//subsetFont.HmtxTable = this.HmtxTable.CreateSubset(glyphIds);
562-
//subsetFont.CmapTable = this.CmapTable.CreateSubset(usedChars);
563-
564-
//// 5. Recalculate checksums
565-
//subsetFont.RecalculateChecksums();
566-
567-
return subsetFont;
568-
}
569-
570534
public static bool TryParseEnum<T>(string value, out T result) where T : struct
571535
{
572536
try
@@ -599,7 +563,7 @@ public static bool TryParseEnum<T>(string value, out T result) where T : struct
599563
/// For subset fonts: Maps original glyph IDs to new subset glyph IDs.
600564
/// Null for non-subset fonts.
601565
/// </summary>
602-
//public Dictionary<ushort, ushort> SubsetGlyphMapping { get; internal set; }
566+
public Dictionary<ushort, ushort> SubsetGlyphMapping { get; internal set; }
603567

604568

605569
/// <summary>

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public void Rewrite(FontSubsettingContext context)
5555
// Build mapping: Unicode code point → NEW glyph ID in subset
5656
Dictionary<uint, ushort> cmapMapping = new Dictionary<uint, ushort>();
5757

58+
5859
foreach (uint codePoint in context.UsedCodePoints)
5960
{
6061
ushort oldGid;
@@ -73,6 +74,18 @@ public void Rewrite(FontSubsettingContext context)
7374
}
7475
}
7576

77+
// DEBUG: Visa vad som faktiskt läggs in
78+
Console.WriteLine("=== cmapMapping innehåll ===");
79+
Console.WriteLine($"'T' (84) i cmapMapping: {(cmapMapping.ContainsKey(84) ? cmapMapping[84].ToString() : "SAKNAS")}");
80+
Console.WriteLine($"'A' (65) i cmapMapping: {(cmapMapping.ContainsKey(65) ? cmapMapping[65].ToString() : "SAKNAS")}");
81+
82+
// Visa också OldToNewGlyphId för 'T'
83+
ushort tOldGid;
84+
context.OriginalFont.CmapTable.TryGetGlyphId(84, out tOldGid);
85+
Console.WriteLine($"'T' OldGID: {tOldGid}");
86+
Console.WriteLine($"OldToNewGlyphId[{tOldGid}]: {(context.OldToNewGlyphId.ContainsKey(tOldGid) ? context.OldToNewGlyphId[tOldGid].ToString() : "SAKNAS")}");
87+
88+
7689
// Always map code point 0 to .notdef (required by spec)
7790
cmapMapping[0] = 0;
7891

@@ -104,6 +117,12 @@ public void Rewrite(FontSubsettingContext context)
104117
// Replace cmap in subset font.
105118
// Now GetMinCharCode() will return the correct value to the validator.
106119
context.SubsetFont.AddOrReplaceTable(newCmap);
120+
121+
// DEBUG: Verifiera direkt efter att tabellen lagts till
122+
Console.WriteLine("=== CmapSubsetProcessor.Rewrite DONE ===");
123+
Console.WriteLine($" SubsetFont hash: {context.SubsetFont.GetHashCode()}");
124+
Console.WriteLine($" CmapTable hash: {context.SubsetFont.CmapTable.GetHashCode()}");
125+
Console.WriteLine($" MapCharToGlyph('T'): {context.SubsetFont.CmapTable.MapCharToGlyph('T')}");
107126
}
108127
}
109128
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ public void Rewrite(FontSubsettingContext context)
8888
: HeadTable.IndexToLocFormats.Offset32;
8989

9090
context.SubsetFont.AddOrReplaceTable(LocaTable.CreateSubset(offsets, context.SubsetFont.HeadTable.IndexToLocFormat));
91+
Console.WriteLine($"=== GlyfAndLocaSubsetProcessor ===");
92+
Console.WriteLine($"NewToOldGlyphId count: {context.NewToOldGlyphId.Count}");
93+
Console.WriteLine($"NewGID 42 → OldGID: {context.NewToOldGlyphId[42]}");
9194
}
9295

9396
private static bool IsEmpty(Glyph g)

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ Date Author Change
1111
01/12/2026 EPPlus Software AB GPOS subset processor
1212
*************************************************************************************************/
1313
using EPPlus.Fonts.OpenType.Tables.Common.Layout.Lookups;
14+
using EPPlus.Fonts.OpenType.Tables.Gpos.Data.Lookups.LookupType4;
1415
using EPPlus.Fonts.OpenType.Tables.Gpos.Handlers;
1516
using System.Collections.Generic;
17+
using System.Diagnostics;
1618
using System.Linq;
1719

1820
namespace EPPlus.Fonts.OpenType.Subsetting
@@ -30,8 +32,9 @@ public GposSubsetProcessor()
3032
var handlers = new IGposLookupHandler[]
3133
{
3234
new SinglePosHandler(), // Type 1
33-
new PairPosHandler(), // Type 2
34-
new MarkToBaseHandler() // Type 4
35+
new PairPosHandler(), // Type 2
36+
new MarkToBaseHandler(), // Type 4
37+
new ExtensionPosHandler() // Type 9
3538
};
3639
_handlers = handlers.ToDictionary(h => h.LookupType);
3740
}
@@ -74,11 +77,26 @@ public void Rewrite(FontSubsettingContext context)
7477
var oldGpos = context.OriginalFont.GposTable;
7578
if (oldGpos == null) return;
7679

77-
// Let GposTable handle the rewriting
80+
Debug.WriteLine($"[GPOS Subset] Original GPOS has {oldGpos.LookupList.Lookups.Count} lookups");
81+
82+
// Count Type 4 (MarkToBase) lookups
83+
int markToBaseCount = 0;
84+
foreach (var lookup in oldGpos.LookupList.Lookups)
85+
{
86+
if (lookup.LookupType == 4 ||
87+
(lookup.LookupType == 9 && lookup.SubTables.Count > 0 &&
88+
lookup.SubTables[0] is MarkToBaseSubTableFormat1))
89+
{
90+
markToBaseCount++;
91+
}
92+
}
93+
Debug.WriteLine($"[GPOS Subset] Original has {markToBaseCount} MarkToBase lookups");
94+
7895
var newGpos = oldGpos.Rewrite(context);
7996

8097
if (newGpos != null)
8198
{
99+
Debug.WriteLine($"[GPOS Subset] New GPOS has {newGpos.LookupList.Lookups.Count} lookups");
82100
context.SubsetFont.AddOrReplaceTable(newGpos);
83101
}
84102
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public OpenTypeFont CreateSubset(OpenTypeFont originalFont, IEnumerable<int> uni
6969

7070
context.SubsetFont.UsedCodePointsForSubset = new List<uint>(context.UsedCodePoints);
7171

72-
//context.SubsetFont.SubsetGlyphMapping = new Dictionary<ushort, ushort>(context.OldToNewGlyphId);
72+
context.SubsetFont.SubsetGlyphMapping = new Dictionary<ushort, ushort>(context.OldToNewGlyphId);
7373

7474
return context.SubsetFont;
7575
}
@@ -80,13 +80,16 @@ private void BuildGlyphMapping(FontSubsettingContext context)
8080
var sortedGlyphs = new List<ushort>(context.IncludedGlyphs);
8181
sortedGlyphs.Sort();
8282

83+
Console.WriteLine($"=== BuildGlyphMapping: {sortedGlyphs.Count} glyphs ===");
8384

8485
for (ushort newId = 0; newId < sortedGlyphs.Count; newId++)
8586
{
8687
ushort oldId = sortedGlyphs[newId];
8788
context.OldToNewGlyphId[oldId] = newId;
8889
context.NewToOldGlyphId.Add(oldId);
8990

91+
if (newId < 20) // Visa första 20
92+
Console.WriteLine($" OldGID:{oldId} → NewGID:{newId}");
9093
}
9194
}
9295
}

src/EPPlus.Fonts.OpenType/Tables/Gpos/GposTableLoader.cs

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Date Author Change
1818
using EPPlus.Fonts.OpenType.Tables.Gpos.Data.Lookups.LookupType4;
1919
using System;
2020
using System.Collections.Generic;
21+
using System.Diagnostics;
2122

2223
namespace EPPlus.Fonts.OpenType.Tables.Gpos
2324
{
@@ -114,7 +115,6 @@ private LookupListTable ReadLookupList(FontsBinaryReader reader, long lookupList
114115
private LookupTable ReadLookup(FontsBinaryReader reader, long lookupStart)
115116
{
116117
var lookup = new LookupTable();
117-
118118
lookup.LookupType = reader.ReadUInt16BigEndian();
119119
lookup.LookupFlag = reader.ReadUInt16BigEndian();
120120
ushort subTableCount = reader.ReadUInt16BigEndian();
@@ -132,14 +132,15 @@ private LookupTable ReadLookup(FontsBinaryReader reader, long lookupStart)
132132
}
133133

134134
lookup.SubTables = new List<FontTableElement>();
135-
136135
for (int i = 0; i < subTableCount; i++)
137136
{
138137
if (subTableOffsets[i] == 0)
138+
{
139139
continue;
140+
}
140141

141142
reader.BaseStream.Position = lookupStart + subTableOffsets[i];
142-
var subTable = ReadSubTable(reader, lookup.LookupType, lookupStart + subTableOffsets[i]);
143+
var subTable = ReadSubTable(reader, lookup.LookupType, lookupStart + subTableOffsets[i]);
143144

144145
if (subTable != null)
145146
{
@@ -194,7 +195,7 @@ private FontTableElement ReadSubTable(FontsBinaryReader reader, ushort lookupTyp
194195
case 9:
195196
// Extension positioning
196197
// TODO: Implement when needed
197-
return null;
198+
return ReadExtensionPosSubTable(reader, subtableStart);
198199

199200
default:
200201
// Unknown lookup type - skip
@@ -204,14 +205,19 @@ private FontTableElement ReadSubTable(FontsBinaryReader reader, ushort lookupTyp
204205

205206
private FontTableElement ReadPairPosSubTable(FontsBinaryReader reader, long subtableStart)
206207
{
208+
// ✅ FIX: Sätt position först!
209+
reader.BaseStream.Position = subtableStart;
210+
207211
ushort posFormat = reader.ReadUInt16BigEndian();
208212

209213
if (posFormat == 1)
210214
{
215+
reader.BaseStream.Position = subtableStart; // Reset för deserializer
211216
return ReadPairPosFormat1(reader, subtableStart);
212217
}
213218
else if (posFormat == 2)
214219
{
220+
reader.BaseStream.Position = subtableStart; // Reset för deserializer
215221
return ReadPairPosFormat2(reader, subtableStart);
216222
}
217223
else
@@ -236,10 +242,14 @@ private PairPosSubTableFormat2 ReadPairPosFormat2(FontsBinaryReader reader, long
236242

237243
private FontTableElement ReadSinglePosSubTable(FontsBinaryReader reader, long subtableStart)
238244
{
245+
// ✅ FIX: Sätt position FÖRST
246+
reader.BaseStream.Position = subtableStart;
247+
239248
// Read format to determine which deserializer to use
240-
long savedPos = reader.BaseStream.Position;
241249
ushort posFormat = reader.ReadUInt16BigEndian();
242-
reader.BaseStream.Position = savedPos; // Reset for deserializer
250+
251+
// Reset for deserializer
252+
reader.BaseStream.Position = subtableStart;
243253

244254
if (posFormat == 1)
245255
{
@@ -259,21 +269,52 @@ private FontTableElement ReadSinglePosSubTable(FontsBinaryReader reader, long su
259269

260270
private FontTableElement ReadMarkToBaseSubTable(FontsBinaryReader reader, long subtableStart)
261271
{
262-
// Read format to verify it's Format 1 (only format for MarkToBase)
263-
long savedPos = reader.BaseStream.Position;
272+
// ✅ FIX: Sätt position INNAN du läser format
273+
reader.BaseStream.Position = subtableStart;
274+
264275
ushort posFormat = reader.ReadUInt16BigEndian();
265-
reader.BaseStream.Position = savedPos; // Reset for deserializer
276+
277+
// Reset to start for deserializer
278+
reader.BaseStream.Position = subtableStart;
266279

267280
if (posFormat == 1)
268281
{
269282
var deserializer = new MarkToBaseSubTableFormat1Deserializer(reader);
270-
return deserializer.Deserialize(subtableStart);
283+
var result = deserializer.Deserialize(subtableStart);
284+
return result;
271285
}
272286
else
273287
{
274-
// Unknown format - skip silently
275288
return null;
276289
}
277290
}
291+
292+
private FontTableElement ReadExtensionPosSubTable(FontsBinaryReader reader, long subtableStart)
293+
{
294+
reader.BaseStream.Position = subtableStart;
295+
296+
ushort posFormat = reader.ReadUInt16BigEndian();
297+
298+
if (posFormat != 1)
299+
{
300+
return null;
301+
}
302+
303+
ushort extensionLookupType = reader.ReadUInt16BigEndian();
304+
uint extensionOffset = reader.ReadUInt32BigEndian();
305+
306+
307+
// ✅ CRITICAL: Prevent infinite recursion!
308+
if (extensionLookupType == 9)
309+
{
310+
return null;
311+
}
312+
313+
long wrappedSubtableStart = subtableStart + extensionOffset;
314+
315+
var result = ReadSubTable(reader, extensionLookupType, wrappedSubtableStart);
316+
317+
return result;
318+
}
278319
}
279320
}

0 commit comments

Comments
 (0)