pdfnative 1.3.0 → 1.4.0

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.
@@ -0,0 +1,1139 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { createHash } from 'crypto';
5
+
6
+ // src/fonts/colr-parser.ts
7
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
8
+ function compose(outer, inner) {
9
+ const [a1, b1, c1, d1, e1, f1] = outer;
10
+ const [a2, b2, c2, d2, e2, f2] = inner;
11
+ return [
12
+ a1 * a2 + c1 * b2,
13
+ b1 * a2 + d1 * b2,
14
+ a1 * c2 + c1 * d2,
15
+ b1 * c2 + d1 * d2,
16
+ a1 * e2 + c1 * f2 + e1,
17
+ b1 * e2 + d1 * f2 + f1
18
+ ];
19
+ }
20
+ var UnsupportedPaint = class extends Error {
21
+ };
22
+ function tableDirectory(view) {
23
+ const numTables = view.getUint16(4);
24
+ const tables = {};
25
+ for (let i = 0; i < numTables; i++) {
26
+ const rec = 12 + i * 16;
27
+ const tag = String.fromCharCode(
28
+ view.getUint8(rec),
29
+ view.getUint8(rec + 1),
30
+ view.getUint8(rec + 2),
31
+ view.getUint8(rec + 3)
32
+ );
33
+ tables[tag] = { offset: view.getUint32(rec + 8), length: view.getUint32(rec + 12) };
34
+ }
35
+ return tables;
36
+ }
37
+ function getUint24(view, pos) {
38
+ return view.getUint8(pos) << 16 | view.getUint8(pos + 1) << 8 | view.getUint8(pos + 2);
39
+ }
40
+ function f2dot14(view, pos) {
41
+ return view.getInt16(pos) / 16384;
42
+ }
43
+ function fixed(view, pos) {
44
+ return view.getInt32(pos) / 65536;
45
+ }
46
+ function parseCpal(bytes) {
47
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
48
+ const tables = tableDirectory(view);
49
+ const cpal = tables["CPAL"];
50
+ if (!cpal) return null;
51
+ const base = cpal.offset;
52
+ const numPaletteEntries = view.getUint16(base + 2);
53
+ const colorRecordsArrayOffset = view.getUint32(base + 8);
54
+ const firstIndex = view.getUint16(base + 12);
55
+ const recBase = base + colorRecordsArrayOffset + firstIndex * 4;
56
+ const colors = [];
57
+ for (let i = 0; i < numPaletteEntries; i++) {
58
+ const p = recBase + i * 4;
59
+ const b = view.getUint8(p);
60
+ const g = view.getUint8(p + 1);
61
+ const r = view.getUint8(p + 2);
62
+ const a = view.getUint8(p + 3);
63
+ colors.push([r, g, b, a]);
64
+ }
65
+ return colors;
66
+ }
67
+ function resolveColor(ctx, paletteIndex, alpha) {
68
+ const base = paletteIndex === 65535 ? [0, 0, 0, 255] : ctx.palette[paletteIndex] ?? [0, 0, 0, 255];
69
+ const a = Math.round(base[3] * Math.max(0, Math.min(1, alpha)));
70
+ return [base[0], base[1], base[2], a];
71
+ }
72
+ var EXTEND = ["pad", "repeat", "reflect"];
73
+ function readColorLine(ctx, offset) {
74
+ const { view } = ctx;
75
+ const extend = EXTEND[view.getUint8(offset)] ?? "pad";
76
+ const numStops = view.getUint16(offset + 1);
77
+ const stops = [];
78
+ let p = offset + 3;
79
+ for (let i = 0; i < numStops; i++) {
80
+ const stopOffset = f2dot14(view, p);
81
+ const paletteIndex = view.getUint16(p + 2);
82
+ const alpha = f2dot14(view, p + 4);
83
+ stops.push({ offset: stopOffset, color: resolveColor(ctx, paletteIndex, alpha) });
84
+ p += 6;
85
+ }
86
+ return { stops, extend };
87
+ }
88
+ function apply(m, x, y) {
89
+ return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
90
+ }
91
+ function avgScale(m) {
92
+ const sx = Math.hypot(m[0], m[1]);
93
+ const sy = Math.hypot(m[2], m[3]);
94
+ return (sx + sy) / 2 || 1;
95
+ }
96
+ function resolveFill(ctx, offset, m) {
97
+ const { view } = ctx;
98
+ const format = view.getUint8(offset);
99
+ switch (format) {
100
+ case 2: {
101
+ const paletteIndex = view.getUint16(offset + 1);
102
+ const alpha = f2dot14(view, offset + 3);
103
+ return { kind: "solid", color: resolveColor(ctx, paletteIndex, alpha) };
104
+ }
105
+ case 4: {
106
+ const colorLineOffset = getUint24(view, offset + 1);
107
+ const x0 = view.getInt16(offset + 4), y0 = view.getInt16(offset + 6);
108
+ const x1 = view.getInt16(offset + 8), y1 = view.getInt16(offset + 10);
109
+ const { stops, extend } = readColorLine(ctx, offset + colorLineOffset);
110
+ return { kind: "linear", p0: apply(m, x0, y0), p1: apply(m, x1, y1), stops, extend };
111
+ }
112
+ case 6: {
113
+ const colorLineOffset = getUint24(view, offset + 1);
114
+ const x0 = view.getInt16(offset + 4), y0 = view.getInt16(offset + 6);
115
+ const r0 = view.getUint16(offset + 8);
116
+ const x1 = view.getInt16(offset + 10), y1 = view.getInt16(offset + 12);
117
+ const r1 = view.getUint16(offset + 14);
118
+ const { stops, extend } = readColorLine(ctx, offset + colorLineOffset);
119
+ const s = avgScale(m);
120
+ return { kind: "radial", c0: apply(m, x0, y0), r0: r0 * s, c1: apply(m, x1, y1), r1: r1 * s, stops, extend };
121
+ }
122
+ case 8: {
123
+ const colorLineOffset = getUint24(view, offset + 1);
124
+ const cx = view.getInt16(offset + 4), cy = view.getInt16(offset + 6);
125
+ const startAngle = f2dot14(view, offset + 8) * 180;
126
+ const endAngle = f2dot14(view, offset + 10) * 180;
127
+ const { stops, extend } = readColorLine(ctx, offset + colorLineOffset);
128
+ const rot = Math.atan2(m[1], m[0]) * 180 / Math.PI;
129
+ return {
130
+ kind: "sweep",
131
+ center: apply(m, cx, cy),
132
+ startAngle: startAngle + rot,
133
+ endAngle: endAngle + rot,
134
+ stops,
135
+ extend
136
+ };
137
+ }
138
+ case 12: {
139
+ const subOffset = getUint24(view, offset + 1);
140
+ const transformOffset = getUint24(view, offset + 4);
141
+ const t = readAffine(view, offset + transformOffset);
142
+ return resolveFill(ctx, offset + subOffset, compose(m, t));
143
+ }
144
+ case 14: {
145
+ const subOffset = getUint24(view, offset + 1);
146
+ const dx = view.getInt16(offset + 4), dy = view.getInt16(offset + 6);
147
+ return resolveFill(ctx, offset + subOffset, compose(m, [1, 0, 0, 1, dx, dy]));
148
+ }
149
+ case 16: {
150
+ const subOffset = getUint24(view, offset + 1);
151
+ const sx = f2dot14(view, offset + 4), sy = f2dot14(view, offset + 6);
152
+ return resolveFill(ctx, offset + subOffset, compose(m, [sx, 0, 0, sy, 0, 0]));
153
+ }
154
+ default:
155
+ throw new UnsupportedPaint(`fill paint format ${format}`);
156
+ }
157
+ }
158
+ function readAffine(view, pos) {
159
+ const xx = fixed(view, pos);
160
+ const yx = fixed(view, pos + 4);
161
+ const xy = fixed(view, pos + 8);
162
+ const yy = fixed(view, pos + 12);
163
+ const dx = fixed(view, pos + 16);
164
+ const dy = fixed(view, pos + 20);
165
+ return [xx, yx, xy, yy, dx, dy];
166
+ }
167
+ function collectLayers(ctx, offset, m, out, depth, blendMode) {
168
+ if (depth > 16) throw new UnsupportedPaint("paint recursion too deep");
169
+ const { view } = ctx;
170
+ const format = view.getUint8(offset);
171
+ switch (format) {
172
+ case 1: {
173
+ const numLayers = view.getUint8(offset + 1);
174
+ const firstLayerIndex = view.getUint32(offset + 2);
175
+ if (!ctx.layerListBase) throw new UnsupportedPaint("PaintColrLayers without LayerList");
176
+ for (let i = 0; i < numLayers; i++) {
177
+ const idx = firstLayerIndex + i;
178
+ const paintOffset = view.getUint32(ctx.layerListBase + 4 + idx * 4);
179
+ collectLayers(ctx, ctx.layerListBase + paintOffset, m, out, depth + 1, blendMode);
180
+ }
181
+ return;
182
+ }
183
+ case 10: {
184
+ const subOffset = getUint24(view, offset + 1);
185
+ const glyphId = view.getUint16(offset + 4);
186
+ const paint = resolveFill(ctx, offset + subOffset, IDENTITY);
187
+ const layer = m === IDENTITY ? { glyphId, paint } : { glyphId, paint, transform: m };
188
+ out.push(blendMode ? { ...layer, blendMode } : layer);
189
+ return;
190
+ }
191
+ case 11: {
192
+ const glyphId = view.getUint16(offset + 1);
193
+ const paintOffset = baseGlyphPaintOffset(ctx, glyphId);
194
+ if (paintOffset === null) throw new UnsupportedPaint("PaintColrGlyph missing base");
195
+ collectLayers(ctx, paintOffset, m, out, depth + 1, blendMode);
196
+ return;
197
+ }
198
+ case 12: {
199
+ const subOffset = getUint24(view, offset + 1);
200
+ const transformOffset = getUint24(view, offset + 4);
201
+ const t = readAffine(view, offset + transformOffset);
202
+ collectLayers(ctx, offset + subOffset, compose(m, t), out, depth + 1, blendMode);
203
+ return;
204
+ }
205
+ case 14: {
206
+ const subOffset = getUint24(view, offset + 1);
207
+ const dx = view.getInt16(offset + 4), dy = view.getInt16(offset + 6);
208
+ collectLayers(ctx, offset + subOffset, compose(m, [1, 0, 0, 1, dx, dy]), out, depth + 1, blendMode);
209
+ return;
210
+ }
211
+ case 16: {
212
+ const subOffset = getUint24(view, offset + 1);
213
+ const sx = f2dot14(view, offset + 4), sy = f2dot14(view, offset + 6);
214
+ collectLayers(ctx, offset + subOffset, compose(m, [sx, 0, 0, sy, 0, 0]), out, depth + 1, blendMode);
215
+ return;
216
+ }
217
+ case 32: {
218
+ const sourceOffset = getUint24(view, offset + 1);
219
+ const mode = view.getUint8(offset + 4);
220
+ const backdropOffset = getUint24(view, offset + 5);
221
+ const bm = compositeModeToBlendMode(mode);
222
+ if (bm === null) throw new UnsupportedPaint(`composite mode ${mode}`);
223
+ collectLayers(ctx, offset + backdropOffset, m, out, depth + 1, blendMode);
224
+ collectLayers(ctx, offset + sourceOffset, m, out, depth + 1, bm === "Normal" ? blendMode : bm);
225
+ return;
226
+ }
227
+ default:
228
+ throw new UnsupportedPaint(`structural paint format ${format}`);
229
+ }
230
+ }
231
+ function compositeModeToBlendMode(mode) {
232
+ switch (mode) {
233
+ case 3:
234
+ return "Normal";
235
+ // SRC_OVER
236
+ case 13:
237
+ return "Screen";
238
+ case 14:
239
+ return "Overlay";
240
+ case 15:
241
+ return "Darken";
242
+ case 16:
243
+ return "Lighten";
244
+ case 17:
245
+ return "ColorDodge";
246
+ case 18:
247
+ return "ColorBurn";
248
+ case 19:
249
+ return "HardLight";
250
+ case 20:
251
+ return "SoftLight";
252
+ case 21:
253
+ return "Difference";
254
+ case 22:
255
+ return "Exclusion";
256
+ case 23:
257
+ return "Multiply";
258
+ case 24:
259
+ return "Hue";
260
+ case 25:
261
+ return "Saturation";
262
+ case 26:
263
+ return "Color";
264
+ case 27:
265
+ return "Luminosity";
266
+ // 0 CLEAR, 1 SRC, 2 DEST, 4 DEST_OVER, 5 SRC_IN, 6 DEST_IN, 7 SRC_OUT,
267
+ // 8 DEST_OUT, 9 SRC_ATOP, 10 DEST_ATOP, 11 XOR, 12 PLUS → unsupported.
268
+ default:
269
+ return null;
270
+ }
271
+ }
272
+ function baseGlyphPaintOffset(ctx, glyphId) {
273
+ const { view, colrBase } = ctx;
274
+ const baseGlyphListOffset = view.getUint32(colrBase + 14);
275
+ if (!baseGlyphListOffset) return null;
276
+ const listBase = colrBase + baseGlyphListOffset;
277
+ const numRecords = view.getUint32(listBase);
278
+ for (let i = 0; i < numRecords; i++) {
279
+ const rec = listBase + 4 + i * 6;
280
+ const gid = view.getUint16(rec);
281
+ if (gid === glyphId) {
282
+ return listBase + view.getUint32(rec + 2);
283
+ }
284
+ }
285
+ return null;
286
+ }
287
+ function parseColrCpal(bytes) {
288
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
289
+ const tables = tableDirectory(view);
290
+ const colr = tables["COLR"];
291
+ if (!colr) return null;
292
+ const palette = parseCpal(bytes) ?? [];
293
+ const colrBase = colr.offset;
294
+ const version = view.getUint16(colrBase);
295
+ const result = {};
296
+ const numBaseGlyphRecords = view.getUint16(colrBase + 2);
297
+ const baseGlyphRecordsOffset = view.getUint32(colrBase + 4);
298
+ const layerRecordsOffset = view.getUint32(colrBase + 8);
299
+ if (numBaseGlyphRecords && baseGlyphRecordsOffset && layerRecordsOffset) {
300
+ const recBase = colrBase + baseGlyphRecordsOffset;
301
+ const layerBase = colrBase + layerRecordsOffset;
302
+ for (let i = 0; i < numBaseGlyphRecords; i++) {
303
+ const rec = recBase + i * 6;
304
+ const gid = view.getUint16(rec);
305
+ const firstLayer = view.getUint16(rec + 2);
306
+ const numLayers = view.getUint16(rec + 4);
307
+ const layers = [];
308
+ for (let j = 0; j < numLayers; j++) {
309
+ const lr = layerBase + (firstLayer + j) * 4;
310
+ const layerGid = view.getUint16(lr);
311
+ const paletteIndex = view.getUint16(lr + 2);
312
+ const color = paletteIndex === 65535 ? [0, 0, 0, 255] : palette[paletteIndex] ?? [0, 0, 0, 255];
313
+ layers.push({ glyphId: layerGid, paint: { kind: "solid", color } });
314
+ }
315
+ if (layers.length) result[gid] = { layers };
316
+ }
317
+ }
318
+ if (version >= 1) {
319
+ const layerListOffset = view.getUint32(colrBase + 18);
320
+ const ctx = {
321
+ view,
322
+ palette,
323
+ colrBase,
324
+ layerListBase: layerListOffset ? colrBase + layerListOffset : 0
325
+ };
326
+ const baseGlyphListOffset = view.getUint32(colrBase + 14);
327
+ if (baseGlyphListOffset) {
328
+ const listBase = colrBase + baseGlyphListOffset;
329
+ const numRecords = view.getUint32(listBase);
330
+ for (let i = 0; i < numRecords; i++) {
331
+ const rec = listBase + 4 + i * 6;
332
+ const gid = view.getUint16(rec);
333
+ const paintOffset = listBase + view.getUint32(rec + 2);
334
+ try {
335
+ const layers = [];
336
+ collectLayers(ctx, paintOffset, IDENTITY, layers, 0);
337
+ if (layers.length) result[gid] = { layers };
338
+ } catch (e) {
339
+ if (!(e instanceof UnsupportedPaint)) throw e;
340
+ }
341
+ }
342
+ }
343
+ }
344
+ return Object.keys(result).length ? result : null;
345
+ }
346
+
347
+ // src/fonts/font-subsetter.ts
348
+ function subsetTTF(ttfInput, usedGids) {
349
+ try {
350
+ let u8;
351
+ let len;
352
+ if (ttfInput instanceof Uint8Array) {
353
+ u8 = ttfInput;
354
+ len = u8.length;
355
+ } else {
356
+ len = ttfInput.length;
357
+ const buf = new ArrayBuffer(len);
358
+ u8 = new Uint8Array(buf);
359
+ for (let i = 0; i < len; i++) u8[i] = ttfInput.charCodeAt(i);
360
+ }
361
+ if (len < 12) return ttfInput instanceof Uint8Array ? uint8ToBinaryString(u8) : ttfInput;
362
+ const view = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
363
+ const numTables = view.getUint16(4);
364
+ const dirEnd = 12 + numTables * 16;
365
+ if (dirEnd > len) return ttfInput instanceof Uint8Array ? uint8ToBinaryString(u8) : ttfInput;
366
+ const tables = {};
367
+ for (let i = 0; i < numTables; i++) {
368
+ const off = 12 + i * 16;
369
+ const tag = String.fromCharCode(u8[off], u8[off + 1], u8[off + 2], u8[off + 3]);
370
+ tables[tag] = {
371
+ offset: view.getUint32(off + 8),
372
+ length: view.getUint32(off + 12)
373
+ };
374
+ }
375
+ const head = tables["head"];
376
+ const maxp = tables["maxp"];
377
+ const loca = tables["loca"];
378
+ const glyf = tables["glyf"];
379
+ if (!head || !maxp || !loca || !glyf) return ttfInput instanceof Uint8Array ? uint8ToBinaryString(u8) : ttfInput;
380
+ const numGlyphs = view.getUint16(maxp.offset + 4);
381
+ const locaFormat = view.getInt16(head.offset + 50);
382
+ const origOffsets = new Uint32Array(numGlyphs + 1);
383
+ for (let i = 0; i <= numGlyphs; i++) {
384
+ origOffsets[i] = locaFormat === 0 ? view.getUint16(loca.offset + i * 2) * 2 : view.getUint32(loca.offset + i * 4);
385
+ }
386
+ const allGids = new Set(usedGids);
387
+ allGids.add(0);
388
+ const MAX_COMPOUND_ITERATIONS = 1e4;
389
+ let iterations = 0;
390
+ const queue = [...allGids];
391
+ while (queue.length > 0) {
392
+ if (++iterations > MAX_COMPOUND_ITERATIONS) break;
393
+ const gid = queue.pop();
394
+ if (gid === void 0 || gid >= numGlyphs) continue;
395
+ const off = origOffsets[gid];
396
+ const next = origOffsets[gid + 1];
397
+ if (off >= next) continue;
398
+ const glyfOff = glyf.offset + off;
399
+ if (glyfOff + 10 > len) continue;
400
+ if (view.getInt16(glyfOff) >= 0) continue;
401
+ let pos2 = glyfOff + 10;
402
+ let flags;
403
+ do {
404
+ if (pos2 + 4 > len) break;
405
+ flags = view.getUint16(pos2);
406
+ const componentGid = view.getUint16(pos2 + 2);
407
+ if (!allGids.has(componentGid)) {
408
+ allGids.add(componentGid);
409
+ queue.push(componentGid);
410
+ }
411
+ pos2 += 4;
412
+ if (flags & 1) pos2 += 4;
413
+ else pos2 += 2;
414
+ if (flags & 8) pos2 += 2;
415
+ else if (flags & 64) pos2 += 4;
416
+ else if (flags & 128) pos2 += 8;
417
+ } while (flags & 32);
418
+ }
419
+ const glyfChunks = [];
420
+ const newOffsets = new Uint32Array(numGlyphs + 1);
421
+ let curOff = 0;
422
+ for (let gid = 0; gid < numGlyphs; gid++) {
423
+ newOffsets[gid] = curOff;
424
+ if (allGids.has(gid)) {
425
+ const off = origOffsets[gid];
426
+ const next = origOffsets[gid + 1];
427
+ const glyphLen = next - off;
428
+ if (glyphLen > 0) {
429
+ glyfChunks.push(u8.slice(glyf.offset + off, glyf.offset + next));
430
+ curOff += glyphLen;
431
+ if (glyphLen & 1) {
432
+ glyfChunks.push(new Uint8Array(1));
433
+ curOff += 1;
434
+ }
435
+ }
436
+ }
437
+ }
438
+ newOffsets[numGlyphs] = curOff;
439
+ const newGlyf = new Uint8Array(curOff);
440
+ let pos = 0;
441
+ for (const chunk of glyfChunks) {
442
+ newGlyf.set(chunk, pos);
443
+ pos += chunk.length;
444
+ }
445
+ const newLocaBuf = new ArrayBuffer((numGlyphs + 1) * 4);
446
+ const newLocaView = new DataView(newLocaBuf);
447
+ for (let i = 0; i <= numGlyphs; i++) newLocaView.setUint32(i * 4, newOffsets[i]);
448
+ const newLoca = new Uint8Array(newLocaBuf);
449
+ const PDF_TABLES = /* @__PURE__ */ new Set(["head", "hhea", "maxp", "OS/2", "cmap", "hmtx", "loca", "glyf", "name", "post"]);
450
+ const tableTags = Object.keys(tables).filter((t) => PDF_TABLES.has(t)).sort();
451
+ const newTableData = {};
452
+ for (const tag of tableTags) {
453
+ const t = tables[tag];
454
+ newTableData[tag] = u8.slice(t.offset, t.offset + t.length);
455
+ }
456
+ newTableData["glyf"] = newGlyf;
457
+ newTableData["loca"] = newLoca;
458
+ const headCopy = new Uint8Array(newTableData["head"]);
459
+ new DataView(headCopy.buffer, headCopy.byteOffset, headCopy.byteLength).setInt16(50, 1);
460
+ new DataView(headCopy.buffer, headCopy.byteOffset, headCopy.byteLength).setUint32(8, 0);
461
+ newTableData["head"] = headCopy;
462
+ const numNewTables = tableTags.length;
463
+ const headerSize = 12 + numNewTables * 16;
464
+ let totalSize = headerSize;
465
+ const tableFileOffsets = {};
466
+ for (const tag of tableTags) {
467
+ tableFileOffsets[tag] = totalSize;
468
+ totalSize += newTableData[tag].length;
469
+ if (totalSize & 3) totalSize += 4 - (totalSize & 3);
470
+ }
471
+ const output = new Uint8Array(totalSize);
472
+ const outView = new DataView(output.buffer);
473
+ outView.setUint32(0, 65536);
474
+ outView.setUint16(4, numNewTables);
475
+ let entrySelector = 0, searchRange = 1;
476
+ while (searchRange * 2 <= numNewTables) {
477
+ searchRange *= 2;
478
+ entrySelector++;
479
+ }
480
+ searchRange *= 16;
481
+ outView.setUint16(6, searchRange);
482
+ outView.setUint16(8, entrySelector);
483
+ outView.setUint16(10, numNewTables * 16 - searchRange);
484
+ for (let i = 0; i < tableTags.length; i++) {
485
+ const tag = tableTags[i];
486
+ const off = 12 + i * 16;
487
+ for (let j = 0; j < 4; j++) output[off + j] = tag.charCodeAt(j);
488
+ outView.setUint32(off + 4, ttfChecksum(newTableData[tag]));
489
+ outView.setUint32(off + 8, tableFileOffsets[tag]);
490
+ outView.setUint32(off + 12, newTableData[tag].length);
491
+ }
492
+ for (const tag of tableTags) output.set(newTableData[tag], tableFileOffsets[tag]);
493
+ let result = "";
494
+ for (let i = 0; i < output.length; i += 8192) {
495
+ const end = Math.min(i + 8192, output.length);
496
+ result += String.fromCharCode.apply(null, Array.from(output.subarray(i, end)));
497
+ }
498
+ return result;
499
+ } catch {
500
+ return ttfInput instanceof Uint8Array ? uint8ToBinaryString(ttfInput) : ttfInput;
501
+ }
502
+ }
503
+ function uint8ToBinaryString(u8) {
504
+ let result = "";
505
+ for (let i = 0; i < u8.length; i += 8192) {
506
+ const end = Math.min(i + 8192, u8.length);
507
+ result += String.fromCharCode.apply(null, Array.from(u8.subarray(i, end)));
508
+ }
509
+ return result;
510
+ }
511
+ function ttfChecksum(data) {
512
+ const len = data.length;
513
+ const padded = len & 3 ? new Uint8Array([...data, ...new Uint8Array(4 - (len & 3))]) : data;
514
+ const view = new DataView(padded.buffer, padded.byteOffset, padded.byteLength);
515
+ let sum = 0;
516
+ for (let i = 0; i < padded.length; i += 4) sum = sum + view.getUint32(i) >>> 0;
517
+ return sum;
518
+ }
519
+
520
+ // scripts/lib/emoji-font-core.ts
521
+ function readTables(view) {
522
+ const numTables = view.getUint16(4);
523
+ const tables = {};
524
+ for (let i = 0; i < numTables; i++) {
525
+ const rec = 12 + i * 16;
526
+ const tag = String.fromCharCode(
527
+ view.getUint8(rec),
528
+ view.getUint8(rec + 1),
529
+ view.getUint8(rec + 2),
530
+ view.getUint8(rec + 3)
531
+ );
532
+ tables[tag] = { offset: view.getUint32(rec + 8), length: view.getUint32(rec + 12) };
533
+ }
534
+ return tables;
535
+ }
536
+ function parseCmap(view, base) {
537
+ const cmap = /* @__PURE__ */ new Map();
538
+ const numSub = view.getUint16(base + 2);
539
+ let bestOff = -1;
540
+ let bestFmt = 0;
541
+ for (let i = 0; i < numSub; i++) {
542
+ const rec = base + 4 + i * 8;
543
+ const plat = view.getUint16(rec);
544
+ const enc = view.getUint16(rec + 2);
545
+ const off = view.getUint32(rec + 4);
546
+ if (plat === 3 && (enc === 1 || enc === 10) || plat === 0 && (enc === 3 || enc === 4 || enc === 6)) {
547
+ const fmt = view.getUint16(base + off);
548
+ if (fmt === 12 && bestFmt < 12) {
549
+ bestOff = base + off;
550
+ bestFmt = 12;
551
+ } else if (fmt === 4 && bestFmt < 4) {
552
+ bestOff = base + off;
553
+ bestFmt = 4;
554
+ }
555
+ }
556
+ }
557
+ if (bestOff < 0) return cmap;
558
+ if (bestFmt === 12) {
559
+ const nGroups = view.getUint32(bestOff + 12);
560
+ for (let g = 0; g < nGroups; g++) {
561
+ const rec = bestOff + 16 + g * 12;
562
+ const start = view.getUint32(rec);
563
+ const end = view.getUint32(rec + 4);
564
+ const startGid = view.getUint32(rec + 8);
565
+ for (let c = start; c <= end; c++) cmap.set(c, startGid + (c - start));
566
+ }
567
+ } else {
568
+ const segX2 = view.getUint16(bestOff + 6);
569
+ const segCount = segX2 / 2;
570
+ const endBase = bestOff + 14;
571
+ const startBase = endBase + segX2 + 2;
572
+ const deltaBase = startBase + segX2;
573
+ const rangeBase = deltaBase + segX2;
574
+ for (let s = 0; s < segCount; s++) {
575
+ const end = view.getUint16(endBase + s * 2);
576
+ const start = view.getUint16(startBase + s * 2);
577
+ const delta = view.getInt16(deltaBase + s * 2);
578
+ const rangeOff = view.getUint16(rangeBase + s * 2);
579
+ if (start === 65535) break;
580
+ for (let c = start; c <= end; c++) {
581
+ let gid;
582
+ if (rangeOff === 0) gid = c + delta & 65535;
583
+ else {
584
+ const gi = rangeBase + s * 2 + rangeOff + (c - start) * 2;
585
+ gid = view.getUint16(gi);
586
+ if (gid !== 0) gid = gid + delta & 65535;
587
+ }
588
+ if (gid !== 0) cmap.set(c, gid);
589
+ }
590
+ }
591
+ }
592
+ return cmap;
593
+ }
594
+ function buildEmojiFontModule(ttf, codepoints, opts) {
595
+ const view = new DataView(ttf.buffer, ttf.byteOffset, ttf.byteLength);
596
+ const tables = readTables(view);
597
+ const unitsPerEm = view.getUint16(tables["head"].offset + 18);
598
+ const xMin = view.getInt16(tables["head"].offset + 36);
599
+ const yMin = view.getInt16(tables["head"].offset + 38);
600
+ const xMax = view.getInt16(tables["head"].offset + 40);
601
+ const yMax = view.getInt16(tables["head"].offset + 42);
602
+ const ascent = view.getInt16(tables["hhea"].offset + 4);
603
+ const descent = view.getInt16(tables["hhea"].offset + 6);
604
+ const numberOfHMetrics = view.getUint16(tables["hhea"].offset + 34);
605
+ const numGlyphs = view.getUint16(tables["maxp"].offset + 4);
606
+ let capHeight = Math.round(ascent * 0.7);
607
+ if (tables["OS/2"] && tables["OS/2"].length >= 90) {
608
+ capHeight = view.getInt16(tables["OS/2"].offset + 88);
609
+ }
610
+ const widthsAll = new Array(numGlyphs).fill(0);
611
+ let lastW = 0;
612
+ for (let i = 0; i < numGlyphs; i++) {
613
+ if (i < numberOfHMetrics) {
614
+ lastW = view.getUint16(tables["hmtx"].offset + i * 4);
615
+ }
616
+ widthsAll[i] = lastW;
617
+ }
618
+ const cmap = parseCmap(view, tables["cmap"].offset);
619
+ const colorGlyphs = parseColrCpal(ttf);
620
+ if (!colorGlyphs) throw new Error("Font has no COLR/CPAL table");
621
+ const subCmap = {};
622
+ const subWidths = {};
623
+ const subColor = {};
624
+ const usedGids = /* @__PURE__ */ new Set([0]);
625
+ let kept = 0;
626
+ let missing = 0;
627
+ const missingCodepoints = [];
628
+ const seen = /* @__PURE__ */ new Set();
629
+ for (const cp of codepoints) {
630
+ if (seen.has(cp)) continue;
631
+ seen.add(cp);
632
+ const gid = cmap.get(cp);
633
+ if (gid === void 0 || !colorGlyphs[gid]) {
634
+ missing++;
635
+ missingCodepoints.push(cp);
636
+ continue;
637
+ }
638
+ subCmap[cp] = gid;
639
+ subWidths[gid] = widthsAll[gid];
640
+ subColor[gid] = colorGlyphs[gid];
641
+ usedGids.add(gid);
642
+ for (const layer of colorGlyphs[gid].layers) usedGids.add(layer.glyphId);
643
+ kept++;
644
+ }
645
+ const subBinary = subsetTTF(ttf, usedGids);
646
+ const ttfBase64 = Buffer.from(subBinary, "latin1").toString("base64");
647
+ const defaultWidth = widthsAll[cmap.get(32) ?? 0] || widthsAll[0] || unitsPerEm;
648
+ const cids = Object.keys(subWidths).map(Number).sort((a, b) => a - b);
649
+ const wParts = [];
650
+ for (const cid of cids) wParts.push(`${cid} [${subWidths[cid]}]`);
651
+ const pdfWidthArray = wParts.join(" ");
652
+ const metrics = {
653
+ unitsPerEm,
654
+ ascent,
655
+ descent,
656
+ capHeight,
657
+ stemV: 80,
658
+ bbox: [xMin, yMin, xMax, yMax],
659
+ defaultWidth,
660
+ numGlyphs
661
+ };
662
+ const fontName = opts.fontName ?? "NotoColorEmoji-Regular";
663
+ const fontNameLiteral = fontName.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
664
+ const banner = opts.makeBanner(kept);
665
+ const js = `${banner}
666
+
667
+ export const metrics = ${JSON.stringify(metrics)};
668
+ export const fontName = '${fontNameLiteral}';
669
+ export const cmap = ${JSON.stringify(subCmap)};
670
+ export const defaultWidth = ${defaultWidth};
671
+ export const widths = ${JSON.stringify(subWidths)};
672
+ export const pdfWidthArray = ${JSON.stringify(pdfWidthArray)};
673
+ export const gsub = {};
674
+ export const ligatures = null;
675
+ export const markAnchors = null;
676
+ export const mark2mark = null;
677
+ export const colorGlyphs = ${JSON.stringify(subColor)};
678
+ export const ttfBase64 = ${JSON.stringify(ttfBase64)};
679
+ `;
680
+ const dts = `${banner}
681
+
682
+ import type { FontData } from '${opts.dtsTypeImport.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}';
683
+
684
+ export declare const metrics: FontData['metrics'];
685
+ export declare const fontName: string;
686
+ export declare const cmap: Record<number, number>;
687
+ export declare const defaultWidth: number;
688
+ export declare const widths: Record<number, number>;
689
+ export declare const pdfWidthArray: string;
690
+ export declare const gsub: Record<number, number>;
691
+ export declare const ligatures: null;
692
+ export declare const markAnchors: null;
693
+ export declare const mark2mark: null;
694
+ export declare const colorGlyphs: NonNullable<FontData['colorGlyphs']>;
695
+ export declare const ttfBase64: string;
696
+ `;
697
+ const sizeKb = Math.round(Buffer.byteLength(js) / 1024);
698
+ return { js, dts, stats: { kept, missing, usedGids: usedGids.size, sizeKb, missingCodepoints } };
699
+ }
700
+ function allColorCodepoints(ttf) {
701
+ const view = new DataView(ttf.buffer, ttf.byteOffset, ttf.byteLength);
702
+ const tables = readTables(view);
703
+ const cmap = parseCmap(view, tables["cmap"].offset);
704
+ const colorGlyphs = parseColrCpal(ttf);
705
+ if (!colorGlyphs) throw new Error("Font has no COLR/CPAL table");
706
+ const out = [];
707
+ for (const [cp, gid] of cmap) {
708
+ if (colorGlyphs[gid]) out.push(cp);
709
+ }
710
+ return out.sort((a, b) => a - b);
711
+ }
712
+
713
+ // scripts/lib/curated-emoji.ts
714
+ var CURATED_EMOJI = [
715
+ // Smileys & emotion
716
+ 128512,
717
+ 128513,
718
+ 128514,
719
+ 128515,
720
+ 128516,
721
+ 128517,
722
+ 128518,
723
+ 128519,
724
+ 128521,
725
+ 128522,
726
+ 128523,
727
+ 128524,
728
+ 128525,
729
+ 128526,
730
+ 128527,
731
+ 128528,
732
+ 128530,
733
+ 128531,
734
+ 128532,
735
+ 128533,
736
+ 128534,
737
+ 128536,
738
+ 128537,
739
+ 128538,
740
+ 128540,
741
+ 128541,
742
+ 128542,
743
+ 128544,
744
+ 128545,
745
+ 128546,
746
+ 128547,
747
+ 128548,
748
+ 128549,
749
+ 128552,
750
+ 128553,
751
+ 128554,
752
+ 128555,
753
+ 128556,
754
+ 128557,
755
+ 128558,
756
+ 128559,
757
+ 128560,
758
+ 128561,
759
+ 128562,
760
+ 128563,
761
+ 128564,
762
+ 128565,
763
+ 128566,
764
+ 128567,
765
+ 128577,
766
+ 128578,
767
+ 128579,
768
+ 128580,
769
+ 129296,
770
+ 129297,
771
+ 129298,
772
+ 129299,
773
+ 129300,
774
+ 129301,
775
+ 129303,
776
+ 129312,
777
+ 129313,
778
+ 129314,
779
+ 129315,
780
+ 129316,
781
+ 129317,
782
+ 129319,
783
+ 129320,
784
+ 129321,
785
+ 129322,
786
+ 129323,
787
+ 129324,
788
+ 129325,
789
+ 129326,
790
+ 129327,
791
+ 129392,
792
+ 129395,
793
+ 129396,
794
+ 129397,
795
+ 129398,
796
+ 129402,
797
+ 129400,
798
+ 129401,
799
+ // Hearts & symbols
800
+ 10084,
801
+ 129505,
802
+ 128155,
803
+ 128154,
804
+ 128153,
805
+ 128156,
806
+ 128420,
807
+ 129293,
808
+ 129294,
809
+ 128148,
810
+ 128149,
811
+ 128150,
812
+ 128151,
813
+ 128152,
814
+ 128157,
815
+ 128158,
816
+ 128159,
817
+ 11088,
818
+ 127775,
819
+ 10024,
820
+ 128165,
821
+ 128171,
822
+ 128293,
823
+ 128175,
824
+ 9989,
825
+ 10060,
826
+ 10071,
827
+ 10067,
828
+ 9888,
829
+ 9851,
830
+ // Hands & people
831
+ 128077,
832
+ 128078,
833
+ 128076,
834
+ 128074,
835
+ 9994,
836
+ 9995,
837
+ 128075,
838
+ 128080,
839
+ 128588,
840
+ 128591,
841
+ 128079,
842
+ 128400,
843
+ 129304,
844
+ 129305,
845
+ 129306,
846
+ 129307,
847
+ 129308,
848
+ 129309,
849
+ 129310,
850
+ 129311,
851
+ 128405,
852
+ 128170,
853
+ // Animals & nature
854
+ 128054,
855
+ 128049,
856
+ 128045,
857
+ 128057,
858
+ 128048,
859
+ 128059,
860
+ 128060,
861
+ 128040,
862
+ 128047,
863
+ 129409,
864
+ 128046,
865
+ 128055,
866
+ 128056,
867
+ 128053,
868
+ 128020,
869
+ 128039,
870
+ 128038,
871
+ 128029,
872
+ 129419,
873
+ 128012,
874
+ 128030,
875
+ 128034,
876
+ 128013,
877
+ 128051,
878
+ 128044,
879
+ 128031,
880
+ 127812,
881
+ 127802,
882
+ 127799,
883
+ 127800,
884
+ 127801,
885
+ 127803,
886
+ 127811,
887
+ 127794,
888
+ 127795,
889
+ 127796,
890
+ // Food & drink
891
+ 127822,
892
+ 127820,
893
+ 127815,
894
+ 127817,
895
+ 127827,
896
+ 127826,
897
+ 127825,
898
+ 127818,
899
+ 127813,
900
+ 129365,
901
+ 127805,
902
+ 127828,
903
+ 127829,
904
+ 127839,
905
+ 127789,
906
+ 127871,
907
+ 127846,
908
+ 127856,
909
+ 127851,
910
+ 127850,
911
+ 9749,
912
+ 127866,
913
+ 127863,
914
+ // Activity, travel & objects
915
+ 9917,
916
+ 127936,
917
+ 127944,
918
+ 127934,
919
+ 127952,
920
+ 127928,
921
+ 127925,
922
+ 127912,
923
+ 128663,
924
+ 128661,
925
+ 128652,
926
+ 9992,
927
+ 128640,
928
+ 128674,
929
+ 8986,
930
+ 128241,
931
+ 128187,
932
+ 128161,
933
+ 128214,
934
+ 9999,
935
+ 128204,
936
+ 128274,
937
+ 128273,
938
+ 127968,
939
+ 127873,
940
+ 127880,
941
+ 127881,
942
+ 127926,
943
+ 128176,
944
+ 128181
945
+ ];
946
+
947
+ // scripts/lib/emoji-cli.ts
948
+ var DEFAULT_OUT = "noto-color-emoji-data.js";
949
+ var DEFAULT_TYPES = "pdfnative";
950
+ function parseArgs(argv) {
951
+ const opts = {
952
+ download: false,
953
+ all: false,
954
+ out: DEFAULT_OUT,
955
+ types: DEFAULT_TYPES,
956
+ help: false
957
+ };
958
+ for (let i = 0; i < argv.length; i++) {
959
+ const arg = argv[i];
960
+ const next = () => {
961
+ const v = argv[++i];
962
+ if (v === void 0) throw new Error(`Missing value for ${arg}`);
963
+ return v;
964
+ };
965
+ switch (arg) {
966
+ case "-h":
967
+ case "--help":
968
+ opts.help = true;
969
+ break;
970
+ case "--ttf":
971
+ opts.ttf = next();
972
+ break;
973
+ case "--download":
974
+ opts.download = true;
975
+ break;
976
+ case "--all":
977
+ opts.all = true;
978
+ break;
979
+ case "--preset":
980
+ opts.preset = next();
981
+ break;
982
+ case "--codepoints":
983
+ opts.codepoints = next();
984
+ break;
985
+ case "--ranges":
986
+ opts.ranges = next();
987
+ break;
988
+ case "--out":
989
+ opts.out = next();
990
+ break;
991
+ case "--font-name":
992
+ opts.fontName = next();
993
+ break;
994
+ case "--types":
995
+ opts.types = next();
996
+ break;
997
+ default:
998
+ throw new Error(`Unknown option: ${arg} (try --help)`);
999
+ }
1000
+ }
1001
+ return opts;
1002
+ }
1003
+ function parseHex(token) {
1004
+ const t = token.trim().replace(/^(u\+|0x|#)/i, "");
1005
+ if (!/^[0-9a-f]+$/i.test(t)) throw new Error(`Invalid codepoint: "${token}"`);
1006
+ const cp = parseInt(t, 16);
1007
+ if (!Number.isFinite(cp) || cp < 0 || cp > 1114111) {
1008
+ throw new Error(`Codepoint out of range: "${token}"`);
1009
+ }
1010
+ return cp;
1011
+ }
1012
+ function resolveCodepoints(opts, allColor) {
1013
+ if (opts.all || opts.preset === "all") return [...allColor()].sort((a, b) => a - b);
1014
+ if (opts.preset && opts.preset !== "curated") {
1015
+ throw new Error(`Unknown preset: "${opts.preset}" (use 'curated' or 'all')`);
1016
+ }
1017
+ const set = /* @__PURE__ */ new Set();
1018
+ let explicit = false;
1019
+ if (opts.preset === "curated") {
1020
+ for (const cp of CURATED_EMOJI) set.add(cp);
1021
+ explicit = true;
1022
+ }
1023
+ if (opts.codepoints) {
1024
+ for (const tok of opts.codepoints.split(",")) {
1025
+ if (tok.trim()) {
1026
+ set.add(parseHex(tok));
1027
+ explicit = true;
1028
+ }
1029
+ }
1030
+ }
1031
+ if (opts.ranges) {
1032
+ for (const tok of opts.ranges.split(",")) {
1033
+ if (!tok.trim()) continue;
1034
+ const [a, b] = tok.split("-");
1035
+ if (a === void 0 || b === void 0) throw new Error(`Invalid range: "${tok}"`);
1036
+ const lo = parseHex(a);
1037
+ const hi = parseHex(b);
1038
+ if (hi < lo) throw new Error(`Range end before start: "${tok}"`);
1039
+ if (hi - lo > 131072) throw new Error(`Range too large: "${tok}"`);
1040
+ for (let cp = lo; cp <= hi; cp++) set.add(cp);
1041
+ explicit = true;
1042
+ }
1043
+ }
1044
+ if (!explicit) for (const cp of CURATED_EMOJI) set.add(cp);
1045
+ return [...set].sort((a, b) => a - b);
1046
+ }
1047
+
1048
+ // scripts/build-emoji-font.ts
1049
+ var DOWNLOAD_URL = "https://raw.githubusercontent.com/google/fonts/main/ofl/notocoloremoji/NotoColorEmoji-Regular.ttf";
1050
+ var DOWNLOAD_SHA256 = "be73479ba4fa277c89b85cd6c71717df30d9d0eff6da8c1e1a201e5b95459299";
1051
+ var HELP = `pdfnative-build-emoji-font \u2014 colour-emoji font data module generator
1052
+
1053
+ Source font (one of):
1054
+ --ttf <path> Path to a COLRv1/CPAL colour font (NotoColorEmoji-Regular.ttf).
1055
+ --download Fetch the official Noto Color Emoji (OFL-1.1) and verify checksum.
1056
+
1057
+ Glyph selection (combine freely; default: --preset curated):
1058
+ --all Every colour glyph in the font (large module).
1059
+ --preset <curated|all> Named selection ('curated' = pdfnative's lean 221-glyph set).
1060
+ --codepoints <list> Comma-separated hex scalars, e.g. 1F600,1F680,2764.
1061
+ --ranges <list> Comma-separated inclusive hex ranges, e.g. 1F600-1F64F.
1062
+
1063
+ Output:
1064
+ --out <path> Output .js path (a sibling .d.ts is written). Default ./noto-color-emoji-data.js
1065
+ --font-name <name> Embedded PostScript font name.
1066
+ --types <path> Type import used in the generated .d.ts. Default 'pdfnative'.
1067
+
1068
+ Other:
1069
+ -h, --help Show this help.
1070
+
1071
+ Examples:
1072
+ pdfnative-build-emoji-font --download --all
1073
+ pdfnative-build-emoji-font --ttf ./NotoColorEmoji-Regular.ttf --codepoints 1F600,1F680
1074
+ pdfnative-build-emoji-font --download --ranges 1F600-1F64F,2600-27BF --out ./emoji.js
1075
+ `;
1076
+ async function fetchFont() {
1077
+ console.log(`Downloading Noto Color Emoji (OFL-1.1) \u2026`);
1078
+ console.log(` ${DOWNLOAD_URL}`);
1079
+ const res = await fetch(DOWNLOAD_URL);
1080
+ if (!res.ok) throw new Error(`Download failed \u2014 HTTP ${res.status}`);
1081
+ const bytes = new Uint8Array(await res.arrayBuffer());
1082
+ const hash = createHash("sha256").update(bytes).digest("hex");
1083
+ if (hash === DOWNLOAD_SHA256) {
1084
+ console.log(` checksum OK (sha256 ${hash.slice(0, 16)}\u2026)`);
1085
+ } else {
1086
+ console.warn(` WARNING: checksum differs from the tested build.`);
1087
+ console.warn(` expected ${DOWNLOAD_SHA256}`);
1088
+ console.warn(` actual ${hash}`);
1089
+ console.warn(` Upstream may have shipped a newer Unicode revision; proceeding.`);
1090
+ }
1091
+ return bytes;
1092
+ }
1093
+ async function main() {
1094
+ const opts = parseArgs(process.argv.slice(2));
1095
+ if (opts.help) {
1096
+ console.log(HELP);
1097
+ return;
1098
+ }
1099
+ if (!opts.ttf && !opts.download) {
1100
+ throw new Error("Provide a source font with --ttf <path> or --download (try --help)");
1101
+ }
1102
+ if (opts.ttf && opts.download) {
1103
+ throw new Error("Use either --ttf or --download, not both");
1104
+ }
1105
+ const ttf = opts.download ? await fetchFont() : new Uint8Array(readFileSync(resolve(opts.ttf)));
1106
+ const codepoints = resolveCodepoints(opts, () => allColorCodepoints(ttf));
1107
+ if (codepoints.length === 0) {
1108
+ throw new Error("No codepoints selected \u2014 use --all, --preset, --codepoints or --ranges");
1109
+ }
1110
+ const outJs = resolve(opts.out);
1111
+ const outDts = outJs.replace(/\.js$/i, "") + ".d.ts";
1112
+ const fontName = opts.fontName ?? "NotoColorEmoji-Regular";
1113
+ const { js, dts, stats } = buildEmojiFontModule(ttf, codepoints, {
1114
+ fontName,
1115
+ dtsTypeImport: opts.types,
1116
+ makeBanner: (kept) => `/**
1117
+ * COLOUR-EMOJI FONT DATA \u2014 ${fontName}
1118
+ * Generated by: pdfnative-build-emoji-font
1119
+ * Source: Noto Color Emoji (OFL-1.1). Colour glyphs: ${kept}.
1120
+ *
1121
+ * Opt in:
1122
+ * import { registerFont } from 'pdfnative';
1123
+ * registerFont('emoji', () => import('./${outJs.split(/[\\/]/).pop()}'));
1124
+ */`
1125
+ });
1126
+ writeFileSync(outJs, js);
1127
+ writeFileSync(outDts, dts);
1128
+ console.log(`Colour-emoji data module written:`);
1129
+ console.log(` requested codepoints: ${codepoints.length}`);
1130
+ console.log(` colour glyphs kept: ${stats.kept} (missing: ${stats.missing})`);
1131
+ console.log(` embedded glyph ids: ${stats.usedGids}`);
1132
+ console.log(` module size: ${stats.sizeKb} KB`);
1133
+ console.log(` ${outJs}`);
1134
+ console.log(` ${outDts}`);
1135
+ }
1136
+ main().catch((err) => {
1137
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
1138
+ process.exit(1);
1139
+ });