@doccloak/core 0.12.1 → 0.14.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +386 -0
  2. package/NOTICE +5 -0
  3. package/README.md +148 -9
  4. package/dist/dom/errors.d.ts +1 -1
  5. package/dist/dom/ocr.d.ts +33 -7
  6. package/dist/dom/ocr.js +98 -4
  7. package/dist/dom/office.d.ts +22 -6
  8. package/dist/dom/office.js +45 -11
  9. package/dist/engine.js +4 -5
  10. package/dist/env.d.ts +3 -3
  11. package/dist/pdf/cmap.d.ts +83 -0
  12. package/dist/pdf/cmap.js +659 -0
  13. package/dist/pdf/encodings.d.ts +49 -0
  14. package/dist/pdf/encodings.js +181 -0
  15. package/dist/pdf/extract.d.ts +108 -0
  16. package/dist/pdf/extract.js +1164 -0
  17. package/dist/pdf/fallback-fonts.d.ts +146 -0
  18. package/dist/pdf/fallback-fonts.js +312 -0
  19. package/dist/pdf/fit.d.ts +35 -0
  20. package/dist/pdf/fit.js +60 -0
  21. package/dist/pdf/fonts.d.ts +82 -0
  22. package/dist/pdf/fonts.js +465 -0
  23. package/dist/pdf/glyphlist.d.ts +37 -0
  24. package/dist/pdf/glyphlist.js +485 -0
  25. package/dist/pdf/index.d.ts +21 -0
  26. package/dist/pdf/index.js +7 -0
  27. package/dist/pdf/lexer.d.ts +61 -0
  28. package/dist/pdf/lexer.js +554 -0
  29. package/dist/pdf/objects.d.ts +57 -0
  30. package/dist/pdf/objects.js +217 -0
  31. package/dist/pdf/oracle.d.ts +149 -0
  32. package/dist/pdf/oracle.js +384 -0
  33. package/dist/pdf/raster.d.ts +119 -0
  34. package/dist/pdf/raster.js +405 -0
  35. package/dist/pdf/read.d.ts +36 -0
  36. package/dist/pdf/read.js +65 -0
  37. package/dist/pdf/serializer.d.ts +21 -0
  38. package/dist/pdf/serializer.js +106 -0
  39. package/dist/pdf/standard-metrics.d.ts +27 -0
  40. package/dist/pdf/standard-metrics.js +72 -0
  41. package/dist/pdf/surgery.d.ts +131 -0
  42. package/dist/pdf/surgery.js +847 -0
  43. package/dist/pdf/textstate.d.ts +88 -0
  44. package/dist/pdf/textstate.js +293 -0
  45. package/dist/pdf/types.d.ts +206 -0
  46. package/dist/pdf/types.js +11 -0
  47. package/dist/pdf/verify.d.ts +33 -0
  48. package/dist/pdf/verify.js +446 -0
  49. package/dist/pdf/write.d.ts +62 -0
  50. package/dist/pdf/write.js +750 -0
  51. package/dist/pipeline.d.ts +15 -0
  52. package/dist/pipeline.js +123 -8
  53. package/dist/providers/bardsai.d.ts +1 -1
  54. package/dist/providers/bardsai.js +5 -1
  55. package/dist/providers/gliner-base.d.ts +1 -1
  56. package/dist/providers/gliner-base.js +5 -1
  57. package/dist/providers/gliner.d.ts +1 -1
  58. package/dist/providers/gliner.js +5 -1
  59. package/dist/regex/rules.data.js +13 -8
  60. package/dist/types.d.ts +17 -2
  61. package/dist/types.js +17 -0
  62. package/dist/worker-protocol.d.ts +11 -0
  63. package/dist/worker-protocol.js +41 -8
  64. package/fonts/liberation/LICENSE +102 -0
  65. package/fonts/liberation/LiberationMono-Bold.ttf +0 -0
  66. package/fonts/liberation/LiberationMono-BoldItalic.ttf +0 -0
  67. package/fonts/liberation/LiberationMono-Italic.ttf +0 -0
  68. package/fonts/liberation/LiberationMono-Regular.ttf +0 -0
  69. package/fonts/liberation/LiberationSans-Bold.ttf +0 -0
  70. package/fonts/liberation/LiberationSans-BoldItalic.ttf +0 -0
  71. package/fonts/liberation/LiberationSans-Italic.ttf +0 -0
  72. package/fonts/liberation/LiberationSans-Regular.ttf +0 -0
  73. package/fonts/liberation/LiberationSerif-Bold.ttf +0 -0
  74. package/fonts/liberation/LiberationSerif-BoldItalic.ttf +0 -0
  75. package/fonts/liberation/LiberationSerif-Italic.ttf +0 -0
  76. package/fonts/liberation/LiberationSerif-Regular.ttf +0 -0
  77. package/package.json +11 -2
  78. package/rules/pl.json +7 -3
  79. package/rules/universal.json +6 -5
@@ -0,0 +1,554 @@
1
+ /**
2
+ * @doccloak/core/pdf - content stream lexer (T206).
3
+ *
4
+ * Tokenizes a PDF content stream (ISO 32000-1 section 7.2 and 7.8.2) into
5
+ * operators with typed operands, keeping the source byte range of every
6
+ * operator so untouched operators can be copied back verbatim by the
7
+ * serializer. Inline images (BI ... ID <binary> EI) are kept as one opaque
8
+ * operator holding the raw bytes. Unknown operators are kept as-is.
9
+ *
10
+ * The lexer never throws on malformed input: an unparsable byte sequence
11
+ * ends up as an operator with `op: ''` and the raw bytes, which the callers
12
+ * treat as "unsafe page" (rasterize) rather than silently dropping content.
13
+ */
14
+ const WHITESPACE = new Set([0x00, 0x09, 0x0a, 0x0c, 0x0d, 0x20]);
15
+ const DELIMITERS = new Set([0x28, 0x29, 0x3c, 0x3e, 0x5b, 0x5d, 0x7b, 0x7d, 0x2f, 0x25]);
16
+ function isWhitespace(b) {
17
+ return WHITESPACE.has(b);
18
+ }
19
+ function isDelimiter(b) {
20
+ return DELIMITERS.has(b);
21
+ }
22
+ function isRegular(b) {
23
+ return !isWhitespace(b) && !isDelimiter(b);
24
+ }
25
+ function isDigitLike(b) {
26
+ return (b >= 0x30 && b <= 0x39) || b === 0x2b || b === 0x2d || b === 0x2e;
27
+ }
28
+ class Scanner {
29
+ constructor(bytes) {
30
+ this.pos = 0;
31
+ this.bytes = bytes;
32
+ }
33
+ skipWhitespaceAndComments() {
34
+ const b = this.bytes;
35
+ while (this.pos < b.length) {
36
+ const c = b[this.pos];
37
+ if (isWhitespace(c)) {
38
+ this.pos++;
39
+ }
40
+ else if (c === 0x25) {
41
+ while (this.pos < b.length && b[this.pos] !== 0x0a && b[this.pos] !== 0x0d)
42
+ this.pos++;
43
+ }
44
+ else {
45
+ break;
46
+ }
47
+ }
48
+ }
49
+ next() {
50
+ this.skipWhitespaceAndComments();
51
+ const b = this.bytes;
52
+ const start = this.pos;
53
+ if (this.pos >= b.length)
54
+ return { type: 'eof', start, end: start };
55
+ const c = b[this.pos];
56
+ if (c === 0x5b) {
57
+ this.pos++;
58
+ return { type: 'arrayOpen', start, end: this.pos };
59
+ }
60
+ if (c === 0x5d) {
61
+ this.pos++;
62
+ return { type: 'arrayClose', start, end: this.pos };
63
+ }
64
+ if (c === 0x3c) {
65
+ if (b[this.pos + 1] === 0x3c) {
66
+ this.pos += 2;
67
+ return { type: 'dictOpen', start, end: this.pos };
68
+ }
69
+ return this.hexString(start);
70
+ }
71
+ if (c === 0x3e) {
72
+ if (b[this.pos + 1] === 0x3e) {
73
+ this.pos += 2;
74
+ return { type: 'dictClose', start, end: this.pos };
75
+ }
76
+ // A lone '>' is malformed; treat as a keyword so the caller marks the op unsafe.
77
+ this.pos++;
78
+ return { type: 'keyword', value: '>', start, end: this.pos };
79
+ }
80
+ if (c === 0x28)
81
+ return this.literalString(start);
82
+ if (c === 0x2f)
83
+ return this.name(start);
84
+ if (c === 0x7b || c === 0x7d) {
85
+ this.pos++;
86
+ return { type: 'keyword', value: String.fromCharCode(c), start, end: this.pos };
87
+ }
88
+ if (c === 0x29) {
89
+ this.pos++;
90
+ return { type: 'keyword', value: ')', start, end: this.pos };
91
+ }
92
+ if (isDigitLike(c))
93
+ return this.number(start);
94
+ // Regular characters: keyword (operator, true/false/null)
95
+ while (this.pos < b.length && isRegular(b[this.pos]))
96
+ this.pos++;
97
+ const word = latin1(b.subarray(start, this.pos));
98
+ if (word === 'true')
99
+ return { type: 'operand', operand: { kind: 'bool', value: true }, start, end: this.pos };
100
+ if (word === 'false')
101
+ return { type: 'operand', operand: { kind: 'bool', value: false }, start, end: this.pos };
102
+ if (word === 'null')
103
+ return { type: 'operand', operand: { kind: 'null' }, start, end: this.pos };
104
+ return { type: 'keyword', value: word, start, end: this.pos };
105
+ }
106
+ number(start) {
107
+ const b = this.bytes;
108
+ while (this.pos < b.length && isRegular(b[this.pos]))
109
+ this.pos++;
110
+ const text = latin1(b.subarray(start, this.pos));
111
+ const value = parsePdfNumber(text);
112
+ if (value === null)
113
+ return { type: 'keyword', value: text, start, end: this.pos };
114
+ return { type: 'operand', operand: { kind: 'number', value }, start, end: this.pos };
115
+ }
116
+ name(start) {
117
+ const b = this.bytes;
118
+ this.pos++; // '/'
119
+ let out = '';
120
+ while (this.pos < b.length && isRegular(b[this.pos])) {
121
+ const c = b[this.pos];
122
+ if (c === 0x23 && this.pos + 2 < b.length + 1) {
123
+ const hex = latin1(b.subarray(this.pos + 1, this.pos + 3));
124
+ if (/^[0-9a-fA-F]{2}$/.test(hex)) {
125
+ out += String.fromCharCode(parseInt(hex, 16));
126
+ this.pos += 3;
127
+ continue;
128
+ }
129
+ }
130
+ out += String.fromCharCode(c);
131
+ this.pos++;
132
+ }
133
+ return { type: 'operand', operand: { kind: 'name', value: out }, start, end: this.pos };
134
+ }
135
+ hexString(start) {
136
+ const b = this.bytes;
137
+ this.pos++; // '<'
138
+ const digits = [];
139
+ while (this.pos < b.length && b[this.pos] !== 0x3e) {
140
+ const c = b[this.pos];
141
+ const d = hexDigit(c);
142
+ if (d >= 0)
143
+ digits.push(d);
144
+ // Whitespace is ignored; anything else is malformed but tolerated (skipped) like viewers do.
145
+ this.pos++;
146
+ }
147
+ this.pos++; // '>'
148
+ if (digits.length % 2 === 1)
149
+ digits.push(0);
150
+ const bytes = new Uint8Array(digits.length / 2);
151
+ for (let i = 0; i < bytes.length; i++)
152
+ bytes[i] = (digits[2 * i] << 4) | digits[2 * i + 1];
153
+ return { type: 'operand', operand: { kind: 'string', bytes, hex: true }, start, end: this.pos };
154
+ }
155
+ literalString(start) {
156
+ const b = this.bytes;
157
+ this.pos++; // '('
158
+ let depth = 1;
159
+ const out = [];
160
+ while (this.pos < b.length) {
161
+ const c = b[this.pos++];
162
+ if (c === 0x5c) {
163
+ const n = b[this.pos++];
164
+ switch (n) {
165
+ case 0x6e:
166
+ out.push(0x0a);
167
+ break; // n
168
+ case 0x72:
169
+ out.push(0x0d);
170
+ break; // r
171
+ case 0x74:
172
+ out.push(0x09);
173
+ break; // t
174
+ case 0x62:
175
+ out.push(0x08);
176
+ break; // b
177
+ case 0x66:
178
+ out.push(0x0c);
179
+ break; // f
180
+ case 0x28:
181
+ out.push(0x28);
182
+ break;
183
+ case 0x29:
184
+ out.push(0x29);
185
+ break;
186
+ case 0x5c:
187
+ out.push(0x5c);
188
+ break;
189
+ case 0x0d:
190
+ if (b[this.pos] === 0x0a)
191
+ this.pos++;
192
+ break; // line continuation
193
+ case 0x0a: break;
194
+ default: {
195
+ if (n >= 0x30 && n <= 0x37) {
196
+ let v = n - 0x30;
197
+ for (let k = 0; k < 2 && b[this.pos] >= 0x30 && b[this.pos] <= 0x37; k++) {
198
+ v = v * 8 + (b[this.pos++] - 0x30);
199
+ }
200
+ out.push(v & 0xff);
201
+ }
202
+ else if (n !== undefined) {
203
+ out.push(n); // unknown escape: the backslash is ignored
204
+ }
205
+ }
206
+ }
207
+ }
208
+ else if (c === 0x28) {
209
+ depth++;
210
+ out.push(c);
211
+ }
212
+ else if (c === 0x29) {
213
+ depth--;
214
+ if (depth === 0)
215
+ break;
216
+ out.push(c);
217
+ }
218
+ else {
219
+ // Raw EOL bytes stay as they are (pdf.js keeps them; only escaped EOLs are line continuations),
220
+ // so the codes we match against are the ones the reference extractor sees.
221
+ out.push(c);
222
+ }
223
+ }
224
+ return { type: 'operand', operand: { kind: 'string', bytes: Uint8Array.from(out), hex: false }, start, end: this.pos };
225
+ }
226
+ }
227
+ function hexDigit(c) {
228
+ if (c >= 0x30 && c <= 0x39)
229
+ return c - 0x30;
230
+ if (c >= 0x41 && c <= 0x46)
231
+ return c - 0x41 + 10;
232
+ if (c >= 0x61 && c <= 0x66)
233
+ return c - 0x61 + 10;
234
+ return -1;
235
+ }
236
+ export function latin1(bytes) {
237
+ let s = '';
238
+ for (let i = 0; i < bytes.length; i++)
239
+ s += String.fromCharCode(bytes[i]);
240
+ return s;
241
+ }
242
+ /**
243
+ * PDF numbers: optional sign, digits, optional fraction; tolerant to '--5', '.5', '5.', '3.4.5' (viewers
244
+ * accept). Any '-' among the sign characters makes the number negative, as pdf.js and Acrobat read '--5'.
245
+ */
246
+ export function parsePdfNumber(text) {
247
+ if (!/^[+-]*(\d+\.?\d*|\.\d+)(\.\d*)*$/.test(text))
248
+ return null;
249
+ const negative = (text.match(/^[+-]*/)?.[0] ?? '').includes('-');
250
+ const body = text.replace(/^[+-]*/, '');
251
+ const firstDot = body.indexOf('.');
252
+ const cleaned = firstDot >= 0 ? body.slice(0, firstDot + 1) + body.slice(firstDot + 1).replace(/\./g, '') : body;
253
+ const value = Number(cleaned);
254
+ if (!Number.isFinite(value))
255
+ return null;
256
+ return negative ? -value : value;
257
+ }
258
+ const INLINE_IMAGE_ABBREV = {
259
+ BPC: 'BitsPerComponent', CS: 'ColorSpace', D: 'Decode', DP: 'DecodeParms', F: 'Filter',
260
+ H: 'Height', W: 'Width', IM: 'ImageMask', I: 'Interpolate', L: 'Length',
261
+ };
262
+ /** Tokenize a content stream into operators. Never throws. */
263
+ export function lexContent(bytes) {
264
+ const scanner = new Scanner(bytes);
265
+ const ops = [];
266
+ let operands = [];
267
+ let operandsStart = -1;
268
+ const flushUnsafe = (start, end) => {
269
+ ops.push({ op: '', operands: [], start, end, raw: bytes.subarray(start, end) });
270
+ };
271
+ for (;;) {
272
+ const tok = scanner.next();
273
+ if (tok.type === 'eof') {
274
+ if (operands.length > 0)
275
+ flushUnsafe(operandsStart, tok.end);
276
+ break;
277
+ }
278
+ if (tok.type === 'operand') {
279
+ if (operands.length === 0)
280
+ operandsStart = tok.start;
281
+ operands.push(tok.operand);
282
+ continue;
283
+ }
284
+ if (tok.type === 'arrayOpen' || tok.type === 'dictOpen') {
285
+ if (operands.length === 0)
286
+ operandsStart = tok.start;
287
+ const composite = readComposite(scanner, tok.type === 'arrayOpen' ? 'array' : 'dict');
288
+ if (composite === null) {
289
+ flushUnsafe(operandsStart, scanner.pos);
290
+ operands = [];
291
+ continue;
292
+ }
293
+ operands.push(composite);
294
+ continue;
295
+ }
296
+ if (tok.type === 'arrayClose' || tok.type === 'dictClose') {
297
+ // Stray closer: unsafe
298
+ flushUnsafe(operands.length ? operandsStart : tok.start, tok.end);
299
+ operands = [];
300
+ continue;
301
+ }
302
+ // keyword = operator
303
+ const start = operands.length ? operandsStart : tok.start;
304
+ if (tok.value === 'BI') {
305
+ const end = readInlineImage(scanner);
306
+ if (end < 0) {
307
+ // No EI: the rest of the stream is opaque to us (a viewer may still find text in it).
308
+ flushUnsafe(start, bytes.length);
309
+ break;
310
+ }
311
+ ops.push({ op: 'BI', operands: [], start, end, raw: bytes.subarray(start, end) });
312
+ operands = [];
313
+ continue;
314
+ }
315
+ if (!/^[A-Za-z'"*]{1,3}[0-9]?$/.test(tok.value)) {
316
+ flushUnsafe(start, tok.end);
317
+ operands = [];
318
+ continue;
319
+ }
320
+ ops.push({ op: tok.value, operands, start, end: tok.end });
321
+ operands = [];
322
+ }
323
+ return ops;
324
+ }
325
+ /** Nesting bound for arrays/dicts inside operands: deeper input is treated as unsafe instead of overflowing the stack. */
326
+ const MAX_COMPOSITE_DEPTH = 64;
327
+ function readComposite(scanner, kind, depth = 0) {
328
+ if (depth >= MAX_COMPOSITE_DEPTH)
329
+ return null;
330
+ if (kind === 'array') {
331
+ const items = [];
332
+ for (;;) {
333
+ const tok = scanner.next();
334
+ if (tok.type === 'eof')
335
+ return null;
336
+ if (tok.type === 'arrayClose')
337
+ return { kind: 'array', items };
338
+ if (tok.type === 'operand') {
339
+ items.push(tok.operand);
340
+ continue;
341
+ }
342
+ if (tok.type === 'arrayOpen' || tok.type === 'dictOpen') {
343
+ const inner = readComposite(scanner, tok.type === 'arrayOpen' ? 'array' : 'dict', depth + 1);
344
+ if (inner === null)
345
+ return null;
346
+ items.push(inner);
347
+ continue;
348
+ }
349
+ return null; // keyword or stray closer inside an array
350
+ }
351
+ }
352
+ const entries = new Map();
353
+ for (;;) {
354
+ const keyTok = scanner.next();
355
+ if (keyTok.type === 'eof')
356
+ return null;
357
+ if (keyTok.type === 'dictClose')
358
+ return { kind: 'dict', entries };
359
+ if (keyTok.type !== 'operand' || keyTok.operand.kind !== 'name')
360
+ return null;
361
+ const valTok = scanner.next();
362
+ let value = null;
363
+ if (valTok.type === 'operand')
364
+ value = valTok.operand;
365
+ else if (valTok.type === 'arrayOpen' || valTok.type === 'dictOpen') {
366
+ value = readComposite(scanner, valTok.type === 'arrayOpen' ? 'array' : 'dict', depth + 1);
367
+ }
368
+ if (value === null)
369
+ return null;
370
+ entries.set(keyTok.operand.value, value);
371
+ }
372
+ }
373
+ /**
374
+ * Consume an inline image after its BI keyword: the key/value pairs up to ID,
375
+ * one whitespace byte, then the binary data up to EI. Returns the end offset
376
+ * (just after EI), or -1 when the image never ends. When the data is unfiltered
377
+ * (or carries /Length) its length is computed; otherwise EI is searched for
378
+ * with the reference extractor's rules.
379
+ */
380
+ function readInlineImage(scanner) {
381
+ const b = scanner.bytes;
382
+ const params = new Map();
383
+ for (;;) {
384
+ const tok = scanner.next();
385
+ if (tok.type === 'eof')
386
+ return -1;
387
+ if (tok.type === 'keyword') {
388
+ if (tok.value === 'ID')
389
+ break;
390
+ continue;
391
+ }
392
+ if (tok.type === 'operand' && tok.operand.kind === 'name') {
393
+ const key = INLINE_IMAGE_ABBREV[tok.operand.value] ?? tok.operand.value;
394
+ const valTok = scanner.next();
395
+ if (valTok.type === 'operand')
396
+ params.set(key, valTok.operand);
397
+ else if (valTok.type === 'arrayOpen' || valTok.type === 'dictOpen') {
398
+ const v = readComposite(scanner, valTok.type === 'arrayOpen' ? 'array' : 'dict');
399
+ if (v)
400
+ params.set(key, v);
401
+ }
402
+ else if (valTok.type === 'eof')
403
+ return -1;
404
+ }
405
+ }
406
+ // Exactly one whitespace byte after ID
407
+ let dataStart = scanner.pos;
408
+ if (dataStart < b.length && isWhitespace(b[dataStart]))
409
+ dataStart++;
410
+ const filter = params.get('Filter');
411
+ const hasFilter = filter !== undefined && !(filter.kind === 'array' && filter.items.length === 0);
412
+ let dataEnd = -1;
413
+ const declared = numberParam(params, 'Length');
414
+ if (declared !== null && declared >= 0 && dataStart + declared <= b.length)
415
+ dataEnd = dataStart + declared;
416
+ if (dataEnd < 0 && !hasFilter) {
417
+ const w = numberParam(params, 'Width');
418
+ const h = numberParam(params, 'Height');
419
+ const bpc = params.get('ImageMask')?.kind === 'bool' && params.get('ImageMask').value ? 1 : numberParam(params, 'BitsPerComponent') ?? 8;
420
+ const ncomp = componentsFor(params.get('ColorSpace'));
421
+ if (w !== null && h !== null && ncomp !== null) {
422
+ const rowBytes = Math.ceil((w * bpc * ncomp) / 8);
423
+ dataEnd = dataStart + rowBytes * h;
424
+ if (dataEnd > b.length)
425
+ dataEnd = -1;
426
+ }
427
+ }
428
+ if (dataEnd < 0) {
429
+ const found = findInlineImageEnd(b, dataStart, filterNames(filter));
430
+ if (found < 0) {
431
+ scanner.pos = b.length;
432
+ return -1;
433
+ }
434
+ scanner.pos = found;
435
+ return found;
436
+ }
437
+ // Find EI after dataEnd
438
+ let p = dataEnd;
439
+ while (p < b.length && isWhitespace(b[p]))
440
+ p++;
441
+ if (b[p] === 0x45 && b[p + 1] === 0x49 && (p + 2 >= b.length || !isRegular(b[p + 2]))) {
442
+ scanner.pos = p + 2;
443
+ return scanner.pos;
444
+ }
445
+ // Length mismatch (filtered data shorter than computed, or garbage): fall back to the search.
446
+ const searched = findInlineImageEnd(b, dataStart, filterNames(filter));
447
+ if (searched < 0) {
448
+ scanner.pos = b.length;
449
+ return -1;
450
+ }
451
+ scanner.pos = searched;
452
+ return scanner.pos;
453
+ }
454
+ function filterNames(filter) {
455
+ if (!filter)
456
+ return [];
457
+ if (filter.kind === 'name')
458
+ return [filter.value];
459
+ if (filter.kind === 'array')
460
+ return filter.items.filter((it) => it.kind === 'name').map((it) => it.value);
461
+ return [];
462
+ }
463
+ /** Bytes pdf.js requires after a candidate EI: ASCII (or EOL) for this many bytes, else the EI is image data. */
464
+ const EI_LOOKAHEAD = 10;
465
+ /**
466
+ * Search for the end of inline image data the way pdf.js does: the first "EI" followed by
467
+ * whitespace (or EOF) whose next bytes are ASCII; a whitespace before the E is not required.
468
+ * DCT data ends at its EOI marker, ASCIIHex at '>', ASCII85 at '~>', so a false "EI" inside
469
+ * such data cannot end the image early. Returns the offset just after EI, or -1 when no end
470
+ * is found (the caller marks the rest of the stream unsafe).
471
+ */
472
+ function findInlineImageEnd(b, from, filters) {
473
+ let p = from;
474
+ const first = filters[0];
475
+ if (first === 'DCTDecode' || first === 'DCT') {
476
+ // Scan JPEG markers for EOI (FFD9); skip entropy-coded segments by their lengths.
477
+ for (let q = from; q + 1 < b.length; q++) {
478
+ if (b[q] === 0xff && b[q + 1] === 0xd9) {
479
+ p = q + 2;
480
+ break;
481
+ }
482
+ }
483
+ }
484
+ else if (first === 'ASCIIHexDecode' || first === 'AHx') {
485
+ const q = b.indexOf(0x3e, from);
486
+ if (q >= 0)
487
+ p = q + 1;
488
+ }
489
+ else if (first === 'ASCII85Decode' || first === 'A85') {
490
+ for (let q = from; q + 1 < b.length; q++) {
491
+ if (b[q] === 0x7e && b[q + 1] === 0x3e) {
492
+ p = q + 2;
493
+ break;
494
+ }
495
+ }
496
+ }
497
+ for (; p + 1 < b.length; p++) {
498
+ if (b[p] !== 0x45 || b[p + 1] !== 0x49)
499
+ continue;
500
+ if (p + 2 < b.length && !isWhitespace(b[p + 2]))
501
+ continue;
502
+ let ok = true;
503
+ for (let k = p + 2; k < Math.min(b.length, p + 2 + EI_LOOKAHEAD); k++) {
504
+ const c = b[k];
505
+ if (c !== 0x0a && c !== 0x0d && c !== 0x09 && (c < 0x20 || c > 0x7e)) {
506
+ ok = false;
507
+ break;
508
+ }
509
+ }
510
+ if (ok)
511
+ return p + 2;
512
+ }
513
+ return -1;
514
+ }
515
+ function numberParam(params, key) {
516
+ const v = params.get(key);
517
+ return v && v.kind === 'number' ? v.value : null;
518
+ }
519
+ function componentsFor(cs) {
520
+ if (!cs)
521
+ return 1;
522
+ if (cs.kind === 'name') {
523
+ switch (cs.value) {
524
+ case 'DeviceGray':
525
+ case 'G':
526
+ case 'CalGray':
527
+ case 'Indexed':
528
+ case 'I': return 1;
529
+ case 'DeviceRGB':
530
+ case 'RGB':
531
+ case 'CalRGB':
532
+ case 'Lab': return 3;
533
+ case 'DeviceCMYK':
534
+ case 'CMYK': return 4;
535
+ default: return null; // named resource: unknown component count
536
+ }
537
+ }
538
+ if (cs.kind === 'array' && cs.items[0]?.kind === 'name') {
539
+ const fam = cs.items[0].value;
540
+ if (fam === 'Indexed' || fam === 'I')
541
+ return 1;
542
+ return componentsFor(cs.items[0]);
543
+ }
544
+ return null;
545
+ }
546
+ /* ---------- operand constructors (for synthesized operators) ---------- */
547
+ export const num = (value) => ({ kind: 'number', value });
548
+ export const name = (value) => ({ kind: 'name', value });
549
+ export const hexString = (bytes) => ({ kind: 'string', bytes, hex: true });
550
+ export const array = (items) => ({ kind: 'array', items });
551
+ export const makeOp = (op, ...operands) => ({ op, operands });
552
+ /** Operator classes used by the state machine and the surgery. */
553
+ export const TEXT_SHOW_OPS = new Set(['Tj', 'TJ', "'", '"']);
554
+ export const TEXT_POSITION_OPS = new Set(['Td', 'TD', 'Tm', 'T*']);
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @doccloak/core/pdf - typed helpers over the pdf-lib object model (T206).
3
+ *
4
+ * pdf-lib (@cantoo/pdf-lib) parses the file into PDFContext objects; these
5
+ * helpers resolve indirect references and coerce values so the rest of the
6
+ * module never touches raw PDFObject subclasses in ad-hoc ways. All getters
7
+ * return undefined for missing or wrong-typed values instead of throwing:
8
+ * callers decide whether a missing value is an "unsafe page" or a default.
9
+ */
10
+ import { PDFArray, PDFBool, PDFContext, PDFDict, PDFHexString, PDFName, PDFNumber, PDFObject, PDFRawStream, PDFRef, PDFStream, PDFString } from '@cantoo/pdf-lib';
11
+ export type { PDFContext, PDFObject };
12
+ export { PDFName, PDFNumber, PDFString, PDFHexString, PDFRawStream, PDFBool, PDFArray, PDFDict, PDFRef, PDFStream };
13
+ /** Follow indirect references (bounded, refs can be chained). */
14
+ export declare function resolve(ctx: PDFContext, obj: PDFObject | undefined): PDFObject | undefined;
15
+ export declare function getDict(ctx: PDFContext, dict: PDFDict | undefined, key: string): PDFDict | undefined;
16
+ export declare function getStream(ctx: PDFContext, dict: PDFDict | undefined, key: string): PDFStream | undefined;
17
+ export declare function getArray(ctx: PDFContext, dict: PDFDict | undefined, key: string): PDFArray | undefined;
18
+ export declare function getName(ctx: PDFContext, dict: PDFDict | undefined, key: string): string | undefined;
19
+ export declare function getNumber(ctx: PDFContext, dict: PDFDict | undefined, key: string): number | undefined;
20
+ export declare function getBool(ctx: PDFContext, dict: PDFDict | undefined, key: string): boolean | undefined;
21
+ /** Raw bytes of a string object (literal or hex), without text decoding. */
22
+ export declare function stringBytes(obj: PDFObject | undefined): Uint8Array | undefined;
23
+ /** Decoded text of a string object (PDFDocEncoding / UTF-16 BOM aware). */
24
+ export declare function stringText(obj: PDFObject | undefined): string | undefined;
25
+ export declare function getString(ctx: PDFContext, dict: PDFDict | undefined, key: string): string | undefined;
26
+ /** Numbers of an array (non-numbers skipped). */
27
+ export declare function numbersOf(ctx: PDFContext, arr: PDFArray | undefined): number[];
28
+ /** Elements of an array, references resolved. */
29
+ export declare function itemsOf(ctx: PDFContext, arr: PDFArray | undefined): PDFObject[];
30
+ /** Names of a name-or-array-of-names value (e.g. /Filter). */
31
+ export declare function namesOf(ctx: PDFContext, obj: PDFObject | undefined): string[];
32
+ export declare class StreamDecodeError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ /**
36
+ * Fully decoded bytes of a stream. Throws StreamDecodeError when a filter is
37
+ * not decodable (JPX, DCT, CCITT, JBIG2, Crypt) or the data is corrupt, so
38
+ * the caller can treat the page as unsafe instead of reading garbage.
39
+ */
40
+ export declare function decodeStream(ctx: PDFContext, stream: PDFStream): Uint8Array;
41
+ /** True when the filter list contains an image codec (the stream holds pixels, not operators). */
42
+ export declare function isImageEncoded(ctx: PDFContext, stream: PDFStream): boolean;
43
+ /** Stable key for an object: 'r<num>g<gen>' for refs, or a synthetic key. */
44
+ export declare function refKey(ref: PDFRef): string;
45
+ /** Find the ref of an object stored directly in a dict entry (undefined for direct objects). */
46
+ export declare function refOf(dict: PDFDict, key: string): PDFRef | undefined;
47
+ /** Iterate (key, value) pairs of a dict with references resolved. */
48
+ export declare function entriesOf(ctx: PDFContext, dict: PDFDict): Array<[string, PDFObject]>;
49
+ /** The content streams of a page: /Contents may be a stream or an array of streams. */
50
+ export declare function pageContentStreams(ctx: PDFContext, pageDict: PDFDict): Array<{
51
+ ref: PDFRef | undefined;
52
+ stream: PDFStream;
53
+ }>;
54
+ /** Resources dict of a page, walking up the /Parent chain when inherited. */
55
+ export declare function pageResources(ctx: PDFContext, pageDict: PDFDict): PDFDict | undefined;
56
+ /** Inherited page attribute (MediaBox, CropBox, Rotate). */
57
+ export declare function inheritedPageValue(ctx: PDFContext, pageDict: PDFDict, key: string): PDFObject | undefined;