@hanzo/build 0.2.62 → 0.2.64

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/lib/pdf.js ADDED
@@ -0,0 +1,847 @@
1
+ /**
2
+ * A PDF, read where it is: its text page by page, and what its structure says
3
+ * about it — version, pages, metadata, encryption, how often it was revised
4
+ * after it was written, and whether each signature still covers the bytes it
5
+ * signed. Nothing leaves the browser.
6
+ *
7
+ * Text comes out of each page's content streams (and the forms they draw),
8
+ * decoded with each font's ToUnicode map: Flate is undone by the platform's own
9
+ * `DecompressionStream`, objects packed into object streams are unpacked, and a
10
+ * font without a map is read as single-byte Latin. What it cannot read it says
11
+ * it cannot: an encrypted file, a page that is only pictures (a scan needs OCR),
12
+ * a stream in a filter other than Flate.
13
+ *
14
+ * A signature is checked for integrity, not trust: that its byte range starts at
15
+ * the first byte, frames exactly its own signature, and reaches the end of the
16
+ * file (or how many bytes were added after it), and that the digest the
17
+ * signature carries (CMS `messageDigest`) is the digest of those bytes. Whether
18
+ * the signer's certificate is trusted is not decided here.
19
+ */
20
+ /** A name (`/Type`), a string's bytes, an indirect reference, an operator in a content stream. */
21
+ export class Name {
22
+ n;
23
+ constructor(n) {
24
+ this.n = n;
25
+ }
26
+ }
27
+ export class Str {
28
+ s;
29
+ constructor(s) {
30
+ this.s = s;
31
+ }
32
+ }
33
+ export class Ref {
34
+ n;
35
+ constructor(n) {
36
+ this.n = n;
37
+ }
38
+ }
39
+ export class Op {
40
+ o;
41
+ constructor(o) {
42
+ this.o = o;
43
+ }
44
+ }
45
+ /** The largest file read. */
46
+ export const LARGEST = 50 << 20;
47
+ /** Bytes as a string of the same length, one char per byte. */
48
+ export function latin(b) {
49
+ let s = '';
50
+ for (let i = 0; i < b.length; i += 0x8000)
51
+ s += String.fromCharCode(...b.subarray(i, i + 0x8000));
52
+ return s;
53
+ }
54
+ const bytesOf = (s) => Uint8Array.from(s, (c) => c.charCodeAt(0));
55
+ /** Hex digits as bytes, a lone last digit read as if a 0 followed it. */
56
+ function unhex(h) {
57
+ const d = h.replace(/[^0-9a-fA-F]/g, '');
58
+ let out = '';
59
+ for (let i = 0; i < d.length; i += 2)
60
+ out += String.fromCharCode(parseInt(d.slice(i, i + 2).padEnd(2, '0'), 16));
61
+ return out;
62
+ }
63
+ const SPACE = /[\0\t\n\f\r ]/;
64
+ const DELIM = /[()<>[\]{}/%]/;
65
+ /** A cursor over PDF syntax: the objects of a file, or the operators of a content stream. */
66
+ export class Lexer {
67
+ src;
68
+ at;
69
+ constructor(src, at = 0) {
70
+ this.src = src;
71
+ this.at = at;
72
+ }
73
+ skip() {
74
+ const s = this.src;
75
+ while (this.at < s.length) {
76
+ const c = s[this.at];
77
+ if (SPACE.test(c))
78
+ this.at++;
79
+ else if (c === '%')
80
+ while (this.at < s.length && s[this.at] !== '\n' && s[this.at] !== '\r')
81
+ this.at++;
82
+ else
83
+ break;
84
+ }
85
+ }
86
+ /** The next value, or undefined at the end. */
87
+ next() {
88
+ this.skip();
89
+ const s = this.src;
90
+ if (this.at >= s.length)
91
+ return undefined;
92
+ const c = s[this.at];
93
+ if (c === '<' && s[this.at + 1] === '<') {
94
+ this.at += 2;
95
+ const d = new Map();
96
+ for (;;) {
97
+ this.skip();
98
+ if (this.at >= s.length)
99
+ return d;
100
+ if (s.startsWith('>>', this.at)) {
101
+ this.at += 2;
102
+ return d;
103
+ }
104
+ const k = this.next();
105
+ if (!(k instanceof Name)) {
106
+ if (k === undefined)
107
+ return d;
108
+ continue;
109
+ }
110
+ const v = this.next();
111
+ if (v !== undefined)
112
+ d.set(k.n, v);
113
+ }
114
+ }
115
+ if (c === '<') {
116
+ const end = s.indexOf('>', this.at);
117
+ const out = unhex(s.slice(this.at + 1, end < 0 ? s.length : end));
118
+ this.at = end < 0 ? s.length : end + 1;
119
+ return new Str(out);
120
+ }
121
+ if (c === '[') {
122
+ this.at++;
123
+ const a = [];
124
+ for (;;) {
125
+ this.skip();
126
+ if (this.at >= s.length)
127
+ return a;
128
+ if (s[this.at] === ']') {
129
+ this.at++;
130
+ return a;
131
+ }
132
+ const v = this.next();
133
+ if (v === undefined)
134
+ return a;
135
+ a.push(v);
136
+ }
137
+ }
138
+ if (c === '(')
139
+ return this.literal();
140
+ if (c === '/') {
141
+ let e = this.at + 1;
142
+ while (e < s.length && !SPACE.test(s[e]) && !DELIM.test(s[e]))
143
+ e++;
144
+ const n = s.slice(this.at + 1, e).replace(/#([0-9a-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
145
+ this.at = e;
146
+ return new Name(n);
147
+ }
148
+ if (c === ']' || c === '>' || c === ')' || c === '{' || c === '}') {
149
+ this.at++;
150
+ return this.next();
151
+ }
152
+ let e = this.at;
153
+ while (e < s.length && !SPACE.test(s[e]) && !DELIM.test(s[e]))
154
+ e++;
155
+ const word = s.slice(this.at, e);
156
+ this.at = e;
157
+ if (/^[+-]?(\d+\.?\d*|\.\d+)$/.test(word)) {
158
+ // `12 0 R` is a reference; anything else is the number it reads as.
159
+ const m = /^\s+(\d+)\s+R(?=[\s/<>[\]()%]|$)/.exec(s.slice(e, e + 32));
160
+ if (m && /^\d+$/.test(word)) {
161
+ this.at = e + m[0].length;
162
+ return new Ref(Number(word));
163
+ }
164
+ return Number(word);
165
+ }
166
+ if (word === 'true')
167
+ return true;
168
+ if (word === 'false')
169
+ return false;
170
+ if (word === 'null')
171
+ return null;
172
+ return new Op(word);
173
+ }
174
+ literal() {
175
+ const s = this.src;
176
+ let depth = 0;
177
+ let out = '';
178
+ let i = this.at;
179
+ for (; i < s.length; i++) {
180
+ const c = s[i];
181
+ if (c === '(') {
182
+ if (depth++ === 0)
183
+ continue;
184
+ }
185
+ else if (c === ')') {
186
+ if (--depth === 0)
187
+ break;
188
+ }
189
+ else if (c === '\\') {
190
+ const d = s[++i] ?? '';
191
+ const esc = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f' };
192
+ if (d in esc)
193
+ out += esc[d];
194
+ else if (/[0-7]/.test(d)) {
195
+ let o = d;
196
+ while (o.length < 3 && /[0-7]/.test(s[i + 1] ?? ''))
197
+ o += s[++i];
198
+ out += String.fromCharCode(parseInt(o, 8) & 0xff);
199
+ }
200
+ else if (d === '\r') {
201
+ if (s[i + 1] === '\n')
202
+ i++;
203
+ }
204
+ else if (d !== '\n')
205
+ out += d;
206
+ continue;
207
+ }
208
+ out += c;
209
+ }
210
+ this.at = i + 1;
211
+ return new Str(out);
212
+ }
213
+ }
214
+ const dict = (v) => (v instanceof Map ? v : new Map());
215
+ const name = (v) => (v instanceof Name ? v.n : '');
216
+ const num = (v) => (typeof v === 'number' ? v : NaN);
217
+ export function resolve(doc, v, depth = 0) {
218
+ return v instanceof Ref && depth < 8 ? resolve(doc, doc.objects.get(v.n)?.value, depth + 1) : v;
219
+ }
220
+ /** Undo a stream's filters: Flate, ASCII85 and hex, in the order named. Null for one it cannot undo. */
221
+ export async function decode(doc, o) {
222
+ if (!o.data)
223
+ return null;
224
+ const f = resolve(doc, dict(o.value).get('Filter'));
225
+ const filters = (Array.isArray(f) ? f : f === undefined ? [] : [f]).map((x) => name(resolve(doc, x)));
226
+ let out = o.data;
227
+ for (const x of filters) {
228
+ if (!out)
229
+ return null;
230
+ if (x === 'FlateDecode' || x === 'Fl')
231
+ out = await inflate(out);
232
+ else if (x === 'ASCII85Decode' || x === 'A85')
233
+ out = a85(latin(out));
234
+ else if (x === 'ASCIIHexDecode' || x === 'AHx')
235
+ out = bytesOf(unhex(latin(out).split('>')[0]));
236
+ else
237
+ return null;
238
+ }
239
+ return out;
240
+ }
241
+ /** ASCII85, as PDF writes it: `z` for four zeros, ending at `~>`. */
242
+ export function a85(s) {
243
+ const body = s.replace(/^\s*<~/, '').split('~>')[0].replace(/\s+/g, '');
244
+ const out = [];
245
+ let group = [];
246
+ const flush = (n) => {
247
+ while (group.length < 5)
248
+ group.push(84);
249
+ let v = 0;
250
+ for (const d of group)
251
+ v = v * 85 + d;
252
+ for (let k = 3; k >= 4 - n; k--)
253
+ out.push((v >>> (k * 8)) & 0xff);
254
+ group = [];
255
+ };
256
+ for (const c of body) {
257
+ if (c === 'z' && !group.length) {
258
+ out.push(0, 0, 0, 0);
259
+ continue;
260
+ }
261
+ const d = c.charCodeAt(0) - 33;
262
+ if (d < 0 || d > 84)
263
+ continue;
264
+ group.push(d);
265
+ if (group.length === 5)
266
+ flush(4);
267
+ }
268
+ if (group.length > 1)
269
+ flush(group.length - 1);
270
+ return Uint8Array.from(out);
271
+ }
272
+ export async function inflate(data) {
273
+ const ds = new DecompressionStream('deflate');
274
+ const w = ds.writable.getWriter();
275
+ w.write(data).catch(() => undefined);
276
+ w.close().catch(() => undefined);
277
+ const r = ds.readable.getReader();
278
+ const parts = [];
279
+ let n = 0;
280
+ try {
281
+ for (;;) {
282
+ const { done, value } = await r.read();
283
+ if (done)
284
+ break;
285
+ parts.push(value);
286
+ n += value.length;
287
+ }
288
+ }
289
+ catch {
290
+ // Junk after the end of the data, or data cut short: what came out stands.
291
+ }
292
+ if (!n)
293
+ return null;
294
+ const out = new Uint8Array(n);
295
+ let at = 0;
296
+ for (const p of parts) {
297
+ out.set(p, at);
298
+ at += p.length;
299
+ }
300
+ return out;
301
+ }
302
+ /** Every object in the file, a later definition of a number winning, and those packed in object streams. */
303
+ export async function open(bytes) {
304
+ const src = latin(bytes);
305
+ const doc = { bytes, src, objects: new Map() };
306
+ const head = /(\d+)\s+(\d+)\s+obj\b/g;
307
+ for (let m = head.exec(src); m; m = head.exec(src)) {
308
+ const lx = new Lexer(src, m.index + m[0].length);
309
+ const value = lx.next() ?? null;
310
+ lx.skip();
311
+ const o = { value };
312
+ if (src.startsWith('stream', lx.at)) {
313
+ let from = lx.at + 6;
314
+ if (src[from] === '\r')
315
+ from++;
316
+ if (src[from] === '\n')
317
+ from++;
318
+ // A direct /Length that lands on endstream is trusted; otherwise endstream is found.
319
+ const len = num(dict(value).get('Length'));
320
+ let to = from + len;
321
+ if (!(Number.isInteger(len) && len >= 0 && /^\s*endstream/.test(src.slice(to, to + 32)))) {
322
+ to = src.indexOf('endstream', from);
323
+ if (to < 0)
324
+ to = src.length;
325
+ while (to > from && (src[to - 1] === '\n' || src[to - 1] === '\r'))
326
+ to--;
327
+ }
328
+ o.data = bytes.subarray(from, to);
329
+ head.lastIndex = to;
330
+ }
331
+ doc.objects.set(Number(m[1]), o);
332
+ }
333
+ for (const o of [...doc.objects.values()]) {
334
+ const d = dict(o.value);
335
+ if (name(d.get('Type')) !== 'ObjStm')
336
+ continue;
337
+ const data = await decode(doc, o);
338
+ if (!data)
339
+ continue;
340
+ const s = latin(data);
341
+ const first = num(resolve(doc, d.get('First')));
342
+ const count = num(resolve(doc, d.get('N')));
343
+ if (!Number.isInteger(first) || !Number.isInteger(count))
344
+ continue;
345
+ const lx = new Lexer(s);
346
+ const pairs = [];
347
+ for (let i = 0; i < count * 2; i++)
348
+ pairs.push(num(lx.next()));
349
+ for (let i = 0; i < count; i++) {
350
+ const id = pairs[i * 2];
351
+ if (!Number.isInteger(id) || doc.objects.has(id))
352
+ continue;
353
+ doc.objects.set(id, { value: new Lexer(s, first + pairs[i * 2 + 1]).next() ?? null });
354
+ }
355
+ }
356
+ return doc;
357
+ }
358
+ /** The last trailer's word for `key`: a classic trailer or a cross-reference stream's, both plain text. */
359
+ function trailer(doc, key) {
360
+ const all = [...doc.src.matchAll(new RegExp(`/${key}\\s+(\\d+)\\s+\\d+\\s+R`, 'g'))];
361
+ const m = all[all.length - 1];
362
+ return m ? new Ref(Number(m[1])) : null;
363
+ }
364
+ /** The pages in reading order, each with the resources it inherits. */
365
+ export function pages(doc) {
366
+ const out = [];
367
+ const seen = new Set();
368
+ const walk = (node, res) => {
369
+ if (seen.has(node))
370
+ return;
371
+ seen.add(node);
372
+ const here = node.get('Resources') ?? res;
373
+ const kids = resolve(doc, node.get('Kids'));
374
+ if (Array.isArray(kids))
375
+ for (const k of kids)
376
+ walk(dict(resolve(doc, k)), here);
377
+ else if (name(node.get('Type')) === 'Page')
378
+ out.push({ page: node, resources: dict(resolve(doc, here)) });
379
+ };
380
+ const root = trailer(doc, 'Root');
381
+ if (root)
382
+ walk(dict(resolve(doc, dict(resolve(doc, root)).get('Pages'))), undefined);
383
+ if (!out.length) {
384
+ for (const [, o] of [...doc.objects].sort((a, b) => a[0] - b[0])) {
385
+ const d = dict(o.value);
386
+ if (name(d.get('Type')) === 'Page')
387
+ out.push({ page: d, resources: dict(resolve(doc, d.get('Resources'))) });
388
+ }
389
+ }
390
+ return out;
391
+ }
392
+ const be16 = (s) => {
393
+ let out = '';
394
+ for (let i = 0; i + 1 < s.length; i += 2)
395
+ out += String.fromCharCode((s.charCodeAt(i) << 8) | s.charCodeAt(i + 1));
396
+ return out;
397
+ };
398
+ /** A CMap's hex destination as text: UTF-16BE, or one byte as written by a producer that ignored that. */
399
+ function hexText(h) {
400
+ const s = unhex(h);
401
+ return s.length === 1 ? s : be16(s);
402
+ }
403
+ /** A ToUnicode CMap: its code width and its map. */
404
+ export function cmap(s, width) {
405
+ const map = new Map();
406
+ const space = /begincodespacerange([\s\S]*?)endcodespacerange/.exec(s);
407
+ const lo = space ? /<([0-9a-fA-F]+)>/.exec(space[1]) : null;
408
+ if (lo)
409
+ width = Math.max(1, Math.ceil(lo[1].length / 2));
410
+ for (const block of s.matchAll(/beginbfchar([\s\S]*?)endbfchar/g)) {
411
+ for (const m of block[1].matchAll(/<([0-9a-fA-F]+)>\s*<([0-9a-fA-F]*)>/g))
412
+ map.set(parseInt(m[1], 16), hexText(m[2]));
413
+ }
414
+ for (const block of s.matchAll(/beginbfrange([\s\S]*?)endbfrange/g)) {
415
+ for (const m of block[1].matchAll(/<([0-9a-fA-F]+)>\s*<([0-9a-fA-F]+)>\s*(<[0-9a-fA-F]*>|\[[^\]]*\])/g)) {
416
+ const a = parseInt(m[1], 16);
417
+ const b = Math.min(parseInt(m[2], 16), a + 0xffff);
418
+ if (m[3].startsWith('[')) {
419
+ const each = [...m[3].matchAll(/<([0-9a-fA-F]*)>/g)];
420
+ for (let c = a; c <= b && c - a < each.length; c++)
421
+ map.set(c, hexText(each[c - a][1]));
422
+ }
423
+ else {
424
+ const base = hexText(m[3].slice(1, -1));
425
+ const last = base.charCodeAt(base.length - 1);
426
+ for (let c = a; c <= b; c++)
427
+ map.set(c, base.slice(0, -1) + String.fromCharCode(last + (c - a)));
428
+ }
429
+ }
430
+ }
431
+ return { width, map: map.size ? map : null };
432
+ }
433
+ /** Glyph names a simple font's /Differences may give a code, as the text they stand for. */
434
+ const GLYPHS = {
435
+ fi: 'fi', fl: 'fl', ff: 'ff', ffi: 'ffi', ffl: 'ffl', space: ' ', hyphen: '-', minus: '−', period: '.', comma: ',', colon: ':',
436
+ semicolon: ';', exclam: '!', question: '?', quoteright: '’', quoteleft: '‘', quotedblleft: '“', quotedblright: '”',
437
+ quotesingle: "'", quotedbl: '"', endash: '–', emdash: '—', bullet: '•', ellipsis: '…', parenleft: '(', parenright: ')',
438
+ bracketleft: '[', bracketright: ']', braceleft: '{', braceright: '}', slash: '/', backslash: '\\', bar: '|', at: '@',
439
+ numbersign: '#', dollar: '$', percent: '%', ampersand: '&', asterisk: '*', plus: '+', equal: '=', less: '<', greater: '>',
440
+ underscore: '_', asciitilde: '~', asciicircum: '^', grave: '`', zero: '0', one: '1', two: '2', three: '3', four: '4',
441
+ five: '5', six: '6', seven: '7', eight: '8', nine: '9', section: '§', dagger: '†', degree: '°', copyright: '©',
442
+ registered: '®', trademark: '™',
443
+ };
444
+ /** A simple font's /Differences as text, where each glyph name says what it is. */
445
+ export function differences(v) {
446
+ if (!Array.isArray(v))
447
+ return null;
448
+ const map = new Map();
449
+ let code = 0;
450
+ for (const x of v) {
451
+ if (typeof x === 'number')
452
+ code = x;
453
+ else if (x instanceof Name) {
454
+ const n = x.n;
455
+ const said = /^[A-Za-z]$/.test(n) ? n : /^uni[0-9A-Fa-f]{4}$/.test(n) ? String.fromCharCode(parseInt(n.slice(3), 16)) : GLYPHS[n];
456
+ if (said !== undefined)
457
+ map.set(code, said);
458
+ code++;
459
+ }
460
+ }
461
+ return map.size ? map : null;
462
+ }
463
+ /** The advances a font lists: a simple font's /Widths from /FirstChar, a composite font's /W. */
464
+ function advances(doc, f) {
465
+ const advance = new Map();
466
+ const kid = dict(resolve(doc, resolve(doc, f.get('DescendantFonts'))?.[0]));
467
+ if (kid.size) {
468
+ const w = resolve(doc, kid.get('W'));
469
+ const list = Array.isArray(w) ? w.map((x) => resolve(doc, x)) : [];
470
+ for (let i = 0; i < list.length;) {
471
+ const c = num(list[i]);
472
+ const next = list[i + 1];
473
+ if (Array.isArray(next)) {
474
+ next.forEach((x, k) => advance.set(c + k, num(resolve(doc, x))));
475
+ i += 2;
476
+ }
477
+ else {
478
+ const to = num(next);
479
+ const v = num(list[i + 2]);
480
+ for (let k = c; k <= to && k - c < 0xffff; k++)
481
+ advance.set(k, v);
482
+ i += 3;
483
+ }
484
+ }
485
+ const dw = num(resolve(doc, kid.get('DW')));
486
+ return { advance, dw: Number.isFinite(dw) ? dw : 1000 };
487
+ }
488
+ const first = num(resolve(doc, f.get('FirstChar')));
489
+ const w = resolve(doc, f.get('Widths'));
490
+ if (Array.isArray(w) && Number.isInteger(first))
491
+ w.forEach((x, k) => advance.set(first + k, num(resolve(doc, x))));
492
+ const missing = num(resolve(doc, dict(resolve(doc, f.get('FontDescriptor'))).get('MissingWidth')));
493
+ return { advance, dw: missing > 0 ? missing : 500 };
494
+ }
495
+ async function font(doc, v) {
496
+ const f = dict(resolve(doc, v));
497
+ const width = name(f.get('Subtype')) === 'Type0' ? 2 : 1;
498
+ const to = f.get('ToUnicode');
499
+ const o = to instanceof Ref ? doc.objects.get(to.n) : undefined;
500
+ const data = o ? await decode(doc, o) : null;
501
+ const read = data ? cmap(latin(data), width) : { width, map: width === 1 ? differences(resolve(doc, dict(resolve(doc, f.get('Encoding'))).get('Differences'))) : null };
502
+ return { ...read, ...advances(doc, f) };
503
+ }
504
+ /** WinAnsi's 0x80–0x9F, where it parts from Latin-1: how a simple font with no map is read. */
505
+ const WIN = '€\u0081‚ƒ„…†‡ˆ‰Š‹Œ\u008dŽ\u008f\u0090‘’“”•–—˜™š›œ\u009džŸ';
506
+ /** The codes a string holds, `width` bytes each. */
507
+ export function codes(s, width) {
508
+ const out = [];
509
+ for (let i = 0; i + width <= s.length; i += width) {
510
+ let code = 0;
511
+ for (let j = 0; j < width; j++)
512
+ code = (code << 8) | s.charCodeAt(i + j);
513
+ out.push(code);
514
+ }
515
+ return out;
516
+ }
517
+ /** What one code says in a font. A two-byte font with no map says nothing: its codes are glyph numbers. */
518
+ export function say(code, f) {
519
+ const said = f?.map?.get(code);
520
+ if (said !== undefined)
521
+ return said;
522
+ if ((f?.width ?? 1) > 1)
523
+ return '';
524
+ return code >= 0x80 && code < 0xa0 ? WIN[code - 0x80] : String.fromCharCode(code);
525
+ }
526
+ /**
527
+ * The text a content stream draws, in the fonts its resources name, and in the
528
+ * forms it draws. The pen is followed through the text operators with each
529
+ * font's advances, so a move down a line breaks the line and a move right past
530
+ * where the last word ended is a space, whichever way the producer placed it.
531
+ */
532
+ async function draw(doc, content, resources, depth = 0) {
533
+ const fonts = dict(resolve(doc, resources.get('Font')));
534
+ const forms = dict(resolve(doc, resources.get('XObject')));
535
+ const known = new Map();
536
+ let f;
537
+ let size = 0;
538
+ let m = [1, 0, 0, 1];
539
+ let lx = 0;
540
+ let ly = 0;
541
+ let x = 0;
542
+ let y = 0;
543
+ let leading = 0;
544
+ let out = '';
545
+ const across = () => Math.abs(size * Math.hypot(m[0], m[1])) || 10;
546
+ const up = () => Math.abs(size * Math.hypot(m[2], m[3])) || 10;
547
+ const line = () => {
548
+ if (out && !out.endsWith('\n'))
549
+ out += '\n';
550
+ };
551
+ const gap = () => {
552
+ if (out && !/\s$/.test(out))
553
+ out += ' ';
554
+ };
555
+ const move = (nx, ny) => {
556
+ if (out && !out.endsWith('\n')) {
557
+ if (Math.abs(ny - y) > 0.5 * up())
558
+ line();
559
+ else if (nx > x + 0.15 * across())
560
+ gap();
561
+ }
562
+ x = nx;
563
+ y = ny;
564
+ };
565
+ const td = (tx, ty) => {
566
+ lx += tx * m[0] + ty * m[2];
567
+ ly += tx * m[1] + ty * m[3];
568
+ move(lx, ly);
569
+ };
570
+ const put = (s) => {
571
+ for (const c of codes(s, f?.width ?? 1)) {
572
+ out += say(c, f);
573
+ x += (((f?.advance.get(c) ?? f?.dw ?? 500) / 1000) * across());
574
+ }
575
+ };
576
+ const lex = new Lexer(content);
577
+ const args = [];
578
+ for (let v = lex.next(); v !== undefined; v = lex.next()) {
579
+ if (!(v instanceof Op)) {
580
+ args.push(v);
581
+ continue;
582
+ }
583
+ const op = v.o;
584
+ const n = (i) => (typeof args[i] === 'number' ? args[i] : 0);
585
+ if (op === 'BT') {
586
+ m = [1, 0, 0, 1];
587
+ lx = ly = 0;
588
+ }
589
+ else if (op === 'Tf' && args[0] instanceof Name) {
590
+ const id = args[0].n;
591
+ if (!known.has(id))
592
+ known.set(id, await font(doc, fonts.get(id)));
593
+ f = known.get(id);
594
+ size = n(1);
595
+ }
596
+ else if (op === 'Tm') {
597
+ m = [n(0), n(1), n(2), n(3)];
598
+ lx = n(4);
599
+ ly = n(5);
600
+ move(lx, ly);
601
+ }
602
+ else if (op === 'Td')
603
+ td(n(0), n(1));
604
+ else if (op === 'TD') {
605
+ leading = -n(1);
606
+ td(n(0), n(1));
607
+ }
608
+ else if (op === 'TL')
609
+ leading = n(0);
610
+ else if (op === 'T*')
611
+ td(0, -leading);
612
+ else if (op === 'Tj' && args[0] instanceof Str)
613
+ put(args[0].s);
614
+ else if ((op === "'" || op === '"') && args[args.length - 1] instanceof Str) {
615
+ td(0, -leading);
616
+ line();
617
+ put(args[args.length - 1].s);
618
+ }
619
+ else if (op === 'TJ' && Array.isArray(args[0])) {
620
+ for (const t of args[0]) {
621
+ if (t instanceof Str)
622
+ put(t.s);
623
+ else if (typeof t === 'number') {
624
+ const shift = (-t / 1000) * across();
625
+ x += shift;
626
+ if (shift > 0.15 * across())
627
+ gap();
628
+ }
629
+ }
630
+ }
631
+ else if (op === 'BI') {
632
+ // An inline picture: its bytes run from ID to EI and are not syntax.
633
+ const id = content.indexOf('ID', lex.at);
634
+ const ei = id < 0 ? -1 : content.slice(id).search(/\sEI(?=\s|$)/);
635
+ lex.at = ei < 0 ? content.length : id + ei + 3;
636
+ }
637
+ else if (op === 'Do' && args[0] instanceof Name && depth < 3) {
638
+ const ref = forms.get(args[0].n);
639
+ const o = ref instanceof Ref ? doc.objects.get(ref.n) : undefined;
640
+ const d = dict(o?.value);
641
+ if (o && name(d.get('Subtype')) === 'Form') {
642
+ const data = await decode(doc, o);
643
+ if (data) {
644
+ const inner = await draw(doc, latin(data), d.has('Resources') ? dict(resolve(doc, d.get('Resources'))) : resources, depth + 1);
645
+ if (inner) {
646
+ line();
647
+ out += inner;
648
+ line();
649
+ }
650
+ }
651
+ }
652
+ }
653
+ args.length = 0;
654
+ }
655
+ return out;
656
+ }
657
+ /** Each page's text, in reading order. */
658
+ export async function text(doc) {
659
+ const out = [];
660
+ for (const { page, resources } of pages(doc)) {
661
+ // Contents is one stream or an array of them, either one possibly behind a reference.
662
+ const c = page.get('Contents');
663
+ const all = resolve(doc, c);
664
+ const parts = [];
665
+ for (const r of Array.isArray(all) ? all : [c]) {
666
+ const o = r instanceof Ref ? doc.objects.get(r.n) : undefined;
667
+ const data = o ? await decode(doc, o) : null;
668
+ if (data)
669
+ parts.push(latin(data));
670
+ }
671
+ const said = await draw(doc, parts.join('\n'), resources);
672
+ out.push(said
673
+ .split('\n')
674
+ .map((l) => l.replace(/[\x00-\x08\x0b-\x1f]/g, '').replace(/\s+$/, ''))
675
+ .join('\n')
676
+ .replace(/\n{3,}/g, '\n\n')
677
+ .trim());
678
+ }
679
+ return out;
680
+ }
681
+ /** A PDF text string: UTF-16 or UTF-8 with its mark, else one byte a character. */
682
+ export function words(s) {
683
+ if (!(s instanceof Str))
684
+ return '';
685
+ const t = s.s;
686
+ if (t.startsWith('\xfe\xff'))
687
+ return be16(t.slice(2));
688
+ if (t.startsWith('\xef\xbb\xbf'))
689
+ return new TextDecoder().decode(bytesOf(t.slice(3)));
690
+ return t;
691
+ }
692
+ /** A PDF date (`D:20261020113000-07'00'`) as a person reads it, or the string as it was. */
693
+ export function date(s) {
694
+ const m = /^D?:?(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?([Z+-])?(\d{2})?'?(\d{2})?/.exec(s);
695
+ if (!m)
696
+ return s;
697
+ const [, y, mo = '01', d = '01', h = '00', mi = '00', se = '00', z, zh, zm = '00'] = m;
698
+ const zone = !z ? '' : z === 'Z' ? ' UTC' : ` ${z}${zh ?? '00'}:${zm}`;
699
+ return `${y}-${mo}-${d} ${h}:${mi}:${se}${zone}`;
700
+ }
701
+ const INFO = ['Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer', 'CreationDate', 'ModDate'];
702
+ /** The OID of the CMS messageDigest attribute, 1.2.840.113549.1.9.4, as DER. */
703
+ const MESSAGE_DIGEST = '\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x09\x04';
704
+ const DIGESTS = { 20: 'SHA-1', 32: 'SHA-256', 48: 'SHA-384', 64: 'SHA-512' };
705
+ /** A DER length at `i`: its value and where the content starts. */
706
+ function length(s, i) {
707
+ const b = s.charCodeAt(i);
708
+ if (b < 0x80)
709
+ return [b, i + 1];
710
+ let n = 0;
711
+ for (let k = 1; k <= (b & 0x7f); k++)
712
+ n = n * 256 + s.charCodeAt(i + k);
713
+ return [n, i + 1 + (b & 0x7f)];
714
+ }
715
+ /** The digest a CMS signature says it signed: the value of its messageDigest attribute, or ''. */
716
+ export function claimed(der) {
717
+ const at = der.indexOf(MESSAGE_DIGEST);
718
+ if (at < 0)
719
+ return '';
720
+ let i = at + MESSAGE_DIGEST.length;
721
+ if (der.charCodeAt(i) !== 0x31)
722
+ return '';
723
+ [, i] = length(der, i + 1);
724
+ if (der.charCodeAt(i) !== 0x04)
725
+ return '';
726
+ const [n, from] = length(der, i + 1);
727
+ return der.slice(from, from + n);
728
+ }
729
+ async function signature(doc, d) {
730
+ const range = resolve(doc, d.get('ByteRange'));
731
+ const [a, b, c, e] = (Array.isArray(range) ? range : []).map((x) => num(resolve(doc, x)));
732
+ const filter = name(resolve(doc, d.get('SubFilter')));
733
+ const n = doc.bytes.length;
734
+ const ok = [a, b, c, e].every((x) => Number.isInteger(x) && x >= 0) && a + b <= c && c + e <= n;
735
+ const framed = ok && a === 0 && doc.src[b] === '<' && doc.src[c - 1] === '>';
736
+ const out = {
737
+ name: words(resolve(doc, d.get('Name'))),
738
+ reason: words(resolve(doc, d.get('Reason'))),
739
+ location: words(resolve(doc, d.get('Location'))),
740
+ time: date(words(resolve(doc, d.get('M')))),
741
+ filter,
742
+ framed,
743
+ after: ok ? n - (c + e) : n,
744
+ intact: null,
745
+ digest: '',
746
+ };
747
+ if (!ok) {
748
+ out.digest = 'its byte range does not fit the file';
749
+ return out;
750
+ }
751
+ if (filter && !/detached$/i.test(filter)) {
752
+ out.digest = `${filter} is not checked here`;
753
+ return out;
754
+ }
755
+ const der = resolve(doc, d.get('Contents'));
756
+ const want = claimed(der instanceof Str ? der.s : '');
757
+ const algo = DIGESTS[want.length];
758
+ if (!algo) {
759
+ out.digest = 'the signature names no digest that can be checked here';
760
+ return out;
761
+ }
762
+ const signed = new Uint8Array(b + e);
763
+ signed.set(doc.bytes.subarray(a, a + b), 0);
764
+ signed.set(doc.bytes.subarray(c, c + e), b);
765
+ const got = latin(new Uint8Array(await crypto.subtle.digest(algo, signed)));
766
+ out.intact = got === want;
767
+ out.digest = algo;
768
+ return out;
769
+ }
770
+ /** What the file's structure says about it. */
771
+ export async function check(doc) {
772
+ const { src } = doc;
773
+ const problems = [];
774
+ const version = /%PDF-(\d\.\d)/.exec(src.slice(0, 1024))?.[1] ?? '';
775
+ if (!version)
776
+ problems.push('It does not start with a PDF header.');
777
+ if (!src.slice(-1024).includes('%%EOF'))
778
+ problems.push('It does not end with %%EOF, so it may be cut short.');
779
+ const starts = [...src.matchAll(/startxref\s+(\d+)/g)];
780
+ const last = starts[starts.length - 1];
781
+ if (!last)
782
+ problems.push('It has no startxref, so a reader cannot find its objects without rebuilding the index.');
783
+ else {
784
+ const at = Number(last[1]);
785
+ if (!src.startsWith('xref', at) && !/^\d+\s+\d+\s+obj/.test(src.slice(at, at + 32)))
786
+ problems.push(`Its index is not where the file says (byte ${at}): it was edited by something that did not rewrite it, or it is damaged.`);
787
+ }
788
+ const list = pages(doc);
789
+ const root = trailer(doc, 'Root');
790
+ const count = root ? num(resolve(doc, dict(resolve(doc, dict(resolve(doc, root)).get('Pages'))).get('Count'))) : NaN;
791
+ if (!root)
792
+ problems.push('It names no document catalog (/Root).');
793
+ else if (Number.isInteger(count) && count !== list.length)
794
+ problems.push(`Its page tree says ${count} pages and holds ${list.length}.`);
795
+ const info = {};
796
+ const where = trailer(doc, 'Info');
797
+ const raw = where ? dict(resolve(doc, where)) : new Map();
798
+ for (const k of INFO) {
799
+ const v = words(resolve(doc, raw.get(k)));
800
+ if (v)
801
+ info[k] = k.endsWith('Date') ? date(v) : v;
802
+ }
803
+ const signatures = [];
804
+ for (const o of doc.objects.values()) {
805
+ const d = dict(o.value);
806
+ if (d.has('ByteRange') && d.has('Contents'))
807
+ signatures.push(await signature(doc, d));
808
+ }
809
+ return {
810
+ version,
811
+ pages: list.length,
812
+ encrypted: /\/Encrypt(?![A-Za-z])/.test(src),
813
+ saves: Math.max(1, (src.match(/%%EOF/g) ?? []).length),
814
+ info,
815
+ problems,
816
+ signatures,
817
+ };
818
+ }
819
+ /** Read a PDF: its structure, and its text when it is not encrypted. */
820
+ export async function read(bytes) {
821
+ if (bytes.length > LARGEST)
822
+ throw new Error(`It is larger than ${LARGEST >> 20} MB`);
823
+ const doc = await open(bytes);
824
+ const c = await check(doc);
825
+ return { check: c, text: c.encrypted ? [] : await text(doc) };
826
+ }
827
+ /** What a read found, in words a model is given. */
828
+ export function report(file, r) {
829
+ const c = r.check;
830
+ const lines = [`${file}: PDF ${c.version || '(no version)'}, ${c.pages} page${c.pages === 1 ? '' : 's'}.`];
831
+ const meta = Object.entries(c.info).map(([k, v]) => `${k}: ${v}`);
832
+ if (meta.length)
833
+ lines.push(`Metadata — ${meta.join('; ')}.`);
834
+ if (c.encrypted)
835
+ lines.push('It is encrypted, so its text could not be read here.');
836
+ lines.push(c.saves > 1 ? `It was changed after it was first written: saved ${c.saves} times.` : 'It is as first written: saved once.');
837
+ lines.push(c.problems.length ? `Structure problems: ${c.problems.join(' ')}` : 'Its structure checks out: header, index and page tree agree.');
838
+ if (!c.signatures.length)
839
+ lines.push('It carries no digital signature.');
840
+ c.signatures.forEach((s, i) => {
841
+ const who = [s.name && `by ${s.name}`, s.time && `at ${s.time}`, s.reason && `for "${s.reason}"`].filter(Boolean).join(' ');
842
+ const covers = !s.framed ? 'its byte range does not frame its own signature' : s.after ? `${s.after} bytes were added after it was signed` : 'it covers the whole file';
843
+ const digest = s.intact === null ? `the digest was not checked (${s.digest})` : s.intact ? `the signed bytes are intact (${s.digest} matches)` : `the signed bytes were changed (${s.digest} does not match)`;
844
+ lines.push(`Signature ${i + 1}${who ? ` ${who}` : ''}: ${covers}; ${digest}. Whether the signer's certificate is trusted was not checked.`);
845
+ });
846
+ return lines.join('\n');
847
+ }