@patrickdk77/pdf-defuse 0.0.0-stage → 0.1.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.
package/dist/io.js ADDED
@@ -0,0 +1,414 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SpillSink = exports.TempDir = exports.Reader = void 0;
37
+ exports.bufferSource = bufferSource;
38
+ exports.fileSource = fileSource;
39
+ exports.bufferSink = bufferSink;
40
+ exports.fileSink = fileSink;
41
+ exports.writableSink = writableSink;
42
+ exports.copyToSink = copyToSink;
43
+ const crypto = __importStar(require("node:crypto"));
44
+ const fsp = __importStar(require("node:fs/promises"));
45
+ const os = __importStar(require("node:os"));
46
+ const path = __importStar(require("node:path"));
47
+ /** A source over bytes already in memory. Reads return views, not copies. */
48
+ function bufferSource(bytes) {
49
+ return {
50
+ size: async () => bytes.length,
51
+ read: async (offset, length) => bytes.subarray(Math.max(0, offset), Math.min(bytes.length, offset + length)),
52
+ };
53
+ }
54
+ /** A source over a local file, read at offsets through one file handle. */
55
+ function fileSource(filePath) {
56
+ // The pending open is shared, so concurrent first calls get one handle and close() closes it.
57
+ let opening;
58
+ let size;
59
+ const open = () => {
60
+ opening ??= fsp.open(filePath, 'r');
61
+ return opening;
62
+ };
63
+ return {
64
+ async size() {
65
+ if (size === undefined)
66
+ size = (await (await open()).stat()).size;
67
+ return size;
68
+ },
69
+ async read(offset, length) {
70
+ const h = await open();
71
+ const total = await this.size();
72
+ const n = Math.max(0, Math.min(length, total - offset));
73
+ const buf = Buffer.allocUnsafe(n);
74
+ let done = 0;
75
+ while (done < n) {
76
+ const { bytesRead } = await h.read(buf, done, n - done, offset + done);
77
+ if (bytesRead === 0)
78
+ break;
79
+ done += bytesRead;
80
+ }
81
+ return done === n ? buf : buf.subarray(0, done);
82
+ },
83
+ async close() {
84
+ const p = opening;
85
+ opening = undefined;
86
+ await p?.then(h => h.close(), () => {
87
+ // An open that failed left nothing to close.
88
+ });
89
+ },
90
+ };
91
+ }
92
+ /** Collects output in memory. */
93
+ function bufferSink() {
94
+ const chunks = [];
95
+ let size = 0;
96
+ return {
97
+ async write(chunk) {
98
+ chunks.push(Buffer.from(chunk));
99
+ size += chunk.length;
100
+ },
101
+ async close() {
102
+ // Nothing to release. The chunks stay for result().
103
+ },
104
+ result: () => Buffer.concat(chunks),
105
+ size: () => size,
106
+ };
107
+ }
108
+ /** FileHandle.write may write less than asked, for example at a file size limit; the next write then reports why. */
109
+ async function writeAll(handle, chunk) {
110
+ for (let done = 0; done < chunk.length;)
111
+ done += (await handle.write(chunk, done, chunk.length - done)).bytesWritten;
112
+ }
113
+ /** Writes to a local file. The file is created on the first write. */
114
+ function fileSink(filePath) {
115
+ let handle;
116
+ return {
117
+ async write(chunk) {
118
+ handle ??= await fsp.open(filePath, 'w');
119
+ await writeAll(handle, chunk);
120
+ },
121
+ async close() {
122
+ handle ??= await fsp.open(filePath, 'w');
123
+ await handle.close();
124
+ },
125
+ async abort() {
126
+ const h = handle;
127
+ handle = undefined;
128
+ if (!h)
129
+ return;
130
+ // A partial regular file goes. abort() never unlinks a device or pipe the caller named.
131
+ const partial = (await h.stat().catch(() => undefined))?.isFile();
132
+ await h.close().catch(() => {
133
+ // abort() runs after a failure, which is the error to report.
134
+ });
135
+ if (partial)
136
+ await fsp.rm(filePath, { force: true });
137
+ },
138
+ };
139
+ }
140
+ /**
141
+ * Wraps any Node Writable, waiting for 'drain' when it applies backpressure. The stream's first error, or its
142
+ * closing before the end, fails the pending write and every later one.
143
+ */
144
+ function writableSink(stream) {
145
+ let failure;
146
+ const pending = new Set();
147
+ const fail = (e) => {
148
+ failure ??= e;
149
+ for (const reject of pending)
150
+ reject(failure);
151
+ pending.clear();
152
+ };
153
+ // Node emits some errors after the write callback, and an 'error' with no listener crashes the process, so this
154
+ // listener stays for the stream's whole life.
155
+ stream.on('error', fail);
156
+ stream.on('close', () => {
157
+ if (!stream.writableFinished)
158
+ fail(stream.errored ?? new Error('The output stream closed before all data was written'));
159
+ });
160
+ const settle = (start) => new Promise((resolve, reject) => {
161
+ if (failure)
162
+ return reject(failure);
163
+ if (stream.destroyed)
164
+ return reject(stream.errored ?? new Error('The output stream was destroyed'));
165
+ pending.add(reject);
166
+ start(err => {
167
+ pending.delete(reject);
168
+ if (err)
169
+ reject(err);
170
+ else
171
+ resolve();
172
+ });
173
+ });
174
+ return {
175
+ write: chunk => settle(done => (stream.write(chunk) ? done() : stream.once('drain', () => done()))),
176
+ close: () => settle(done => stream.end((err) => done(err))),
177
+ // The reader sees an error instead of an end, so it cannot mistake a partial output for a whole one.
178
+ abort: async (error) => {
179
+ stream.destroy(error instanceof Error ? error : new Error(String(error)));
180
+ },
181
+ };
182
+ }
183
+ /** Random-access reader with a small block cache, so the parser can ask for overlapping windows cheaply. */
184
+ class Reader {
185
+ source;
186
+ size;
187
+ blockSize;
188
+ maxBlocks;
189
+ cache = new Map();
190
+ order = [];
191
+ constructor(source, size, blockSize = 65536, maxBlocks = 64) {
192
+ this.source = source;
193
+ this.size = size;
194
+ this.blockSize = blockSize;
195
+ this.maxBlocks = maxBlocks;
196
+ }
197
+ async block(index) {
198
+ const hit = this.cache.get(index);
199
+ if (hit)
200
+ return hit;
201
+ const data = await this.source.read(index * this.blockSize, this.blockSize);
202
+ this.cache.set(index, data);
203
+ this.order.push(index);
204
+ if (this.order.length > this.maxBlocks) {
205
+ const evicted = this.order.shift();
206
+ if (evicted !== undefined)
207
+ this.cache.delete(evicted);
208
+ }
209
+ return data;
210
+ }
211
+ /** Reads up to `length` bytes at `offset`; shorter only at the end of the source. */
212
+ async read(offset, length) {
213
+ if (offset >= this.size || length <= 0)
214
+ return new Uint8Array(0);
215
+ const end = Math.min(this.size, offset + length);
216
+ if (end - offset > this.blockSize * 4)
217
+ return this.source.read(offset, end - offset);
218
+ const first = Math.floor(offset / this.blockSize);
219
+ const last = Math.floor((end - 1) / this.blockSize);
220
+ if (first === last) {
221
+ const b = await this.block(first);
222
+ return b.subarray(offset - first * this.blockSize, end - first * this.blockSize);
223
+ }
224
+ const out = new Uint8Array(end - offset);
225
+ let pos = 0;
226
+ for (let i = first; i <= last; i++) {
227
+ const b = await this.block(i);
228
+ const from = i === first ? offset - first * this.blockSize : 0;
229
+ const to = i === last ? end - i * this.blockSize : b.length;
230
+ out.set(b.subarray(from, to), pos);
231
+ pos += to - from;
232
+ }
233
+ return out;
234
+ }
235
+ /** Yields the range in chunks without caching them. */
236
+ async *chunks(offset, length, chunkSize = 262144) {
237
+ let pos = offset;
238
+ const end = Math.min(this.size, offset + length);
239
+ while (pos < end) {
240
+ const n = Math.min(chunkSize, end - pos);
241
+ const c = await this.source.read(pos, n);
242
+ if (c.length === 0)
243
+ break;
244
+ yield c;
245
+ pos += c.length;
246
+ }
247
+ }
248
+ }
249
+ exports.Reader = Reader;
250
+ /** A private temporary directory, removed by cleanup(). It also keeps the run's first I/O error. */
251
+ class TempDir {
252
+ base;
253
+ // The pending mkdtemp is shared, so concurrent first calls make one directory and cleanup() removes it.
254
+ dir;
255
+ seq = 0;
256
+ /**
257
+ * The first error from a temporary file or a watched source. The parser recovers from a failed read as if the
258
+ * file were damaged, so the engine checks this to report an I/O error instead of a verdict on the PDF.
259
+ */
260
+ ioError;
261
+ constructor(base = os.tmpdir()) {
262
+ this.base = base;
263
+ }
264
+ async file(suffix = '.bin') {
265
+ this.dir ??= fsp.mkdtemp(path.join(this.base, 'pdf-defuse-'));
266
+ return path.join(await this.dir, `${this.seq++}${suffix}`);
267
+ }
268
+ async cleanup() {
269
+ const dir = await this.dir?.catch(() => undefined);
270
+ this.dir = undefined;
271
+ if (dir)
272
+ await fsp.rm(dir, { recursive: true, force: true });
273
+ }
274
+ /**
275
+ * Wraps a source so that its errors also land in ioError. Once the source has given its size, a read that returns
276
+ * more or fewer bytes than that size allows is an error too: the parser would take the shifted bytes for the file.
277
+ */
278
+ watch(source) {
279
+ const keep = async (op) => {
280
+ try {
281
+ return await op();
282
+ }
283
+ catch (e) {
284
+ this.ioError ??= e;
285
+ throw e;
286
+ }
287
+ };
288
+ let size;
289
+ return {
290
+ size: () => keep(async () => {
291
+ size = await source.size();
292
+ return size;
293
+ }),
294
+ read: (offset, length) => keep(async () => {
295
+ const b = await source.read(offset, length);
296
+ const want = size === undefined ? b.length : Math.max(0, Math.min(length, size - offset));
297
+ if (b.length !== want)
298
+ throw Object.assign(new Error(`The source returned ${b.length} bytes at offset ${offset} where ${want} were expected`), { code: 'EIO' });
299
+ return b;
300
+ }),
301
+ close: source.close &&
302
+ (() => {
303
+ if (!source.close)
304
+ throw new Error('The source no longer has close()');
305
+ return source.close();
306
+ }),
307
+ };
308
+ }
309
+ }
310
+ exports.TempDir = TempDir;
311
+ /** A sink that keeps small output in memory and moves to a temporary file past a threshold. */
312
+ class SpillSink {
313
+ temp;
314
+ threshold;
315
+ chunks = [];
316
+ size = 0;
317
+ filePath;
318
+ handle;
319
+ md5 = crypto.createHash('md5');
320
+ hex;
321
+ spilled = false;
322
+ constructor(temp, threshold) {
323
+ this.temp = temp;
324
+ this.threshold = threshold;
325
+ }
326
+ async write(chunk) {
327
+ this.md5.update(chunk);
328
+ this.size += chunk.length;
329
+ if (!this.handle && this.size <= this.threshold) {
330
+ this.chunks.push(Buffer.from(chunk));
331
+ return;
332
+ }
333
+ try {
334
+ if (!this.handle) {
335
+ this.filePath = await this.temp.file('.spill');
336
+ this.handle = await fsp.open(this.filePath, 'w');
337
+ this.spilled = true;
338
+ for (const c of this.chunks)
339
+ await writeAll(this.handle, c);
340
+ this.chunks = [];
341
+ }
342
+ await writeAll(this.handle, chunk);
343
+ }
344
+ catch (e) {
345
+ // Some callers treat a failed spill like undecodable data; the engine still finds the error here.
346
+ this.temp.ioError ??= e;
347
+ throw e;
348
+ }
349
+ }
350
+ async close() {
351
+ await this.handle?.close();
352
+ this.handle = undefined;
353
+ }
354
+ /** Closes the sink and deletes its temporary file, once nothing will read what it holds again. */
355
+ async dispose() {
356
+ await this.close();
357
+ this.chunks = [];
358
+ const file = this.filePath;
359
+ this.filePath = undefined;
360
+ if (file === undefined)
361
+ return;
362
+ try {
363
+ await fsp.rm(file, { force: true });
364
+ }
365
+ catch (e) {
366
+ this.temp.ioError ??= e;
367
+ throw e;
368
+ }
369
+ }
370
+ get length() {
371
+ return this.size;
372
+ }
373
+ /** The temporary file, once the sink has spilled. */
374
+ get path() {
375
+ return this.filePath;
376
+ }
377
+ digest() {
378
+ this.hex ??= this.md5.digest('hex');
379
+ return this.hex;
380
+ }
381
+ /** A source over what was written. Call after close(). */
382
+ source() {
383
+ return this.filePath ? this.temp.watch(fileSource(this.filePath)) : bufferSource(Buffer.concat(this.chunks));
384
+ }
385
+ async *read(chunkSize = 262144) {
386
+ const src = this.source();
387
+ // A consumer that stops early, such as a failed output write, still closes the file.
388
+ try {
389
+ const total = await src.size();
390
+ for (let pos = 0; pos < total; pos += chunkSize)
391
+ yield await src.read(pos, Math.min(chunkSize, total - pos));
392
+ }
393
+ finally {
394
+ await src.close?.();
395
+ }
396
+ }
397
+ }
398
+ exports.SpillSink = SpillSink;
399
+ /**
400
+ * Copies a whole source, calling `checkTime` before each chunk. A read that returns more or fewer bytes than it asked
401
+ * for fails the copy, which would otherwise shift every byte after it.
402
+ */
403
+ async function copyToSink(source, sink, checkTime) {
404
+ const total = await source.size();
405
+ const chunk = 262144;
406
+ for (let pos = 0; pos < total; pos += chunk) {
407
+ checkTime?.();
408
+ const want = Math.min(chunk, total - pos);
409
+ const b = await source.read(pos, want);
410
+ if (b.length !== want)
411
+ throw Object.assign(new Error(`The source returned ${b.length} bytes at offset ${pos} where ${want} were expected`), { code: 'EIO' });
412
+ await sink.write(b);
413
+ }
414
+ }
@@ -0,0 +1,52 @@
1
+ /** PDF object model. Arrays are plain JS arrays; null is PDF null. */
2
+ export type PdfObject = null | boolean | number | PdfName | PdfString | PdfObject[] | PdfDict | PdfRef | PdfStream;
3
+ export declare class PdfName {
4
+ readonly name: string;
5
+ /** `name` is decoded and has no leading slash. */
6
+ constructor(name: string);
7
+ }
8
+ export declare class PdfString {
9
+ bytes: Uint8Array;
10
+ constructor(bytes: Uint8Array);
11
+ }
12
+ export declare class PdfRef {
13
+ readonly num: number;
14
+ readonly gen: number;
15
+ constructor(num: number, gen: number);
16
+ }
17
+ export declare class PdfDict {
18
+ readonly map: Map<string, PdfObject>;
19
+ get(key: string): PdfObject | undefined;
20
+ has(key: string): boolean;
21
+ set(key: string, value: PdfObject): void;
22
+ delete(key: string): void;
23
+ keys(): string[];
24
+ entries(): Array<[string, PdfObject]>;
25
+ clone(): PdfDict;
26
+ /** Name value of `key`, without the slash, or undefined. */
27
+ name(key: string): string | undefined;
28
+ number(key: string): number | undefined;
29
+ }
30
+ /** A stream as found in the source. The body is located, not loaded. */
31
+ export declare class PdfStream {
32
+ dict: PdfDict;
33
+ /** Absolute offset of the first body byte in the source. */
34
+ offset: number;
35
+ /** Raw (encoded, possibly encrypted) body length. */
36
+ length: number;
37
+ constructor(dict: PdfDict,
38
+ /** Absolute offset of the first body byte in the source. */
39
+ offset: number,
40
+ /** Raw (encoded, possibly encrypted) body length. */
41
+ length: number);
42
+ }
43
+ /** The dictionary of a dict or a stream. */
44
+ export declare function dictOf(v: PdfObject | undefined): PdfDict | undefined;
45
+ /**
46
+ * Decode a PDF text string: UTF-16BE, UTF-16LE or UTF-8 with BOM, otherwise PDFDocEncoding. pdf.js reads the UTF-16LE
47
+ * mark too, so a tooltip or file name written that way shows as text. Each form is decoded in one call, since a string
48
+ * built one character at a time costs tens of bytes of heap per input byte.
49
+ */
50
+ export declare function decodeTextString(bytes: Uint8Array): string;
51
+ /** Encode a JS string as a PDF text string: Latin-1 bytes when PDFDocEncoding reads them back unchanged, else UTF-16BE. */
52
+ export declare function encodeTextString(text: string): Uint8Array;
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PdfStream = exports.PdfDict = exports.PdfRef = exports.PdfString = exports.PdfName = void 0;
4
+ exports.dictOf = dictOf;
5
+ exports.decodeTextString = decodeTextString;
6
+ exports.encodeTextString = encodeTextString;
7
+ class PdfName {
8
+ name;
9
+ /** `name` is decoded and has no leading slash. */
10
+ constructor(name) {
11
+ this.name = name;
12
+ }
13
+ }
14
+ exports.PdfName = PdfName;
15
+ class PdfString {
16
+ bytes;
17
+ constructor(bytes) {
18
+ this.bytes = bytes;
19
+ }
20
+ }
21
+ exports.PdfString = PdfString;
22
+ class PdfRef {
23
+ num;
24
+ gen;
25
+ constructor(num, gen) {
26
+ this.num = num;
27
+ this.gen = gen;
28
+ }
29
+ }
30
+ exports.PdfRef = PdfRef;
31
+ class PdfDict {
32
+ map = new Map();
33
+ get(key) {
34
+ return this.map.get(key);
35
+ }
36
+ has(key) {
37
+ return this.map.has(key);
38
+ }
39
+ set(key, value) {
40
+ this.map.set(key, value);
41
+ }
42
+ delete(key) {
43
+ this.map.delete(key);
44
+ }
45
+ keys() {
46
+ return [...this.map.keys()];
47
+ }
48
+ entries() {
49
+ return [...this.map.entries()];
50
+ }
51
+ clone() {
52
+ const d = new PdfDict();
53
+ for (const [k, v] of this.map)
54
+ d.map.set(k, v);
55
+ return d;
56
+ }
57
+ /** Name value of `key`, without the slash, or undefined. */
58
+ name(key) {
59
+ const v = this.map.get(key);
60
+ return v instanceof PdfName ? v.name : undefined;
61
+ }
62
+ number(key) {
63
+ const v = this.map.get(key);
64
+ return typeof v === 'number' ? v : undefined;
65
+ }
66
+ }
67
+ exports.PdfDict = PdfDict;
68
+ /** A stream as found in the source. The body is located, not loaded. */
69
+ class PdfStream {
70
+ dict;
71
+ offset;
72
+ length;
73
+ constructor(dict,
74
+ /** Absolute offset of the first body byte in the source. */
75
+ offset,
76
+ /** Raw (encoded, possibly encrypted) body length. */
77
+ length) {
78
+ this.dict = dict;
79
+ this.offset = offset;
80
+ this.length = length;
81
+ }
82
+ }
83
+ exports.PdfStream = PdfStream;
84
+ /** The dictionary of a dict or a stream. */
85
+ function dictOf(v) {
86
+ if (v instanceof PdfDict)
87
+ return v;
88
+ if (v instanceof PdfStream)
89
+ return v.dict;
90
+ return undefined;
91
+ }
92
+ const PDF_DOC_HIGH = {
93
+ 24: 0x02d8,
94
+ 25: 0x02c7,
95
+ 26: 0x02c6,
96
+ 27: 0x02d9,
97
+ 28: 0x02dd,
98
+ 29: 0x02db,
99
+ 30: 0x02da,
100
+ 31: 0x02dc,
101
+ 128: 0x2022,
102
+ 129: 0x2020,
103
+ 130: 0x2021,
104
+ 131: 0x2026,
105
+ 132: 0x2014,
106
+ 133: 0x2013,
107
+ 134: 0x0192,
108
+ 135: 0x2044,
109
+ 136: 0x2039,
110
+ 137: 0x203a,
111
+ 138: 0x2212,
112
+ 139: 0x2030,
113
+ 140: 0x201e,
114
+ 141: 0x201c,
115
+ 142: 0x201d,
116
+ 143: 0x2018,
117
+ 144: 0x2019,
118
+ 145: 0x201a,
119
+ 146: 0x2122,
120
+ 147: 0xfb01,
121
+ 148: 0xfb02,
122
+ 149: 0x0141,
123
+ 150: 0x0152,
124
+ 151: 0x0160,
125
+ 152: 0x0178,
126
+ 153: 0x017d,
127
+ 154: 0x0131,
128
+ 155: 0x0142,
129
+ 156: 0x0153,
130
+ 157: 0x0161,
131
+ 158: 0x017e,
132
+ 160: 0x20ac,
133
+ };
134
+ const PDF_DOC = new Uint16Array(256).map((_, b) => PDF_DOC_HIGH[b] ?? b);
135
+ /**
136
+ * Decode a PDF text string: UTF-16BE, UTF-16LE or UTF-8 with BOM, otherwise PDFDocEncoding. pdf.js reads the UTF-16LE
137
+ * mark too, so a tooltip or file name written that way shows as text. Each form is decoded in one call, since a string
138
+ * built one character at a time costs tens of bytes of heap per input byte.
139
+ */
140
+ function decodeTextString(bytes) {
141
+ // An odd trailing byte is dropped.
142
+ if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
143
+ return Buffer.from(bytes.subarray(2, bytes.length - (bytes.length & 1)))
144
+ .swap16()
145
+ .toString('utf16le');
146
+ }
147
+ if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
148
+ return Buffer.from(bytes.buffer, bytes.byteOffset + 2, bytes.length - 2 - (bytes.length & 1)).toString('utf16le');
149
+ }
150
+ if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
151
+ return Buffer.from(bytes.subarray(3)).toString('utf8');
152
+ }
153
+ let remapped = false;
154
+ for (let i = 0; i < bytes.length && !remapped; i++)
155
+ remapped = PDF_DOC[bytes[i]] !== bytes[i];
156
+ if (!remapped)
157
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.length).toString('latin1');
158
+ const out = Buffer.allocUnsafe(bytes.length * 2);
159
+ for (let i = 0; i < bytes.length; i++) {
160
+ const c = PDF_DOC[bytes[i]];
161
+ out[2 * i] = c & 0xff;
162
+ out[2 * i + 1] = c >> 8;
163
+ }
164
+ return out.toString('utf16le');
165
+ }
166
+ /** Encode a JS string as a PDF text string: Latin-1 bytes when PDFDocEncoding reads them back unchanged, else UTF-16BE. */
167
+ function encodeTextString(text) {
168
+ // PDFDocEncoding maps 0x18-0x1F to accents and leaves 0x7F undefined.
169
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: the range PDFDocEncoding keeps as is starts with control codes.
170
+ if (/^[\x00-\x17\x20-\x7e]*$/.test(text))
171
+ return Buffer.from(text, 'latin1');
172
+ const out = Buffer.alloc(2 + text.length * 2);
173
+ out[0] = 0xfe;
174
+ out[1] = 0xff;
175
+ for (let i = 0; i < text.length; i++)
176
+ out.writeUInt16BE(text.charCodeAt(i), 2 + i * 2);
177
+ return out;
178
+ }