modern-pdf-lib 0.11.6 → 0.12.1

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.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Create, parse, fill, merge, sign, and manipulate PDF documents<br />in Node, Den
15
15
 
16
16
  [![npm version](https://img.shields.io/npm/v/modern-pdf-lib?style=flat-square&color=cb3837)](https://www.npmjs.com/package/modern-pdf-lib)
17
17
  [![bundle size](https://img.shields.io/badge/gzip-36kb_core-blue?style=flat-square)](https://bundlephobia.com/package/modern-pdf-lib)
18
- [![tests](https://img.shields.io/badge/tests-1%2C952_passing-brightgreen?style=flat-square)](#)
18
+ [![tests](https://img.shields.io/badge/tests-1%2C973_passing-brightgreen?style=flat-square)](#)
19
19
  [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-3178c6?style=flat-square&logo=typescript&logoColor=white)](#)
20
20
  [![License: MIT](https://img.shields.io/badge/license-MIT-yellow?style=flat-square)](LICENSE)
21
21
 
@@ -429,7 +429,7 @@ modern-pdf-lib/
429
429
  outline/ Bookmarks / document outline
430
430
  metadata/ XMP metadata, viewer preferences
431
431
  wasm/ Rust crate sources (4 modules)
432
- tests/ 1,952 tests across 90 suites
432
+ tests/ 1,973 tests across 91 suites
433
433
  docs/ VitePress documentation
434
434
  ```
435
435
 
@@ -441,7 +441,7 @@ modern-pdf-lib/
441
441
  git clone https://github.com/ABCrimson/modern-pdf-lib.git
442
442
  cd modern-pdf-lib
443
443
  npm install
444
- npm test # 1,952 tests
444
+ npm test # 1,973 tests
445
445
  npm run typecheck # TypeScript 6.0 strict
446
446
  npm run build # ESM + CJS + declarations
447
447
  ```
@@ -204,4 +204,4 @@ Object.defineProperty(exports, 'fflateAdapter_exports', {
204
204
  return fflateAdapter_exports;
205
205
  }
206
206
  });
207
- //# sourceMappingURL=fflateAdapter-cT4YeY_h.cjs.map
207
+ //# sourceMappingURL=fflateAdapter-AHC_S3cb.cjs.map
@@ -193,4 +193,4 @@ var FflateEngine = class {
193
193
 
194
194
  //#endregion
195
195
  export { fflateAdapter_exports as n, decompressSync as t };
196
- //# sourceMappingURL=fflateAdapter-D2mv_ttM.mjs.map
196
+ //# sourceMappingURL=fflateAdapter-DX0VqT5k.mjs.map
@@ -0,0 +1,495 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
2
+
3
+ //#region src/assets/font/ttfSubset.ts
4
+ /**
5
+ * @module assets/font/ttfSubset
6
+ *
7
+ * Pure TypeScript TrueType font subsetter.
8
+ *
9
+ * Reduces font file size by replacing glyph outline data for unused
10
+ * glyphs with zero-length entries. Keeps all glyph ID slots intact
11
+ * so that `CIDToGIDMap /Identity` (CID = GID) continues to work
12
+ * without any changes to the PDF embedding pipeline.
13
+ *
14
+ * The subsetter:
15
+ * 1. Parses the TrueType table directory and `loca`/`glyf` tables.
16
+ * 2. Resolves composite glyph dependencies (component glyphs).
17
+ * 3. Builds a new `glyf` table containing only data for retained glyphs.
18
+ * 4. Builds a new `loca` table with updated offsets.
19
+ * 5. Reassembles a valid TrueType font with correct checksums.
20
+ *
21
+ * No external dependencies. No Buffer — uses Uint8Array and DataView.
22
+ */
23
+ /** Composite glyph flag: arguments are 16-bit instead of 8-bit. */
24
+ const ARG_1_AND_2_ARE_WORDS = 1;
25
+ /** Composite glyph flag: a single F2Dot14 scale follows. */
26
+ const WE_HAVE_A_SCALE = 8;
27
+ /** Composite glyph flag: more component records follow. */
28
+ const MORE_COMPONENTS = 32;
29
+ /** Composite glyph flag: separate X and Y scales follow. */
30
+ const WE_HAVE_AN_X_AND_Y_SCALE = 64;
31
+ /** Composite glyph flag: a 2×2 transformation matrix follows. */
32
+ const WE_HAVE_A_TWO_BY_TWO = 128;
33
+ function parseTableDir(data) {
34
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
35
+ const numTables = view.getUint16(4, false);
36
+ const tables = /* @__PURE__ */ new Map();
37
+ const requiredLen = 12 + numTables * 16;
38
+ if (data.length < requiredLen) throw new RangeError("Font data too small for table directory");
39
+ for (let i = 0; i < numTables; i++) {
40
+ const off = 12 + i * 16;
41
+ const tag = String.fromCharCode(data[off], data[off + 1], data[off + 2], data[off + 3]);
42
+ tables.set(tag, {
43
+ tag,
44
+ offset: view.getUint32(off + 8, false),
45
+ length: view.getUint32(off + 12, false)
46
+ });
47
+ }
48
+ return tables;
49
+ }
50
+ /**
51
+ * Parse the `loca` table to get glyph offsets within the `glyf` table.
52
+ *
53
+ * Returns an array of `numGlyphs + 1` offsets. The glyph data for
54
+ * glyph `i` spans from `offsets[i]` to `offsets[i+1]`. A zero-length
55
+ * span means the glyph has no outline (e.g. space or unused).
56
+ */
57
+ function parseLoca(data, locaOffset, locaLength, numGlyphs, indexToLocFormat) {
58
+ const view = new DataView(data.buffer, data.byteOffset + locaOffset, locaLength);
59
+ const offsets = [];
60
+ const count = numGlyphs + 1;
61
+ if (indexToLocFormat === 0) for (let i = 0; i < count; i++) offsets.push(view.getUint16(i * 2, false) * 2);
62
+ else for (let i = 0; i < count; i++) offsets.push(view.getUint32(i * 4, false));
63
+ return offsets;
64
+ }
65
+ /**
66
+ * Scan composite glyphs and collect all component glyph IDs that are
67
+ * referenced (directly or transitively).
68
+ */
69
+ function resolveCompositeDeps(data, glyfOffset, locaOffsets, usedGids) {
70
+ const retained = new Set(usedGids);
71
+ const queue = [...usedGids];
72
+ while (queue.length > 0) {
73
+ const gid = queue.pop();
74
+ const glyphStart = glyfOffset + locaOffsets[gid];
75
+ if (glyfOffset + locaOffsets[gid + 1] <= glyphStart) continue;
76
+ const view = new DataView(data.buffer, data.byteOffset);
77
+ if (view.getInt16(glyphStart, false) >= 0) continue;
78
+ let off = glyphStart + 10;
79
+ let flags;
80
+ do {
81
+ flags = view.getUint16(off, false);
82
+ const componentGid = view.getUint16(off + 2, false);
83
+ off += 4;
84
+ if (!retained.has(componentGid)) {
85
+ retained.add(componentGid);
86
+ queue.push(componentGid);
87
+ }
88
+ if (flags & ARG_1_AND_2_ARE_WORDS) off += 4;
89
+ else off += 2;
90
+ if (flags & WE_HAVE_A_SCALE) off += 2;
91
+ else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) off += 4;
92
+ else if (flags & WE_HAVE_A_TWO_BY_TWO) off += 8;
93
+ } while (flags & MORE_COMPONENTS);
94
+ }
95
+ return retained;
96
+ }
97
+ function calcTableChecksum(data, offset, length) {
98
+ const aligned = length + 3 & -4;
99
+ let sum = 0;
100
+ const view = new DataView(data.buffer, data.byteOffset + offset);
101
+ for (let i = 0; i < aligned; i += 4) if (i + 3 < length) sum = sum + view.getUint32(i, false) >>> 0;
102
+ else {
103
+ let val = 0;
104
+ for (let j = 0; j < 4; j++) val = val << 8 | (i + j < length ? data[offset + i + j] : 0);
105
+ sum = sum + val >>> 0;
106
+ }
107
+ return sum;
108
+ }
109
+ function writeTag(buf, offset, tag) {
110
+ buf[offset] = tag.charCodeAt(0);
111
+ buf[offset + 1] = tag.charCodeAt(1);
112
+ buf[offset + 2] = tag.charCodeAt(2);
113
+ buf[offset + 3] = tag.charCodeAt(3);
114
+ }
115
+ /**
116
+ * Compute `searchRange`, `entrySelector`, `rangeShift` for the offset
117
+ * table header.
118
+ */
119
+ function offsetTableParams(numTables) {
120
+ let entrySelector = 0;
121
+ let searchRange = 1;
122
+ while (searchRange * 2 <= numTables) {
123
+ searchRange *= 2;
124
+ entrySelector++;
125
+ }
126
+ searchRange *= 16;
127
+ const rangeShift = numTables * 16 - searchRange;
128
+ return {
129
+ searchRange,
130
+ entrySelector,
131
+ rangeShift
132
+ };
133
+ }
134
+ /**
135
+ * Subset a TrueType font, keeping only glyph outlines for the
136
+ * specified glyph IDs.
137
+ *
138
+ * Unused glyphs become zero-length entries in the `glyf` table.
139
+ * All glyph ID slots are preserved so `CIDToGIDMap /Identity` still
140
+ * works. Composite glyph dependencies are resolved automatically.
141
+ *
142
+ * @param fontData The raw TrueType font bytes.
143
+ * @param usedGlyphIds Set of glyph IDs to retain (GID 0 is always kept).
144
+ * @returns The subsetted font bytes.
145
+ */
146
+ function subsetTtf(fontData, usedGlyphIds) {
147
+ if (fontData.length < 28) return new Uint8Array(fontData);
148
+ let tables;
149
+ try {
150
+ tables = parseTableDir(fontData);
151
+ } catch {
152
+ return new Uint8Array(fontData);
153
+ }
154
+ const headEntry = tables.get("head");
155
+ const locaEntry = tables.get("loca");
156
+ const glyfEntry = tables.get("glyf");
157
+ const maxpEntry = tables.get("maxp");
158
+ if (!headEntry || !locaEntry || !glyfEntry || !maxpEntry) return new Uint8Array(fontData);
159
+ const view = new DataView(fontData.buffer, fontData.byteOffset, fontData.byteLength);
160
+ const indexToLocFormat = view.getInt16(headEntry.offset + 50, false);
161
+ const numGlyphs = view.getUint16(maxpEntry.offset + 4, false);
162
+ const locaOffsets = parseLoca(fontData, locaEntry.offset, locaEntry.length, numGlyphs, indexToLocFormat);
163
+ const allUsed = new Set(usedGlyphIds);
164
+ allUsed.add(0);
165
+ const retained = resolveCompositeDeps(fontData, glyfEntry.offset, locaOffsets, allUsed);
166
+ let newGlyfSize = 0;
167
+ for (let gid = 0; gid < numGlyphs; gid++) if (retained.has(gid)) {
168
+ const glyphLen = locaOffsets[gid + 1] - locaOffsets[gid];
169
+ newGlyfSize += glyphLen + 1 & -2;
170
+ }
171
+ const newGlyf = new Uint8Array(newGlyfSize);
172
+ const newLocaOffsets = [];
173
+ let glyfPos = 0;
174
+ for (let gid = 0; gid < numGlyphs; gid++) {
175
+ newLocaOffsets.push(glyfPos);
176
+ if (retained.has(gid)) {
177
+ const srcStart = glyfEntry.offset + locaOffsets[gid];
178
+ const glyphLen = locaOffsets[gid + 1] - locaOffsets[gid];
179
+ if (glyphLen > 0) {
180
+ newGlyf.set(fontData.subarray(srcStart, srcStart + glyphLen), glyfPos);
181
+ glyfPos += glyphLen + 1 & -2;
182
+ }
183
+ }
184
+ }
185
+ newLocaOffsets.push(glyfPos);
186
+ const useShortLoca = glyfPos <= 131070;
187
+ let newLocaSize;
188
+ let newLoca;
189
+ if (useShortLoca) {
190
+ newLocaSize = (numGlyphs + 1) * 2;
191
+ newLoca = new Uint8Array(newLocaSize);
192
+ const locaView = new DataView(newLoca.buffer);
193
+ for (let i = 0; i <= numGlyphs; i++) locaView.setUint16(i * 2, newLocaOffsets[i] >>> 1, false);
194
+ } else {
195
+ newLocaSize = (numGlyphs + 1) * 4;
196
+ newLoca = new Uint8Array(newLocaSize);
197
+ const locaView = new DataView(newLoca.buffer);
198
+ for (let i = 0; i <= numGlyphs; i++) locaView.setUint32(i * 4, newLocaOffsets[i], false);
199
+ }
200
+ const copyTags = [
201
+ "hhea",
202
+ "maxp",
203
+ "OS/2",
204
+ "name",
205
+ "cmap",
206
+ "post",
207
+ "cvt ",
208
+ "fpgm",
209
+ "prep",
210
+ "hmtx",
211
+ "gasp",
212
+ "GDEF",
213
+ "GPOS",
214
+ "GSUB"
215
+ ];
216
+ const outputTables = [];
217
+ const newHead = new Uint8Array(headEntry.length);
218
+ newHead.set(fontData.subarray(headEntry.offset, headEntry.offset + headEntry.length));
219
+ const headView = new DataView(newHead.buffer);
220
+ headView.setInt16(50, useShortLoca ? 0 : 1, false);
221
+ headView.setUint32(8, 0, false);
222
+ outputTables.push({
223
+ tag: "head",
224
+ data: newHead
225
+ });
226
+ for (const tag of copyTags) {
227
+ const entry = tables.get(tag);
228
+ if (entry) outputTables.push({
229
+ tag,
230
+ data: fontData.subarray(entry.offset, entry.offset + entry.length)
231
+ });
232
+ }
233
+ outputTables.push({
234
+ tag: "loca",
235
+ data: newLoca
236
+ });
237
+ outputTables.push({
238
+ tag: "glyf",
239
+ data: newGlyf
240
+ });
241
+ const numOutputTables = outputTables.length;
242
+ const { searchRange, entrySelector, rangeShift } = offsetTableParams(numOutputTables);
243
+ const headerSize = 12;
244
+ const dirSize = numOutputTables * 16;
245
+ let totalSize = headerSize + dirSize;
246
+ for (const t of outputTables) totalSize += t.data.length + 3 & -4;
247
+ const output = new Uint8Array(totalSize);
248
+ const outView = new DataView(output.buffer);
249
+ outView.setUint32(0, 65536, false);
250
+ outView.setUint16(4, numOutputTables, false);
251
+ outView.setUint16(6, searchRange, false);
252
+ outView.setUint16(8, entrySelector, false);
253
+ outView.setUint16(10, rangeShift, false);
254
+ let dataOffset = headerSize + dirSize;
255
+ for (let i = 0; i < outputTables.length; i++) {
256
+ const t = outputTables[i];
257
+ const dirOff = headerSize + i * 16;
258
+ const alignedLen = t.data.length + 3 & -4;
259
+ output.set(t.data, dataOffset);
260
+ const checksum = calcTableChecksum(output, dataOffset, t.data.length);
261
+ writeTag(output, dirOff, t.tag);
262
+ outView.setUint32(dirOff + 4, checksum, false);
263
+ outView.setUint32(dirOff + 8, dataOffset, false);
264
+ outView.setUint32(dirOff + 12, t.data.length, false);
265
+ dataOffset += alignedLen;
266
+ }
267
+ const adjustment = 2981146554 - calcTableChecksum(output, 0, output.length) >>> 0;
268
+ for (let i = 0; i < outputTables.length; i++) if (outputTables[i].tag === "head") {
269
+ const headDataOff = outView.getUint32(headerSize + i * 16 + 8, false);
270
+ outView.setUint32(headDataOff + 8, adjustment, false);
271
+ const newHeadChecksum = calcTableChecksum(output, headDataOff, outputTables[i].data.length);
272
+ outView.setUint32(headerSize + i * 16 + 4, newHeadChecksum, false);
273
+ break;
274
+ }
275
+ return output;
276
+ }
277
+
278
+ //#endregion
279
+ //#region src/assets/font/fontSubset.ts
280
+ /**
281
+ * @module assets/font/fontSubset
282
+ *
283
+ * Font subsetting — removes unused glyphs from a TrueType font to
284
+ * reduce file size. Offers WASM-accelerated subsetting with a pure
285
+ * JS subsetter as the default.
286
+ *
287
+ * The JS subsetter replaces unused glyph outlines with zero-length
288
+ * entries in the `glyf` table, preserving all glyph ID slots so that
289
+ * `CIDToGIDMap /Identity` continues to work without changes to the
290
+ * PDF embedding pipeline. Composite glyph dependencies are resolved
291
+ * automatically.
292
+ *
293
+ * Also builds CMap streams for the subset encoding, mapping CIDs to
294
+ * Unicode codepoints (required for PDF text extraction / copy-paste).
295
+ *
296
+ * No Buffer — uses Uint8Array exclusively.
297
+ * No fs — no file system access.
298
+ * No require() — ESM import only.
299
+ */
300
+ var fontSubset_exports = /* @__PURE__ */ __exportAll({
301
+ buildSubsetCmap: () => buildSubsetCmap,
302
+ computeSubsetTag: () => computeSubsetTag,
303
+ initSubsetWasm: () => initSubsetWasm,
304
+ subsetFont: () => subsetFont
305
+ });
306
+ /**
307
+ * WASM subsetter is not available — the TTF WASM module (`src/wasm/ttf`)
308
+ * provides `parse_font()` for metric extraction but does NOT include a
309
+ * `subset_font()` export. The pure JS subsetter (`ttfSubset.ts`) is
310
+ * the primary subsetting path for all runtimes.
311
+ *
312
+ * This flag exists for forward-compatibility: a dedicated subsetting
313
+ * WASM module could be added in the future.
314
+ */
315
+ let wasmSubsetReady = false;
316
+ /**
317
+ * Initialize the font subsetting WASM module.
318
+ *
319
+ * **Note:** No WASM subsetter currently exists — the TTF WASM module
320
+ * only parses fonts, it does not subset them. The pure JS subsetter
321
+ * (`subsetTtf`) handles all subsetting. This function is a no-op
322
+ * placeholder for forward-compatibility.
323
+ *
324
+ * @param _wasmSource - Unused. Reserved for a future WASM subsetter.
325
+ */
326
+ async function initSubsetWasm(_wasmSource) {
327
+ wasmSubsetReady = false;
328
+ }
329
+ /**
330
+ * Subset a font using a WASM module.
331
+ *
332
+ * Currently no WASM subsetter exists — this delegates to the JS subsetter.
333
+ * Reserved for forward-compatibility with a future WASM subsetting module.
334
+ *
335
+ * @internal
336
+ */
337
+ function subsetWithWasm(fontData, usedGlyphIds) {
338
+ return subsetJs(fontData, usedGlyphIds);
339
+ }
340
+ /**
341
+ * Subset a TrueType font using the pure JS subsetter.
342
+ *
343
+ * Replaces unused glyph outline data with zero-length entries in the
344
+ * `glyf` table while preserving all glyph ID slots. This keeps
345
+ * `CIDToGIDMap /Identity` working (CID = GID) while dramatically
346
+ * reducing the font program size.
347
+ *
348
+ * Composite glyph dependencies are resolved automatically — if a used
349
+ * glyph references component glyphs, those components are retained too.
350
+ *
351
+ * The `newToOldGid` array contains the used glyph IDs in sorted order.
352
+ * Since glyph IDs are not renumbered, each "new" GID equals the
353
+ * original GID (identity mapping for the used glyphs).
354
+ *
355
+ * @param fontData The raw TTF font file bytes.
356
+ * @param usedGlyphIds Set of glyph IDs that are actually used.
357
+ * @returns A {@link SubsetResult} with the subsetted font bytes and
358
+ * identity glyph-ID mappings for the used glyphs.
359
+ * @internal
360
+ */
361
+ function subsetJs(fontData, usedGlyphIds) {
362
+ const allGids = new Set(usedGlyphIds);
363
+ allGids.add(0);
364
+ const subsetFontData = subsetTtf(fontData, allGids);
365
+ const sortedGids = allGids.values().toArray().sort((a, b) => a - b);
366
+ const newToOldGid = sortedGids;
367
+ const oldToNewGid = /* @__PURE__ */ new Map();
368
+ for (const gid of sortedGids) oldToNewGid.set(gid, gid);
369
+ return {
370
+ fontData: subsetFontData,
371
+ newToOldGid,
372
+ oldToNewGid
373
+ };
374
+ }
375
+ /**
376
+ * Subset a TrueType font to include only the specified glyphs.
377
+ *
378
+ * Uses WASM-accelerated subsetting if available (via {@link initSubsetWasm}),
379
+ * otherwise falls back to returning the full font with identity glyph
380
+ * mappings.
381
+ *
382
+ * Glyph ID 0 (.notdef) is always included automatically.
383
+ *
384
+ * @param fontData - The raw TTF font file bytes.
385
+ * @param usedGlyphIds - Set of glyph IDs that are actually used.
386
+ * @returns The subset result with the (possibly reduced) font and
387
+ * glyph-ID remapping tables.
388
+ */
389
+ function subsetFont(fontData, usedGlyphIds) {
390
+ if (usedGlyphIds.size === 0) return subsetJs(fontData, new Set([0]));
391
+ if (wasmSubsetReady) return subsetWithWasm(fontData, usedGlyphIds);
392
+ return subsetJs(fontData, usedGlyphIds);
393
+ }
394
+ /**
395
+ * Build a PDF `/ToUnicode` CMap stream for a subsetted font.
396
+ *
397
+ * The CMap maps CIDs (new glyph IDs in the subset) back to Unicode
398
+ * codepoints, enabling text extraction and copy-paste in PDF viewers.
399
+ *
400
+ * @param subsetResult - The result from {@link subsetFont}.
401
+ * @param originalCmapTable - The original font's cmap table (Unicode
402
+ * codepoint → original glyph ID).
403
+ * @returns A CMap stream body and a CID-to-Unicode lookup.
404
+ */
405
+ function buildSubsetCmap(subsetResult, originalCmapTable) {
406
+ const oldGidToUnicode = /* @__PURE__ */ new Map();
407
+ for (const [codepoint, gid] of originalCmapTable) {
408
+ const existing = oldGidToUnicode.get(gid);
409
+ if (existing) existing.push(codepoint);
410
+ else oldGidToUnicode.set(gid, [codepoint]);
411
+ }
412
+ const cidToUnicode = /* @__PURE__ */ new Map();
413
+ for (let newGid = 0; newGid < subsetResult.newToOldGid.length; newGid++) {
414
+ const oldGid = subsetResult.newToOldGid[newGid];
415
+ const unicodes = oldGidToUnicode.get(oldGid);
416
+ if (unicodes && unicodes.length > 0) cidToUnicode.set(newGid, unicodes);
417
+ }
418
+ return {
419
+ cmapStream: generateToUnicodeCmap(cidToUnicode),
420
+ cidToUnicode
421
+ };
422
+ }
423
+ /**
424
+ * Generate a PDF `/ToUnicode` CMap program.
425
+ *
426
+ * The CMap uses `beginbfchar` / `endbfchar` sections to map individual
427
+ * CIDs to Unicode values. For ranges of consecutive mappings it uses
428
+ * `beginbfrange` / `endbfrange`.
429
+ *
430
+ * @internal
431
+ */
432
+ function generateToUnicodeCmap(cidToUnicode) {
433
+ const lines = [];
434
+ lines.push("/CIDInit /ProcSet findresource begin");
435
+ lines.push("12 dict begin");
436
+ lines.push("begincmap");
437
+ lines.push("/CIDSystemInfo");
438
+ lines.push("<< /Registry (Adobe)");
439
+ lines.push("/Ordering (UCS)");
440
+ lines.push("/Supplement 0");
441
+ lines.push(">> def");
442
+ lines.push("/CMapName /Adobe-Identity-UCS def");
443
+ lines.push("/CMapType 2 def");
444
+ lines.push("1 begincodespacerange");
445
+ lines.push("<0000> <FFFF>");
446
+ lines.push("endcodespacerange");
447
+ const entries = cidToUnicode.entries().toArray().sort((a, b) => a[0] - b[0]);
448
+ const CHUNK_SIZE = 100;
449
+ for (let i = 0; i < entries.length; i += CHUNK_SIZE) {
450
+ const chunk = entries.slice(i, i + CHUNK_SIZE);
451
+ lines.push(`${chunk.length} beginbfchar`);
452
+ for (const [cid, unicodes] of chunk) {
453
+ const cidHex = cid.toString(16).padStart(4, "0").toUpperCase();
454
+ if (unicodes.length === 1) {
455
+ const uniHex = unicodes[0].toString(16).padStart(4, "0").toUpperCase();
456
+ lines.push(`<${cidHex}> <${uniHex}>`);
457
+ } else {
458
+ let uniHex = "";
459
+ for (const cp of unicodes.slice(0, 1)) uniHex += cp.toString(16).padStart(4, "0").toUpperCase();
460
+ lines.push(`<${cidHex}> <${uniHex}>`);
461
+ }
462
+ }
463
+ lines.push("endbfchar");
464
+ }
465
+ lines.push("endcmap");
466
+ lines.push("CMapName currentdict /CMap defineresource pop");
467
+ lines.push("end");
468
+ lines.push("end");
469
+ return lines.join("\n");
470
+ }
471
+ /**
472
+ * Generate a 6-letter uppercase tag for the subset font name.
473
+ *
474
+ * PDF spec recommends a tag prefix like `ABCDEF+FontName` to indicate
475
+ * the font is subsetted. This function generates a deterministic tag
476
+ * from the set of glyph IDs.
477
+ *
478
+ * @param usedGlyphIds - The set of glyph IDs in the subset.
479
+ * @returns A 6-character uppercase ASCII string (e.g. `"BCDEFG"`).
480
+ */
481
+ function computeSubsetTag(usedGlyphIds) {
482
+ let hash = 0;
483
+ for (const gid of usedGlyphIds) hash = (hash << 5) - hash + gid | 0;
484
+ const tag = [];
485
+ let h = Math.abs(hash);
486
+ for (let i = 0; i < 6; i++) {
487
+ tag.push(String.fromCharCode(65 + h % 26));
488
+ h = Math.floor(h / 26);
489
+ }
490
+ return tag.join("");
491
+ }
492
+
493
+ //#endregion
494
+ export { subsetFont as i, computeSubsetTag as n, fontSubset_exports as r, buildSubsetCmap as t };
495
+ //# sourceMappingURL=fontSubset-ZpLoOZ2e.mjs.map