pagyra-js 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,71 +1,90 @@
1
1
  import { createToUnicodeCMapText } from "./to-unicode.js";
2
+ import { BinaryWriter } from "./binary-writer.js";
3
+ import { LocaTableReader } from "./loca-reader.js";
4
+ import { DefaultDataViewReader } from "./ttf-table-provider.js";
2
5
  /**
3
6
  * Creates a PDF font subset for TTF fonts.
4
7
  */
5
8
  export function createPdfFontSubset(options) {
6
9
  const { baseName, fontMetrics, fontProgram, usedGlyphIds } = options;
7
10
  const encoding = options.encoding ?? "identity";
8
- const glyphIds = Array.from(usedGlyphIds).sort((a, b) => a - b);
9
- if (!glyphIds.includes(0)) {
10
- glyphIds.unshift(0);
11
+ // 1. Compute closure of used glyphs (checking composite glyphs)
12
+ const initialGids = Array.from(usedGlyphIds);
13
+ if (!initialGids.includes(0)) {
14
+ initialGids.push(0); // Always include .notdef
11
15
  }
12
- const gidToCharCode = new Map();
13
- if (encoding === "identity") {
14
- for (const gid of glyphIds) {
15
- gidToCharCode.set(gid, gid);
16
- }
17
- }
18
- else {
19
- for (let i = 0; i < glyphIds.length; i++) {
20
- gidToCharCode.set(glyphIds[i], i);
21
- }
22
- }
23
- const firstChar = encoding === "identity" ? Math.min(...glyphIds) : 0;
24
- const lastChar = encoding === "identity" ? Math.max(...glyphIds) : glyphIds.length - 1;
16
+ const { glyphIds, gidMap } = computeSubsetClosure(fontProgram, initialGids);
25
17
  const unitsPerEm = fontMetrics.metrics.unitsPerEm;
26
18
  const widths = [];
27
- if (encoding === "identity") {
19
+ const usedGidSet = new Set(glyphIds);
20
+ let firstChar;
21
+ let lastChar;
22
+ if (encoding === "sequential") {
23
+ firstChar = 0;
24
+ lastChar = glyphIds.length - 1;
28
25
  for (const gid of glyphIds) {
29
26
  const glyphMetric = fontMetrics.glyphMetrics.get(gid);
30
27
  const advanceWidth = glyphMetric?.advanceWidth ?? 0;
31
- const pdfWidth = Math.round((advanceWidth / unitsPerEm) * 1000);
32
- widths.push(pdfWidth);
28
+ widths.push(Math.round((advanceWidth / unitsPerEm) * 1000));
33
29
  }
34
30
  }
35
31
  else {
36
- for (const gid of glyphIds) {
37
- const glyphMetric = fontMetrics.glyphMetrics.get(gid);
32
+ // Identity: CIDs = Original GIDs.
33
+ // We use a sparse range from 0 to maxGid to ensure stability.
34
+ const maxGid = Math.max(...glyphIds);
35
+ firstChar = 0;
36
+ lastChar = maxGid;
37
+ for (let i = 0; i <= maxGid; i++) {
38
+ const glyphMetric = fontMetrics.glyphMetrics.get(i);
38
39
  const advanceWidth = glyphMetric?.advanceWidth ?? 0;
39
- const pdfWidth = Math.round((advanceWidth / unitsPerEm) * 1000);
40
- widths.push(pdfWidth);
40
+ widths.push(Math.round((advanceWidth / unitsPerEm) * 1000));
41
41
  }
42
42
  }
43
+ // Generate ToUnicode CMap
44
+ // we map CID -> Unicode.
43
45
  const cmapEntries = [];
44
46
  const unicodeMap = fontMetrics.cmap.unicodeMap;
45
- for (const gid of glyphIds) {
46
- const unicodes = [];
47
- for (const [cp, mappedGid] of unicodeMap.entries()) {
48
- if (mappedGid === gid) {
49
- unicodes.push(cp);
47
+ const gidToUnicodes = new Map();
48
+ for (const [cp, mappedGid] of unicodeMap.entries()) {
49
+ const list = gidToUnicodes.get(mappedGid) || [];
50
+ list.push(cp);
51
+ gidToUnicodes.set(mappedGid, list);
52
+ }
53
+ if (encoding === "sequential") {
54
+ for (let i = 0; i < glyphIds.length; i++) {
55
+ const originalGid = glyphIds[i];
56
+ const unicodes = gidToUnicodes.get(originalGid);
57
+ if (unicodes) {
58
+ for (const u of unicodes) {
59
+ cmapEntries.push({ gid: i, unicode: u });
60
+ }
50
61
  }
51
62
  }
52
- if (unicodes.length > 0) {
53
- const cid = gidToCharCode.get(gid);
54
- if (cid !== undefined) {
55
- for (const unicode of unicodes) {
56
- cmapEntries.push({ gid: cid, unicode });
63
+ }
64
+ else {
65
+ // Identity: CID = Original GID
66
+ for (const gid of glyphIds) {
67
+ const unicodes = gidToUnicodes.get(gid);
68
+ if (unicodes) {
69
+ for (const u of unicodes) {
70
+ cmapEntries.push({ gid: gid, unicode: u });
57
71
  }
58
72
  }
59
73
  }
60
74
  }
61
75
  const toUnicodeCMap = createToUnicodeCMapText(cmapEntries);
62
- const fontFile = extractFontFile(fontProgram);
76
+ // Generate the binary TTF
77
+ const fontFile = subsetFont(fontProgram, glyphIds, encoding);
63
78
  const encodeGlyph = (gid) => {
64
- const charCode = gidToCharCode.get(gid);
65
- if (charCode === undefined) {
66
- throw new Error(`Glyph ID ${gid} not found in subset`);
79
+ if (encoding === "sequential") {
80
+ const index = gidMap.get(gid);
81
+ if (index === undefined)
82
+ throw new Error(`Glyph ID ${gid} not found in subset`);
83
+ return index;
84
+ }
85
+ else {
86
+ return gid;
67
87
  }
68
- return charCode;
69
88
  };
70
89
  return {
71
90
  name: `/${baseName}`,
@@ -76,113 +95,299 @@ export function createPdfFontSubset(options) {
76
95
  fontFile,
77
96
  encodeGlyph,
78
97
  glyphIds,
98
+ gidMap
79
99
  };
80
100
  }
81
- /**
82
- * Extracts the font file from FontProgram.
83
- * Builds a complete TTF with all available tables.
84
- */
85
- function extractFontFile(fontProgram) {
86
- if (fontProgram.getRawTableData) {
87
- return buildTtfFromTables(fontProgram);
101
+ function computeSubsetClosure(fontProgram, initialGids) {
102
+ const reader = new DefaultDataViewReader();
103
+ const glyfData = fontProgram.getRawTableData?.("glyf");
104
+ const locaData = fontProgram.getRawTableData?.("loca");
105
+ const headData = fontProgram.getRawTableData?.("head");
106
+ const used = new Set(initialGids);
107
+ const stack = [...initialGids];
108
+ if (!glyfData || !locaData || !headData) {
109
+ const sorted = Array.from(used).sort((a, b) => a - b);
110
+ const map = new Map();
111
+ sorted.forEach((gid, i) => map.set(gid, i));
112
+ return { glyphIds: sorted, gidMap: map };
113
+ }
114
+ const headView = new DataView(headData.buffer, headData.byteOffset, headData.byteLength);
115
+ const locaView = new DataView(locaData.buffer, locaData.byteOffset, locaData.byteLength);
116
+ const glyfView = new DataView(glyfData.buffer, glyfData.byteOffset, glyfData.byteLength);
117
+ const indexToLocFormat = reader.getUint16(headView, 50);
118
+ const locaReader = new LocaTableReader(locaView, indexToLocFormat, reader);
119
+ while (stack.length > 0) {
120
+ const gid = stack.pop();
121
+ const offset = locaReader.getGlyphOffset(gid);
122
+ if (!offset || offset.start === offset.end)
123
+ continue;
124
+ if (offset.start + 2 > glyfView.byteLength)
125
+ continue;
126
+ const numberOfContours = reader.getInt16(glyfView, offset.start);
127
+ if (numberOfContours < 0) {
128
+ let pos = offset.start + 10;
129
+ let flags;
130
+ do {
131
+ if (pos + 4 > glyfView.byteLength)
132
+ break;
133
+ flags = reader.getUint16(glyfView, pos);
134
+ const componentGid = reader.getUint16(glyfView, pos + 2);
135
+ if (!used.has(componentGid)) {
136
+ used.add(componentGid);
137
+ stack.push(componentGid);
138
+ }
139
+ pos += 4;
140
+ const arg1And2AreWords = (flags & 0x0001) !== 0;
141
+ if (arg1And2AreWords)
142
+ pos += 4;
143
+ else
144
+ pos += 2;
145
+ const weHaveAScale = (flags & 0x0008) !== 0;
146
+ const weHaveAnXAndYScale = (flags & 0x0040) !== 0;
147
+ const weHaveATwoByTwo = (flags & 0x0080) !== 0;
148
+ if (weHaveAScale)
149
+ pos += 2;
150
+ else if (weHaveAnXAndYScale)
151
+ pos += 4;
152
+ else if (weHaveATwoByTwo)
153
+ pos += 8;
154
+ } while (flags & 0x0020);
155
+ }
88
156
  }
89
- console.warn("Font subsetting not fully implemented; no raw table data available");
90
- return new Uint8Array(0);
157
+ const sorted = Array.from(used).sort((a, b) => a - b);
158
+ const map = new Map();
159
+ sorted.forEach((gid, i) => map.set(gid, i));
160
+ return { glyphIds: sorted, gidMap: map };
91
161
  }
92
- /**
93
- * Builds a complete TTF file from available tables in FontProgram.
94
- */
95
- function buildTtfFromTables(fontProgram) {
96
- const tableTags = [
97
- 'head', 'hhea', 'maxp', 'OS/2', 'hmtx', 'cmap',
98
- 'loca', 'glyf', 'name', 'post', 'cvt ', 'fpgm', 'prep'
99
- ];
100
- const tables = new Map();
101
- for (const tag of tableTags) {
102
- const data = fontProgram.getRawTableData(tag);
103
- if (data && data.length > 0) {
104
- tables.set(tag, data);
162
+ function subsetFont(fontProgram, glyphIds, encoding) {
163
+ const reader = new DefaultDataViewReader();
164
+ const getTable = (tag) => fontProgram.getRawTableData?.(tag);
165
+ const head = getTable("head");
166
+ const hhea = getTable("hhea");
167
+ const maxp = getTable("maxp");
168
+ const hmtx = getTable("hmtx");
169
+ const loca = getTable("loca");
170
+ const glyf = getTable("glyf");
171
+ const OS2 = getTable("OS/2");
172
+ if (!head || !hhea || !maxp || !hmtx || !loca || !glyf) {
173
+ throw new Error("Missing required tables for subsetting");
174
+ }
175
+ const headView = new DataView(head.buffer, head.byteOffset, head.byteLength);
176
+ const locaView = new DataView(loca.buffer, loca.byteOffset, loca.byteLength);
177
+ const glyfView = new DataView(glyf.buffer, glyf.byteOffset, glyf.byteLength);
178
+ const hmtxView = new DataView(hmtx.buffer, hmtx.byteOffset, hmtx.byteLength);
179
+ const hheaView = new DataView(hhea.buffer, hhea.byteOffset, hhea.byteLength);
180
+ const indexToLocFormat = reader.getUint16(headView, 50);
181
+ const locaReader = new LocaTableReader(locaView, indexToLocFormat, reader);
182
+ const numberOfHMetrics = reader.getUint16(hheaView, 34);
183
+ const newLoca = new BinaryWriter();
184
+ const newGlyf = new BinaryWriter();
185
+ const usedGidSet = new Set(glyphIds);
186
+ let maxGidToEmit;
187
+ if (encoding === "sequential") {
188
+ maxGidToEmit = glyphIds.length - 1;
189
+ const gidMap = new Map();
190
+ glyphIds.forEach((gid, i) => gidMap.set(gid, i));
191
+ for (const originalGid of glyphIds) {
192
+ newLoca.writeUint32(newGlyf.byteLength());
193
+ const range = locaReader.getGlyphOffset(originalGid);
194
+ if (range && range.end > range.start) {
195
+ const glyphData = new Uint8Array(glyf.buffer, glyf.byteOffset + range.start, range.end - range.start);
196
+ const numberOfContours = reader.getInt16(new DataView(glyphData.buffer, glyphData.byteOffset, glyphData.byteLength), 0);
197
+ if (numberOfContours < 0) {
198
+ const newGlyphData = new Uint8Array(glyphData);
199
+ const view = new DataView(newGlyphData.buffer);
200
+ let pos = 10;
201
+ let flags;
202
+ do {
203
+ flags = reader.getUint16(view, pos);
204
+ const oldCompGid = reader.getUint16(view, pos + 2);
205
+ view.setUint16(pos + 2, gidMap.get(oldCompGid) ?? 0, false);
206
+ pos += 4;
207
+ const arg1And2AreWords = (flags & 0x0001) !== 0;
208
+ if (arg1And2AreWords)
209
+ pos += 4;
210
+ else
211
+ pos += 2;
212
+ const weHaveAScale = (flags & 0x0008) !== 0;
213
+ const weHaveAnXAndYScale = (flags & 0x0040) !== 0;
214
+ const weHaveATwoByTwo = (flags & 0x0080) !== 0;
215
+ if (weHaveAScale)
216
+ pos += 2;
217
+ else if (weHaveAnXAndYScale)
218
+ pos += 4;
219
+ else if (weHaveATwoByTwo)
220
+ pos += 8;
221
+ } while (flags & 0x0020);
222
+ newGlyf.writeBytes(newGlyphData);
223
+ }
224
+ else {
225
+ newGlyf.writeBytes(glyphData);
226
+ }
227
+ }
228
+ }
229
+ }
230
+ else {
231
+ // Identity Mode: Sparse TTF
232
+ maxGidToEmit = Math.max(...glyphIds);
233
+ for (let gid = 0; gid <= maxGidToEmit; gid++) {
234
+ newLoca.writeUint32(newGlyf.byteLength());
235
+ if (usedGidSet.has(gid)) {
236
+ const range = locaReader.getGlyphOffset(gid);
237
+ if (range && range.end > range.start) {
238
+ const glyphData = new Uint8Array(glyf.buffer, glyf.byteOffset + range.start, range.end - range.start);
239
+ newGlyf.writeBytes(glyphData);
240
+ }
241
+ }
242
+ }
243
+ }
244
+ newLoca.writeUint32(newGlyf.byteLength());
245
+ // 2. MAXP
246
+ const newMaxp = new BinaryWriter();
247
+ newMaxp.writeBytes(new Uint8Array(maxp.buffer, maxp.byteOffset, 6));
248
+ const maxpView = new DataView(newMaxp.getData().buffer);
249
+ maxpView.setUint16(4, maxGidToEmit + 1, false);
250
+ if (maxp.byteLength > 6) {
251
+ newMaxp.writeBytes(new Uint8Array(maxp.buffer, maxp.byteOffset + 6, Math.min(maxp.byteLength - 6, 26)));
252
+ }
253
+ // 3. HMTX
254
+ const newHmtx = new BinaryWriter();
255
+ if (encoding === "sequential") {
256
+ for (const originalGid of glyphIds) {
257
+ let advance = 0, lsb = 0;
258
+ if (originalGid < numberOfHMetrics) {
259
+ advance = reader.getUint16(hmtxView, originalGid * 4);
260
+ lsb = reader.getInt16(hmtxView, originalGid * 4 + 2);
261
+ }
262
+ else {
263
+ advance = reader.getUint16(hmtxView, (numberOfHMetrics - 1) * 4);
264
+ const lsbOffset = numberOfHMetrics * 4 + (originalGid - numberOfHMetrics) * 2;
265
+ if (lsbOffset + 2 <= hmtxView.byteLength)
266
+ lsb = reader.getInt16(hmtxView, lsbOffset);
267
+ }
268
+ newHmtx.writeUint16(advance);
269
+ newHmtx.writeInt16(lsb);
105
270
  }
106
271
  }
107
- if (tables.size === 0) {
108
- console.warn("No TTF tables available for font subsetting");
109
- return new Uint8Array(0);
272
+ else {
273
+ for (let gid = 0; gid <= maxGidToEmit; gid++) {
274
+ let advance = 0, lsb = 0;
275
+ if (gid < numberOfHMetrics) {
276
+ advance = reader.getUint16(hmtxView, gid * 4);
277
+ lsb = reader.getInt16(hmtxView, gid * 4 + 2);
278
+ }
279
+ else {
280
+ advance = reader.getUint16(hmtxView, (numberOfHMetrics - 1) * 4);
281
+ const lsbOffset = numberOfHMetrics * 4 + (gid - numberOfHMetrics) * 2;
282
+ if (lsbOffset + 2 <= hmtxView.byteLength)
283
+ lsb = reader.getInt16(hmtxView, lsbOffset);
284
+ }
285
+ newHmtx.writeUint16(advance);
286
+ newHmtx.writeInt16(lsb);
287
+ }
110
288
  }
111
- const numTables = tables.size;
289
+ // 4. HHEA
290
+ const newHhea = new BinaryWriter();
291
+ newHhea.writeBytes(new Uint8Array(hhea));
292
+ const newHheaView = new DataView(newHhea.getData().buffer);
293
+ newHheaView.setUint16(34, maxGidToEmit + 1, false);
294
+ // 5. CMAP (Minimal Dummy)
295
+ const newCmap = new BinaryWriter();
296
+ newCmap.writeUint16(0);
297
+ newCmap.writeUint16(1);
298
+ newCmap.writeUint16(3);
299
+ newCmap.writeUint16(1);
300
+ newCmap.writeUint32(12);
301
+ const startCmap = newCmap.byteLength();
302
+ newCmap.writeUint16(4);
303
+ newCmap.writeUint16(0);
304
+ newCmap.writeUint16(0);
305
+ newCmap.writeUint16(0);
306
+ newCmap.writeUint16(0);
307
+ newCmap.writeUint16(0);
308
+ newCmap.writeUint16(0);
309
+ newCmap.writeUint16(0xFFFF);
310
+ newCmap.writeUint16(0);
311
+ newCmap.writeUint16(0);
312
+ newCmap.writeUint16(0);
313
+ newCmap.writeUint16(0);
314
+ const endCmap = newCmap.byteLength();
315
+ const cmapView = new DataView(newCmap.getData().buffer);
316
+ cmapView.setUint16(startCmap + 2, endCmap - startCmap, false);
317
+ // 6. HEAD
318
+ const newHead = new BinaryWriter();
319
+ newHead.writeBytes(new Uint8Array(head));
320
+ const newHeadView = new DataView(newHead.getData().buffer);
321
+ newHeadView.setUint16(50, 1, false);
322
+ newHeadView.setUint32(8, 0, false);
323
+ const tables = [
324
+ ["head", newHead.getData()],
325
+ ["hhea", newHhea.getData()],
326
+ ["maxp", newMaxp.getData()],
327
+ ["hmtx", newHmtx.getData()],
328
+ ["loca", newLoca.getData()],
329
+ ["glyf", newGlyf.getData()],
330
+ ["cmap", newCmap.getData()]
331
+ ];
332
+ if (OS2)
333
+ tables.push(["OS/2", new Uint8Array(OS2)]);
334
+ tables.sort((a, b) => a[0].localeCompare(b[0]));
335
+ const numTables = tables.length;
112
336
  const searchRange = Math.pow(2, Math.floor(Math.log2(numTables))) * 16;
113
337
  const entrySelector = Math.floor(Math.log2(numTables));
114
338
  const rangeShift = numTables * 16 - searchRange;
115
- const headerSize = 12;
116
- const directorySize = numTables * 16;
117
- let dataSize = 0;
118
- for (const data of tables.values()) {
119
- dataSize += data.length + (4 - (data.length % 4)) % 4;
120
- }
121
- const totalSize = headerSize + directorySize + dataSize;
122
- const buffer = new ArrayBuffer(totalSize);
123
- const view = new DataView(buffer);
124
- const bytes = new Uint8Array(buffer);
125
- view.setUint32(0, 0x00010000, false);
126
- view.setUint16(4, numTables, false);
127
- view.setUint16(6, searchRange, false);
128
- view.setUint16(8, entrySelector, false);
129
- view.setUint16(10, rangeShift, false);
130
- let directoryOffset = 12;
131
- let dataOffset = headerSize + directorySize;
339
+ const fullTtf = new BinaryWriter();
340
+ fullTtf.writeUint32(0x00010000);
341
+ fullTtf.writeUint16(numTables);
342
+ fullTtf.writeUint16(searchRange);
343
+ fullTtf.writeUint16(entrySelector);
344
+ fullTtf.writeUint16(rangeShift);
345
+ let offset = 12 + numTables * 16;
132
346
  for (const [tag, data] of tables) {
133
- const tagBytes = new TextEncoder().encode(tag.padEnd(4));
134
- bytes.set(tagBytes.slice(0, 4), directoryOffset);
135
- const checksum = calculateTableChecksum(data);
136
- view.setUint32(directoryOffset + 4, checksum, false);
137
- view.setUint32(directoryOffset + 8, dataOffset, false);
138
- view.setUint32(directoryOffset + 12, data.length, false);
139
- bytes.set(data, dataOffset);
140
- const paddedLength = data.length + (4 - (data.length % 4)) % 4;
141
- directoryOffset += 16;
142
- dataOffset += paddedLength;
143
- }
144
- updateHeadChecksum(bytes, view, tables);
145
- return bytes;
347
+ fullTtf.writeBytes(new TextEncoder().encode(tag.padEnd(4)).slice(0, 4));
348
+ fullTtf.writeUint32(calculateChecksum(data));
349
+ fullTtf.writeUint32(offset);
350
+ fullTtf.writeUint32(data.length);
351
+ let paddedLength = data.length;
352
+ if (data.length % 4 !== 0)
353
+ paddedLength += 4 - (data.length % 4);
354
+ offset += paddedLength;
355
+ }
356
+ for (const [_, data] of tables) {
357
+ fullTtf.writeBytes(data);
358
+ const padding = (4 - (data.length % 4)) % 4;
359
+ for (let k = 0; k < padding; k++)
360
+ fullTtf.writeUint8(0);
361
+ }
362
+ const ttfBytes = fullTtf.getData();
363
+ const ttfView = new DataView(ttfBytes.buffer);
364
+ const fileChecksum = calculateChecksum(ttfBytes);
365
+ const adjustment = (0xB1B0AFBA - fileChecksum) >>> 0;
366
+ let headOffset = 0;
367
+ for (let i = 0; i < numTables; i++) {
368
+ const tag = new TextDecoder().decode(ttfBytes.slice(12 + i * 16, 12 + i * 16 + 4));
369
+ if (tag === "head") {
370
+ headOffset = ttfView.getUint32(12 + i * 16 + 8, false);
371
+ break;
372
+ }
373
+ }
374
+ if (headOffset > 0)
375
+ ttfView.setUint32(headOffset + 8, adjustment, false);
376
+ return ttfBytes;
146
377
  }
147
- /**
148
- * Calculate TTF table checksum
149
- */
150
- function calculateTableChecksum(data) {
378
+ function calculateChecksum(data) {
151
379
  let sum = 0;
152
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
153
380
  const nLongs = Math.floor(data.length / 4);
154
- for (let i = 0; i < nLongs; i++) {
381
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
382
+ for (let i = 0; i < nLongs; i++)
155
383
  sum = (sum + view.getUint32(i * 4, false)) >>> 0;
156
- }
157
384
  const remaining = data.length % 4;
158
385
  if (remaining > 0) {
159
386
  let last = 0;
160
- for (let i = 0; i < remaining; i++) {
387
+ for (let i = 0; i < remaining; i++)
161
388
  last = (last << 8) | data[nLongs * 4 + i];
162
- }
163
389
  last = last << ((4 - remaining) * 8);
164
390
  sum = (sum + last) >>> 0;
165
391
  }
166
392
  return sum >>> 0;
167
393
  }
168
- /**
169
- * Update the checkSumAdjustment field in the head table
170
- */
171
- function updateHeadChecksum(bytes, view, tables) {
172
- const numTables = tables.size;
173
- let headOffset = -1;
174
- for (let i = 0; i < numTables; i++) {
175
- const dirOffset = 12 + i * 16;
176
- const tag = new TextDecoder().decode(bytes.slice(dirOffset, dirOffset + 4));
177
- if (tag.trim() === 'head') {
178
- headOffset = view.getUint32(dirOffset + 8, false);
179
- break;
180
- }
181
- }
182
- if (headOffset === -1)
183
- return;
184
- view.setUint32(headOffset + 8, 0, false);
185
- const fileChecksum = calculateTableChecksum(bytes);
186
- const adjustment = (0xB1B0AFBA - fileChecksum) >>> 0;
187
- view.setUint32(headOffset + 8, adjustment, false);
188
- }
@@ -62,12 +62,24 @@ export class SubsetResourceManager {
62
62
  scaleTo1000(metrics.headBBox[3]),
63
63
  ]
64
64
  : [-1000, -1000, 1000, 1000];
65
- const fontFile = font.embedded?.subset ?? subset.fontFile;
65
+ // Ensure we use the subset font file if available, falling back to full font only if subset is empty
66
+ const fontFile = (subset.fontFile && subset.fontFile.length > 0) ? subset.fontFile : (font.embedded?.subset ?? new Uint8Array(0));
66
67
  if (!fontFile || fontFile.length === 0) {
67
68
  log("font", "warn", "missing-font-file-for-subset", { baseFont: font.baseFont, alias: subset.name });
68
69
  return font.ref;
69
70
  }
70
- const { DW, W } = computeWidths(metrics);
71
+ // Use widths from subset if available, otherwise compute from metrics
72
+ let DW = 1000;
73
+ let W = [];
74
+ if (subset.widths && subset.widths.length > 0) {
75
+ // For sequential subsets starting at CID 0
76
+ W = [subset.firstChar, subset.widths];
77
+ }
78
+ else {
79
+ const result = computeWidths(metrics);
80
+ DW = result.DW;
81
+ W = result.W;
82
+ }
71
83
  const subsetTag = subset.name.startsWith("/") ? subset.name.slice(1) : subset.name;
72
84
  const subsetBaseName = `${subsetTag}+${font.baseFont}`;
73
85
  const fontFileRef = this.doc.registerStream(fontFile, {});
@@ -26,7 +26,8 @@ export function parseTtfBuffer(buffer) {
26
26
  const cmap = new CmapParser(parser, cmapTable);
27
27
  const kerning = mergeKerningMaps(parseKerningTable(parser), parseGposKerning(parser));
28
28
  const glyfProvider = createGlyfOutlineProvider(parser);
29
- return new TtfFontMetrics(metrics, glyphMetrics, cmap, headBBox, glyfProvider, kerning);
29
+ const getRawTableData = (tag) => parser.getRawTableDataString(tag);
30
+ return new TtfFontMetrics(metrics, glyphMetrics, cmap, headBBox, glyfProvider, kerning, getRawTableData);
30
31
  }
31
32
  /**
32
33
  * Parse the 'kern' table (format 0) into a nested map of adjustments in font units.
@@ -22,4 +22,5 @@ export declare class TtfTableParser {
22
22
  getInt8(table: DataView, offset: number): number;
23
23
  getUint8(table: DataView, offset: number): number;
24
24
  getUint32(table: DataView, offset: number): number;
25
+ getRawTableDataString(tagStr: string): Uint8Array | null;
25
26
  }
@@ -107,4 +107,11 @@ export class TtfTableParser {
107
107
  throw new Error("Read beyond table bounds (getUint32)");
108
108
  return table.getUint32(offset, false); // big-endian
109
109
  }
110
+ getRawTableDataString(tagStr) {
111
+ const tag = tagStr.split('').reduce((acc, char) => (acc << 8) | char.charCodeAt(0), 0) >>> 0;
112
+ const entry = this.tableDirectory.get(tag);
113
+ if (!entry)
114
+ return null;
115
+ return new Uint8Array(this.dataView.buffer, this.dataView.byteOffset + entry.offset, entry.length);
116
+ }
110
117
  }
@@ -103,22 +103,6 @@ export class TextRenderer {
103
103
  const wordSpacingPx = run.wordSpacing ?? 0;
104
104
  const wordSpacingPt = this.coordinateTransformer.convertPxToPt(wordSpacingPx);
105
105
  const appliedWordSpacing = wordSpacingPx !== 0 && wordSpacingPt !== 0;
106
- // Shadows use the base font resource (non-subset) for compatibility with Identity-H encoding
107
- this.registerFont(font);
108
- if (run.textShadows && run.textShadows.length > 0) {
109
- const shadowCommands = await this.shadowRenderer.render({
110
- run,
111
- font,
112
- encoded,
113
- Tm: run.lineMatrix ?? { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 },
114
- fontSizePt,
115
- fontSizePx: run.fontSize,
116
- wordSpacingPt,
117
- appliedWordSpacing,
118
- fontResourceName: font.resourceName,
119
- });
120
- this.commands.push(...shadowCommands);
121
- }
122
106
  const subsetResource = this.fontRegistry.ensureSubsetForGlyphRun(glyphRun, font);
123
107
  this.registerSubsetFont(subsetResource.alias, subsetResource.ref);
124
108
  const gradientBackground = run.textGradient;
@@ -153,6 +137,22 @@ export class TextRenderer {
153
137
  }
154
138
  }
155
139
  }
140
+ if (run.textShadows && run.textShadows.length > 0) {
141
+ const shadowCommands = await this.shadowRenderer.render({
142
+ run,
143
+ font,
144
+ encoded,
145
+ Tm: run.lineMatrix ?? { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 },
146
+ fontSizePt,
147
+ fontSizePx: run.fontSize,
148
+ wordSpacingPt,
149
+ appliedWordSpacing,
150
+ fontResourceName: font.resourceName,
151
+ subset: subsetResource.subset,
152
+ subsetAlias: subsetResource.alias
153
+ });
154
+ this.commands.push(...shadowCommands);
155
+ }
156
156
  const glyphCommands = drawGlyphRun(glyphRun, subsetResource.subset, 0, 0, fontSizePt, color, this.graphicsStateManager, wordSpacingPt, { tm: textMatrix });
157
157
  this.commands.push(...glyphCommands);
158
158
  if (run.decorations) {
@@ -276,6 +276,7 @@ function buildUnifiedFontFromResource(run, font) {
276
276
  unitsPerEm: metrics.metrics.unitsPerEm,
277
277
  glyphCount: metrics.glyphMetrics.size,
278
278
  getGlyphOutline: metrics.getGlyphOutline,
279
+ getRawTableData: metrics.getRawTableData,
279
280
  },
280
281
  css: {
281
282
  family: run.fontFamily,
@@ -13,6 +13,10 @@ export interface TextShadowRenderContext {
13
13
  wordSpacingPt: number;
14
14
  appliedWordSpacing: boolean;
15
15
  fontResourceName?: string;
16
+ subset?: {
17
+ gidMap: Map<number, number>;
18
+ };
19
+ subsetAlias?: string;
16
20
  }
17
21
  export declare class TextShadowRenderer {
18
22
  private readonly coordinateTransformer;