layoutbridge-light 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.de.md +31 -2
  3. package/README.md +31 -3
  4. package/THIRD-PARTY-NOTICES.md +2 -2
  5. package/dist/src/mcp/catalog.js +9 -4
  6. package/dist/src/mcp/content-credentials.js +65 -0
  7. package/dist/src/mcp/dispatch.js +8 -1
  8. package/dist/src/mcp/license/polar-ids.js +1 -1
  9. package/dist/src/mcp/pdf-postprocess.js +798 -0
  10. package/dist/src/mcp/server.js +1 -1
  11. package/dist/src/mcp/target-folder.js +25 -0
  12. package/dist/src/mcp/tools.js +275 -20
  13. package/dist/src/mcp/typography-rules.js +1 -1
  14. package/dist/src/transport/com.js +224 -38
  15. package/dist/src/transport/transport.js +22 -0
  16. package/indesign/commands/25-export.js +258 -11
  17. package/indesign/commands/26-export-image.js +1 -1
  18. package/indesign/commands/31-view.js +100 -0
  19. package/indesign/commands/35-parents.js +44 -24
  20. package/indesign/commands/40-frames.js +35 -0
  21. package/indesign/commands/47-anchored.js +142 -0
  22. package/indesign/commands/50-modify.js +25 -3
  23. package/indesign/commands/60-images.js +28 -2
  24. package/indesign/commands/69-placeholders.js +7 -2
  25. package/indesign/commands/70-text.js +39 -4
  26. package/indesign/commands/75-tables.js +27 -1
  27. package/indesign/commands/78-frame-lines.js +59 -0
  28. package/indesign/commands/79-check-typography.js +143 -4
  29. package/indesign/commands/80-styles.js +105 -10
  30. package/indesign/commands/89-toc-index.js +61 -4
  31. package/indesign/commands/91-epub.js +92 -9
  32. package/indesign/commands/92-accessibility.js +1131 -0
  33. package/indesign/interpreter.js +51 -1
  34. package/package.json +1 -1
  35. package/rules/typography-rules.json +3 -2
@@ -0,0 +1,798 @@
1
+ /**
2
+ * Nachbearbeitung eines fertigen PDFs ohne InDesign (A-80, Sitzung 82,
3
+ * E-120 und E-121).
4
+ *
5
+ * 1. Ungetaggte Pfade als Artefakt: InDesign 21.6 schreibt Konturen und
6
+ * Flaechen von Textrahmen und alle Tabellen-Zellkonturen ausserhalb jeder
7
+ * markierten Sequenz (Live-Probe Sitzung 82; PAC: „Path object not
8
+ * tagged“). Solche Pfadobjekte werden in `/Artifact BMC … EMC`
9
+ * eingeschlossen.
10
+ * 2. PDF/UA-Kennung: `pdfuaid:part` im XMP des Dokuments (Entscheidung
11
+ * des Anbieters 11.11.2026, Mittelweg).
12
+ *
13
+ * Beides als inkrementelle Aktualisierung (ISO 32000-1, 7.5.6): Die
14
+ * Originalbytes bleiben, neue Fassungen der geaenderten Objekte, ein neuer
15
+ * Querverweisabschnitt (Tabelle oder Stream wie im Original) und ein neuer
16
+ * Trailer werden angehaengt. Verschluesselte PDFs werden nicht angefasst.
17
+ */
18
+ import { readFileSync, writeFileSync } from 'node:fs';
19
+ import { deflateSync, inflateSync } from 'node:zlib';
20
+ export class PdfPostprocessError extends Error {
21
+ code;
22
+ constructor(message, code) {
23
+ super(message);
24
+ this.code = code;
25
+ }
26
+ }
27
+ const WS = new Set([0x00, 0x09, 0x0a, 0x0c, 0x0d, 0x20]);
28
+ const DELIM = new Set(['(', ')', '<', '>', '[', ']', '{', '}', '/', '%']);
29
+ function isWs(src, i) {
30
+ return WS.has(src.charCodeAt(i));
31
+ }
32
+ function isRegular(ch) {
33
+ return ch !== undefined && !WS.has(ch.charCodeAt(0)) && !DELIM.has(ch);
34
+ }
35
+ export function skipWs(src, pos) {
36
+ let i = pos;
37
+ while (i < src.length) {
38
+ if (isWs(src, i)) {
39
+ i += 1;
40
+ }
41
+ else if (src[i] === '%') {
42
+ while (i < src.length && src[i] !== '\n' && src[i] !== '\r')
43
+ i += 1;
44
+ }
45
+ else {
46
+ break;
47
+ }
48
+ }
49
+ return i;
50
+ }
51
+ function literalStringEnd(src, start) {
52
+ let depth = 0;
53
+ let i = start;
54
+ while (i < src.length) {
55
+ const ch = src[i];
56
+ if (ch === '\\') {
57
+ i += 2;
58
+ continue;
59
+ }
60
+ if (ch === '(')
61
+ depth += 1;
62
+ if (ch === ')') {
63
+ depth -= 1;
64
+ if (depth === 0)
65
+ return i + 1;
66
+ }
67
+ i += 1;
68
+ }
69
+ throw new PdfPostprocessError('Unterminated string in PDF.', 'pdf_syntax');
70
+ }
71
+ function regularEnd(src, start) {
72
+ let i = start;
73
+ while (i < src.length && isRegular(src[i]))
74
+ i += 1;
75
+ return i;
76
+ }
77
+ const NUMBER = /^[+-]?(\d+\.?\d*|\.\d+)$/;
78
+ export function parseValue(src, pos) {
79
+ let i = skipWs(src, pos);
80
+ const ch = src[i];
81
+ if (ch === undefined)
82
+ throw new PdfPostprocessError('Unexpected end of PDF data.', 'pdf_syntax');
83
+ if (ch === '<' && src[i + 1] === '<') {
84
+ const entries = new Map();
85
+ i += 2;
86
+ for (;;) {
87
+ i = skipWs(src, i);
88
+ if (src[i] === '>' && src[i + 1] === '>')
89
+ return { value: { t: 'dict', entries }, end: i + 2 };
90
+ const key = parseValue(src, i);
91
+ if (key.value.t !== 'name')
92
+ throw new PdfPostprocessError('Dictionary key is no name.', 'pdf_syntax');
93
+ const value = parseValue(src, key.end);
94
+ entries.set(key.value.v, value.value);
95
+ i = value.end;
96
+ }
97
+ }
98
+ if (ch === '<') {
99
+ const end = src.indexOf('>', i);
100
+ if (end < 0)
101
+ throw new PdfPostprocessError('Unterminated hex string.', 'pdf_syntax');
102
+ return { value: { t: 'str', raw: src.slice(i, end + 1) }, end: end + 1 };
103
+ }
104
+ if (ch === '(') {
105
+ const end = literalStringEnd(src, i);
106
+ return { value: { t: 'str', raw: src.slice(i, end) }, end };
107
+ }
108
+ if (ch === '[') {
109
+ const items = [];
110
+ i += 1;
111
+ for (;;) {
112
+ i = skipWs(src, i);
113
+ if (src[i] === ']')
114
+ return { value: { t: 'arr', items }, end: i + 1 };
115
+ const item = parseValue(src, i);
116
+ items.push(item.value);
117
+ i = item.end;
118
+ }
119
+ }
120
+ if (ch === '/') {
121
+ const end = regularEnd(src, i + 1);
122
+ return { value: { t: 'name', v: src.slice(i + 1, end) }, end };
123
+ }
124
+ const end = regularEnd(src, i);
125
+ if (end === i)
126
+ throw new PdfPostprocessError('Unexpected character in PDF: ' + JSON.stringify(ch), 'pdf_syntax');
127
+ const word = src.slice(i, end);
128
+ if (NUMBER.test(word)) {
129
+ /* Verweis "n g R"? */
130
+ if (/^\d+$/.test(word)) {
131
+ const a = skipWs(src, end);
132
+ const bEnd = regularEnd(src, a);
133
+ const second = src.slice(a, bEnd);
134
+ if (/^\d+$/.test(second)) {
135
+ const c = skipWs(src, bEnd);
136
+ if (src[c] === 'R' && !isRegular(src[c + 1])) {
137
+ return { value: { t: 'ref', n: Number(word), g: Number(second) }, end: c + 1 };
138
+ }
139
+ }
140
+ }
141
+ return { value: { t: 'num', raw: word }, end };
142
+ }
143
+ return { value: { t: 'kw', raw: word }, end };
144
+ }
145
+ export function serialize(value) {
146
+ switch (value.t) {
147
+ case 'name':
148
+ return '/' + value.v;
149
+ case 'num':
150
+ case 'str':
151
+ case 'kw':
152
+ return value.raw;
153
+ case 'ref':
154
+ return value.n + ' ' + value.g + ' R';
155
+ case 'arr':
156
+ return '[' + value.items.map(serialize).join(' ') + ']';
157
+ case 'dict': {
158
+ const parts = [];
159
+ for (const [key, entry] of value.entries)
160
+ parts.push('/' + key + ' ' + serialize(entry));
161
+ return '<<' + parts.join(' ') + '>>';
162
+ }
163
+ }
164
+ }
165
+ function dictOf(value, what) {
166
+ if (value === undefined || value.t !== 'dict')
167
+ throw new PdfPostprocessError(what + ' is no dictionary.', 'pdf_structure');
168
+ return value.entries;
169
+ }
170
+ function numberOf(value) {
171
+ return value !== undefined && value.t === 'num' ? Number(value.raw) : null;
172
+ }
173
+ export class PdfFile {
174
+ bytes;
175
+ src;
176
+ xref = new Map();
177
+ freed = new Set();
178
+ trailer = new Map();
179
+ lastXrefOffset = 0;
180
+ usesXrefStream = false;
181
+ objStmCache = new Map();
182
+ constructor(bytes) {
183
+ this.bytes = bytes;
184
+ this.src = bytes.toString('latin1');
185
+ this.readXrefChain();
186
+ }
187
+ readXrefChain() {
188
+ const at = this.src.lastIndexOf('startxref');
189
+ if (at < 0)
190
+ throw new PdfPostprocessError('No startxref – not a complete PDF.', 'pdf_structure');
191
+ const offset = parseValue(this.src, at + 'startxref'.length);
192
+ if (offset.value.t !== 'num')
193
+ throw new PdfPostprocessError('startxref without offset.', 'pdf_structure');
194
+ this.lastXrefOffset = Number(offset.value.raw);
195
+ let next = this.lastXrefOffset;
196
+ const seen = new Set();
197
+ let first = true;
198
+ while (next !== null && !seen.has(next)) {
199
+ seen.add(next);
200
+ const trailer = this.readXrefSection(next, first);
201
+ if (first) {
202
+ this.trailer = trailer;
203
+ first = false;
204
+ }
205
+ const xrefStm = numberOf(trailer.get('XRefStm'));
206
+ if (xrefStm !== null && !seen.has(xrefStm)) {
207
+ seen.add(xrefStm);
208
+ this.readXrefSection(xrefStm, false);
209
+ }
210
+ next = numberOf(trailer.get('Prev'));
211
+ }
212
+ }
213
+ setEntry(num, entry) {
214
+ if (this.xref.has(num) || this.freed.has(num))
215
+ return;
216
+ if (entry === null)
217
+ this.freed.add(num);
218
+ else
219
+ this.xref.set(num, entry);
220
+ }
221
+ readXrefSection(offset, first) {
222
+ let i = skipWs(this.src, offset);
223
+ if (this.src.startsWith('xref', i)) {
224
+ i += 4;
225
+ for (;;) {
226
+ i = skipWs(this.src, i);
227
+ if (this.src.startsWith('trailer', i)) {
228
+ const trailer = parseValue(this.src, i + 7);
229
+ return dictOf(trailer.value, 'Trailer');
230
+ }
231
+ const start = parseValue(this.src, i);
232
+ const count = parseValue(this.src, start.end);
233
+ if (start.value.t !== 'num' || count.value.t !== 'num')
234
+ throw new PdfPostprocessError('Broken xref table.', 'pdf_structure');
235
+ i = count.end;
236
+ const s = Number(start.value.raw);
237
+ const c = Number(count.value.raw);
238
+ for (let k = 0; k < c; k++) {
239
+ i = skipWs(this.src, i);
240
+ const line = this.src.slice(i, i + 18);
241
+ const m = /^(\d{10}) (\d{5}) ([nf])/.exec(line);
242
+ if (!m)
243
+ throw new PdfPostprocessError('Broken xref entry.', 'pdf_structure');
244
+ this.setEntry(s + k, m[3] === 'n' ? { type: 1, a: Number(m[1]), b: Number(m[2]) } : null);
245
+ i += 18;
246
+ }
247
+ }
248
+ }
249
+ /* Querverweisstrom */
250
+ if (first)
251
+ this.usesXrefStream = true;
252
+ const obj = this.parseIndirectAt(offset);
253
+ const dict = dictOf(obj.value, 'XRef stream');
254
+ if (obj.stream === null)
255
+ throw new PdfPostprocessError('XRef stream without data.', 'pdf_structure');
256
+ const data = this.decodeStream(dict, obj.stream);
257
+ const w = dict.get('W');
258
+ if (w === undefined || w.t !== 'arr' || w.items.length !== 3)
259
+ throw new PdfPostprocessError('XRef stream without /W.', 'pdf_structure');
260
+ const widths = w.items.map((item) => numberOf(item) ?? 0);
261
+ const size = numberOf(dict.get('Size')) ?? 0;
262
+ const index = dict.get('Index');
263
+ const ranges = index !== undefined && index.t === 'arr' ? index.items.map((item) => numberOf(item) ?? 0) : [0, size];
264
+ const rowLength = widths[0] + widths[1] + widths[2];
265
+ let row = 0;
266
+ for (let r = 0; r + 1 < ranges.length; r += 2) {
267
+ const start = ranges[r];
268
+ const count = ranges[r + 1];
269
+ for (let k = 0; k < count; k++, row++) {
270
+ const base = row * rowLength;
271
+ if (base + rowLength > data.length)
272
+ break;
273
+ const field = (from, width, fallback) => {
274
+ if (width === 0)
275
+ return fallback;
276
+ let value = 0;
277
+ for (let b = 0; b < width; b++)
278
+ value = value * 256 + data[from + b];
279
+ return value;
280
+ };
281
+ const type = field(base, widths[0], 1);
282
+ const a = field(base + widths[0], widths[1], 0);
283
+ const b = field(base + widths[0] + widths[1], widths[2], 0);
284
+ if (type === 1)
285
+ this.setEntry(start + k, { type: 1, a, b });
286
+ else if (type === 2)
287
+ this.setEntry(start + k, { type: 2, a, b });
288
+ else
289
+ this.setEntry(start + k, null);
290
+ }
291
+ }
292
+ return dict;
293
+ }
294
+ /** Objekt "n g obj … endobj" an einem Byte-Versatz. */
295
+ parseIndirectAt(offset) {
296
+ const head = /^\s*(\d+)\s+(\d+)\s+obj/.exec(this.src.slice(offset, offset + 40));
297
+ if (!head)
298
+ throw new PdfPostprocessError('No object at offset ' + offset + '.', 'pdf_structure');
299
+ const gen = Number(head[2]);
300
+ const parsed = parseValue(this.src, offset + head[0].length);
301
+ let i = skipWs(this.src, parsed.end);
302
+ if (!this.src.startsWith('stream', i))
303
+ return { value: parsed.value, stream: null, gen };
304
+ i += 6;
305
+ if (this.src[i] === '\r')
306
+ i += 1;
307
+ if (this.src[i] === '\n')
308
+ i += 1;
309
+ const dict = dictOf(parsed.value, 'Stream');
310
+ let length = this.resolveNumber(dict.get('Length'));
311
+ if (length === null || !this.src.startsWith('endstream', skipWs(this.src, i + length))) {
312
+ /* Laenge passt nicht: bis endstream suchen. */
313
+ const end = this.src.indexOf('endstream', i);
314
+ if (end < 0)
315
+ throw new PdfPostprocessError('Stream without endstream.', 'pdf_structure');
316
+ length = end - i;
317
+ while (length > 0 && (this.src[i + length - 1] === '\n' || this.src[i + length - 1] === '\r'))
318
+ length -= 1;
319
+ }
320
+ return { value: parsed.value, stream: this.bytes.subarray(i, i + length), gen };
321
+ }
322
+ resolveNumber(value) {
323
+ if (value === undefined)
324
+ return null;
325
+ if (value.t === 'num')
326
+ return Number(value.raw);
327
+ if (value.t === 'ref')
328
+ return numberOf(this.getObject(value.n)?.value);
329
+ return null;
330
+ }
331
+ resolve(value) {
332
+ if (value !== undefined && value.t === 'ref')
333
+ return this.getObject(value.n)?.value;
334
+ return value;
335
+ }
336
+ generationOf(num) {
337
+ const entry = this.xref.get(num);
338
+ return entry !== undefined && entry.type === 1 ? entry.b : 0;
339
+ }
340
+ get maxObjectNumber() {
341
+ let max = 0;
342
+ for (const num of this.xref.keys())
343
+ max = Math.max(max, num);
344
+ return max;
345
+ }
346
+ getObject(num) {
347
+ const entry = this.xref.get(num);
348
+ if (entry === undefined)
349
+ return null;
350
+ if (entry.type === 1)
351
+ return this.parseIndirectAt(entry.a);
352
+ let stm = this.objStmCache.get(entry.a);
353
+ if (stm === undefined) {
354
+ const container = this.getObject(entry.a);
355
+ if (container === null || container.stream === null)
356
+ throw new PdfPostprocessError('Object stream ' + entry.a + ' missing.', 'pdf_structure');
357
+ const dict = dictOf(container.value, 'Object stream');
358
+ const data = this.decodeStream(dict, container.stream).toString('latin1');
359
+ const n = numberOf(dict.get('N')) ?? 0;
360
+ const firstOffset = numberOf(dict.get('First')) ?? 0;
361
+ const offsets = new Map();
362
+ let p = 0;
363
+ for (let k = 0; k < n; k++) {
364
+ const objNum = parseValue(data, p);
365
+ const objOff = parseValue(data, objNum.end);
366
+ p = objOff.end;
367
+ if (objNum.value.t === 'num' && objOff.value.t === 'num')
368
+ offsets.set(Number(objNum.value.raw), firstOffset + Number(objOff.value.raw));
369
+ }
370
+ stm = { data, offsets };
371
+ this.objStmCache.set(entry.a, stm);
372
+ }
373
+ const at = stm.offsets.get(num);
374
+ if (at === undefined)
375
+ return null;
376
+ return { value: parseValue(stm.data, at).value, stream: null, gen: 0 };
377
+ }
378
+ /** Stream-Daten entschluesseln (nur FlateDecode, mit PNG-Praediktoren). */
379
+ decodeStream(dict, raw) {
380
+ const filter = this.resolve(dict.get('Filter'));
381
+ const filters = [];
382
+ if (filter !== undefined) {
383
+ if (filter.t === 'name')
384
+ filters.push(filter.v);
385
+ else if (filter.t === 'arr')
386
+ for (const item of filter.items)
387
+ if (item.t === 'name')
388
+ filters.push(item.v);
389
+ }
390
+ let data = raw;
391
+ for (const name of filters) {
392
+ if (name !== 'FlateDecode' && name !== 'Fl')
393
+ throw new PdfPostprocessError('Unsupported stream filter ' + name + '.', 'pdf_filter');
394
+ data = inflateSync(data);
395
+ }
396
+ let parms = this.resolve(dict.get('DecodeParms'));
397
+ if (parms !== undefined && parms.t === 'arr')
398
+ parms = this.resolve(parms.items[0]);
399
+ if (parms !== undefined && parms.t === 'dict') {
400
+ const predictor = numberOf(parms.entries.get('Predictor')) ?? 1;
401
+ if (predictor >= 10) {
402
+ const columns = numberOf(parms.entries.get('Columns')) ?? 1;
403
+ const colors = numberOf(parms.entries.get('Colors')) ?? 1;
404
+ const bpc = numberOf(parms.entries.get('BitsPerComponent')) ?? 8;
405
+ data = unpredictPng(data, Math.ceil((columns * colors * bpc) / 8), Math.max(1, Math.ceil((colors * bpc) / 8)));
406
+ }
407
+ else if (predictor !== 1) {
408
+ throw new PdfPostprocessError('Unsupported predictor ' + predictor + '.', 'pdf_filter');
409
+ }
410
+ }
411
+ return data;
412
+ }
413
+ }
414
+ export function unpredictPng(data, rowLength, bpp) {
415
+ const rows = Math.floor(data.length / (rowLength + 1));
416
+ const out = Buffer.alloc(rows * rowLength);
417
+ for (let r = 0; r < rows; r++) {
418
+ const type = data[r * (rowLength + 1)];
419
+ const src = r * (rowLength + 1) + 1;
420
+ const dst = r * rowLength;
421
+ for (let i = 0; i < rowLength; i++) {
422
+ const raw = data[src + i];
423
+ const left = i >= bpp ? out[dst + i - bpp] : 0;
424
+ const up = r > 0 ? out[dst - rowLength + i] : 0;
425
+ const upLeft = r > 0 && i >= bpp ? out[dst - rowLength + i - bpp] : 0;
426
+ let value;
427
+ if (type === 0)
428
+ value = raw;
429
+ else if (type === 1)
430
+ value = raw + left;
431
+ else if (type === 2)
432
+ value = raw + up;
433
+ else if (type === 3)
434
+ value = raw + Math.floor((left + up) / 2);
435
+ else if (type === 4) {
436
+ const p = left + up - upLeft;
437
+ const pa = Math.abs(p - left);
438
+ const pb = Math.abs(p - up);
439
+ const pc = Math.abs(p - upLeft);
440
+ value = raw + (pa <= pb && pa <= pc ? left : pb <= pc ? up : upLeft);
441
+ }
442
+ else
443
+ throw new PdfPostprocessError('Unknown PNG predictor row type ' + type + '.', 'pdf_filter');
444
+ out[dst + i] = value & 0xff;
445
+ }
446
+ }
447
+ return out;
448
+ }
449
+ /* ------------------------------------------------------- Inhaltsstroeme */
450
+ const PATH_CONSTRUCTION = new Set(['m', 'l', 'c', 'v', 'y', 'h', 're']);
451
+ const PATH_PAINTING = new Set(['S', 's', 'f', 'F', 'f*', 'B', 'B*', 'b', 'b*']);
452
+ const CLIPPING = new Set(['W', 'W*']);
453
+ /**
454
+ * Pfadobjekte (und Schattierungen `sh`) ausserhalb jeder markierten Sequenz.
455
+ * Liefert Bereiche [Anfang, Ende) im Inhaltsstrom.
456
+ */
457
+ export function findUntaggedPaths(content) {
458
+ const ranges = [];
459
+ let depth = 0;
460
+ let inText = false;
461
+ let operandStart = null;
462
+ let pathStart = null;
463
+ let i = 0;
464
+ while (i < content.length) {
465
+ i = skipWs(content, i);
466
+ if (i >= content.length)
467
+ break;
468
+ const ch = content[i];
469
+ const tokenStart = i;
470
+ if (ch === '(' || ch === '<' || ch === '[' || ch === '/' || ch === '{' || ch === '}') {
471
+ if (ch === '{' || ch === '}') {
472
+ i += 1;
473
+ }
474
+ else {
475
+ i = parseValue(content, i).end;
476
+ }
477
+ if (operandStart === null)
478
+ operandStart = tokenStart;
479
+ continue;
480
+ }
481
+ if (ch === ']' || ch === '>' || ch === ')') {
482
+ /* Stoerzeichen ueberspringen, nicht deuten. */
483
+ i += 1;
484
+ continue;
485
+ }
486
+ const end = regularEnd(content, i);
487
+ const word = content.slice(i, end);
488
+ i = end;
489
+ if (NUMBER.test(word) || word === 'true' || word === 'false' || word === 'null') {
490
+ if (operandStart === null)
491
+ operandStart = tokenStart;
492
+ continue;
493
+ }
494
+ /* Operator */
495
+ const opStart = operandStart ?? tokenStart;
496
+ operandStart = null;
497
+ if (word === 'BI') {
498
+ /* Inline-Bild: bis ID, dann Binaerdaten bis EI. */
499
+ const id = content.indexOf('ID', i);
500
+ if (id < 0)
501
+ break;
502
+ const ei = /\sEI(?=[\s\x00]|$)/g;
503
+ ei.lastIndex = id + 3;
504
+ const match = ei.exec(content);
505
+ i = match === null ? content.length : match.index + 3;
506
+ pathStart = null;
507
+ continue;
508
+ }
509
+ if (PATH_CONSTRUCTION.has(word)) {
510
+ if (pathStart === null)
511
+ pathStart = opStart;
512
+ continue;
513
+ }
514
+ if (CLIPPING.has(word))
515
+ continue;
516
+ if (PATH_PAINTING.has(word)) {
517
+ if (pathStart !== null && depth === 0 && !inText)
518
+ ranges.push([pathStart, i]);
519
+ pathStart = null;
520
+ continue;
521
+ }
522
+ if (word === 'sh') {
523
+ if (depth === 0 && !inText)
524
+ ranges.push([opStart, i]);
525
+ pathStart = null;
526
+ continue;
527
+ }
528
+ pathStart = null;
529
+ if (word === 'BDC' || word === 'BMC')
530
+ depth += 1;
531
+ else if (word === 'EMC')
532
+ depth = Math.max(0, depth - 1);
533
+ else if (word === 'BT')
534
+ inText = true;
535
+ else if (word === 'ET')
536
+ inText = false;
537
+ }
538
+ return ranges;
539
+ }
540
+ export function wrapAsArtifacts(content, ranges) {
541
+ let out = '';
542
+ let last = 0;
543
+ for (const [start, end] of ranges) {
544
+ out += content.slice(last, start) + '\n/Artifact BMC\n' + content.slice(start, end) + '\nEMC\n';
545
+ last = end;
546
+ }
547
+ return out + content.slice(last);
548
+ }
549
+ /* ------------------------------------------------------------------- XMP */
550
+ const PDFUA_NS = 'http://www.aiim.org/pdfua/ns/id/';
551
+ export function addPdfUaIdentifier(xmp) {
552
+ if (/pdfuaid:part/.test(xmp))
553
+ return { xmp, existed: true };
554
+ const close = xmp.lastIndexOf('</rdf:RDF>');
555
+ if (close < 0)
556
+ throw new PdfPostprocessError('The XMP metadata has no rdf:RDF element.', 'xmp_structure');
557
+ const description = '<rdf:Description rdf:about="" xmlns:pdfuaid="' + PDFUA_NS + '">\n' +
558
+ ' <pdfuaid:part>1</pdfuaid:part>\n' +
559
+ ' </rdf:Description>\n ';
560
+ return { xmp: xmp.slice(0, close) + description + xmp.slice(close), existed: false };
561
+ }
562
+ const XMP_SKELETON = '<?xpacket begin="\u00ef\u00bb\u00bf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n' +
563
+ '<x:xmpmeta xmlns:x="adobe:ns:meta/">\n <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n </rdf:RDF>\n</x:xmpmeta>\n<?xpacket end="w"?>';
564
+ function streamObject(dict, data, compress) {
565
+ const entries = new Map(dict);
566
+ entries.delete('Length');
567
+ entries.delete('Filter');
568
+ entries.delete('DecodeParms');
569
+ entries.delete('DL');
570
+ const payload = compress ? deflateSync(data) : data;
571
+ if (compress)
572
+ entries.set('Filter', { t: 'name', v: 'FlateDecode' });
573
+ entries.set('Length', { t: 'num', raw: String(payload.length) });
574
+ return serialize({ t: 'dict', entries }) + '\nstream\n' + payload.toString('latin1') + '\nendstream';
575
+ }
576
+ function incrementalUpdate(pdf, objects) {
577
+ const sorted = [...objects].sort((a, b) => a.num - b.num);
578
+ const parts = [];
579
+ let offset = pdf.bytes.length;
580
+ const lead = pdf.src.endsWith('\n') || pdf.src.endsWith('\r') ? '' : '\n';
581
+ parts.push(lead);
582
+ offset += lead.length;
583
+ const offsets = new Map();
584
+ for (const object of sorted) {
585
+ offsets.set(object.num, offset);
586
+ const text = object.num + ' ' + object.gen + ' obj\n' + object.body + '\nendobj\n';
587
+ parts.push(text);
588
+ offset += Buffer.byteLength(text, 'latin1');
589
+ }
590
+ const oldSize = numberOf(pdf.trailer.get('Size')) ?? pdf.maxObjectNumber + 1;
591
+ const keep = ['Root', 'Info', 'ID'];
592
+ const trailerEntries = new Map();
593
+ if (pdf.usesXrefStream) {
594
+ const xrefNum = Math.max(oldSize, ...sorted.map((o) => o.num + 1));
595
+ const all = [...sorted.map((o) => ({ num: o.num, gen: o.gen, at: offsets.get(o.num) })), { num: xrefNum, gen: 0, at: offset }];
596
+ const index = [];
597
+ for (const entry of all) {
598
+ const lastStart = index.length >= 2 ? index[index.length - 2] : -2;
599
+ const lastCount = index.length >= 2 ? index[index.length - 1] : 0;
600
+ if (lastStart + lastCount === entry.num)
601
+ index[index.length - 1] = lastCount + 1;
602
+ else
603
+ index.push(entry.num, 1);
604
+ }
605
+ const data = Buffer.alloc(all.length * 7);
606
+ all.forEach((entry, k) => {
607
+ data[k * 7] = 1;
608
+ data.writeUInt32BE(entry.at, k * 7 + 1);
609
+ data.writeUInt16BE(entry.gen, k * 7 + 5);
610
+ });
611
+ trailerEntries.set('Type', { t: 'name', v: 'XRef' });
612
+ trailerEntries.set('Size', { t: 'num', raw: String(xrefNum + 1) });
613
+ trailerEntries.set('Index', { t: 'arr', items: index.map((n) => ({ t: 'num', raw: String(n) })) });
614
+ trailerEntries.set('W', { t: 'arr', items: [1, 4, 2].map((n) => ({ t: 'num', raw: String(n) })) });
615
+ for (const key of keep) {
616
+ const value = pdf.trailer.get(key);
617
+ if (value !== undefined)
618
+ trailerEntries.set(key, value);
619
+ }
620
+ trailerEntries.set('Prev', { t: 'num', raw: String(pdf.lastXrefOffset) });
621
+ trailerEntries.set('Length', { t: 'num', raw: String(data.length) });
622
+ parts.push(xrefNum + ' 0 obj\n' + serialize({ t: 'dict', entries: trailerEntries }) + '\nstream\n' + data.toString('latin1') + '\nendstream\nendobj\n');
623
+ parts.push('startxref\n' + offset + '\n%%EOF\n');
624
+ }
625
+ else {
626
+ let table = 'xref\n';
627
+ let k = 0;
628
+ while (k < sorted.length) {
629
+ let run = 1;
630
+ while (k + run < sorted.length && sorted[k + run].num === sorted[k].num + run)
631
+ run += 1;
632
+ table += sorted[k].num + ' ' + run + '\n';
633
+ for (let r = 0; r < run; r++) {
634
+ const object = sorted[k + r];
635
+ table += String(offsets.get(object.num)).padStart(10, '0') + ' ' + String(object.gen).padStart(5, '0') + ' n\r\n';
636
+ }
637
+ k += run;
638
+ }
639
+ const size = Math.max(oldSize, ...sorted.map((o) => o.num + 1));
640
+ trailerEntries.set('Size', { t: 'num', raw: String(size) });
641
+ for (const key of keep) {
642
+ const value = pdf.trailer.get(key);
643
+ if (value !== undefined)
644
+ trailerEntries.set(key, value);
645
+ }
646
+ trailerEntries.set('Prev', { t: 'num', raw: String(pdf.lastXrefOffset) });
647
+ parts.push(table + 'trailer\n' + serialize({ t: 'dict', entries: trailerEntries }) + '\nstartxref\n' + offset + '\n%%EOF\n');
648
+ }
649
+ return Buffer.concat([pdf.bytes, Buffer.from(parts.join(''), 'latin1')]);
650
+ }
651
+ function pageList(pdf, root) {
652
+ const pages = [];
653
+ const seen = new Set();
654
+ const visit = (node, depth) => {
655
+ if (node === undefined || node.t !== 'ref' || depth > 64 || seen.has(node.n))
656
+ return;
657
+ seen.add(node.n);
658
+ const dict = pdf.resolve(node);
659
+ if (dict === undefined || dict.t !== 'dict')
660
+ return;
661
+ const type = dict.entries.get('Type');
662
+ const kids = pdf.resolve(dict.entries.get('Kids'));
663
+ if (kids !== undefined && kids.t === 'arr' && !(type !== undefined && type.t === 'name' && type.v === 'Page')) {
664
+ for (const kid of kids.items)
665
+ visit(kid, depth + 1);
666
+ return;
667
+ }
668
+ const refs = [];
669
+ let contents = dict.entries.get('Contents');
670
+ if (contents !== undefined && contents.t === 'ref') {
671
+ const target = pdf.getObject(contents.n);
672
+ if (target !== null && target.value.t === 'arr')
673
+ contents = target.value;
674
+ }
675
+ if (contents !== undefined && contents.t === 'ref')
676
+ refs.push(contents.n);
677
+ else if (contents !== undefined && contents.t === 'arr')
678
+ for (const item of contents.items)
679
+ if (item.t === 'ref')
680
+ refs.push(item.n);
681
+ pages.push({ num: node.n, dict: dict.entries, contents: refs });
682
+ };
683
+ visit(root.get('Pages'), 0);
684
+ return pages;
685
+ }
686
+ /** Der Inhalt einer Seite als ein Text: die Teilstroeme gelten als aneinandergehaengt (ISO 32000-1, 7.8.2). */
687
+ function pageContent(pdf, page) {
688
+ const parts = [];
689
+ for (const num of page.contents) {
690
+ const object = pdf.getObject(num);
691
+ if (object === null || object.stream === null || object.value.t !== 'dict')
692
+ return null;
693
+ parts.push(pdf.decodeStream(object.value.entries, object.stream).toString('latin1'));
694
+ }
695
+ return parts.join('\n');
696
+ }
697
+ export function postprocessPdf(path, options) {
698
+ const pdf = new PdfFile(readFileSync(path));
699
+ if (pdf.trailer.has('Encrypt'))
700
+ throw new PdfPostprocessError('The PDF is encrypted; it is left unchanged.', 'pdf_encrypted');
701
+ const rootRef = pdf.trailer.get('Root');
702
+ if (rootRef === undefined || rootRef.t !== 'ref')
703
+ throw new PdfPostprocessError('The PDF has no document catalog.', 'pdf_structure');
704
+ const rootObject = pdf.getObject(rootRef.n);
705
+ const root = dictOf(rootObject?.value, 'Catalog');
706
+ const tagged = root.has('StructTreeRoot');
707
+ const objects = [];
708
+ const result = {
709
+ bytes: pdf.bytes.length,
710
+ tagged,
711
+ pages: 0,
712
+ untagged_paths_found: 0,
713
+ untagged_paths_marked: 0,
714
+ content_streams_changed: 0,
715
+ pdfua_identifier_set: false,
716
+ pdfua_identifier_existed: false,
717
+ };
718
+ const pages = pageList(pdf, root);
719
+ result.pages = pages.length;
720
+ let nextNum = Math.max(numberOf(pdf.trailer.get('Size')) ?? 0, pdf.maxObjectNumber + 1);
721
+ for (const page of pages) {
722
+ const content = pageContent(pdf, page);
723
+ if (content === null)
724
+ continue;
725
+ const ranges = findUntaggedPaths(content);
726
+ result.untagged_paths_found += ranges.length;
727
+ if (options.artifactPaths && ranges.length > 0) {
728
+ const streamNum = nextNum++;
729
+ objects.push({ num: streamNum, gen: 0, body: streamObject(new Map(), Buffer.from(wrapAsArtifacts(content, ranges), 'latin1'), true) });
730
+ const newPage = new Map(page.dict);
731
+ newPage.set('Contents', { t: 'ref', n: streamNum, g: 0 });
732
+ objects.push({ num: page.num, gen: pdf.generationOf(page.num), body: serialize({ t: 'dict', entries: newPage }) });
733
+ result.untagged_paths_marked += ranges.length;
734
+ result.content_streams_changed += 1;
735
+ }
736
+ }
737
+ if (options.pdfua) {
738
+ const metaRef = root.get('Metadata');
739
+ let metaNum;
740
+ let metaDict = new Map([
741
+ ['Type', { t: 'name', v: 'Metadata' }],
742
+ ['Subtype', { t: 'name', v: 'XML' }],
743
+ ]);
744
+ let xmp = XMP_SKELETON;
745
+ if (metaRef !== undefined && metaRef.t === 'ref') {
746
+ metaNum = metaRef.n;
747
+ const meta = pdf.getObject(metaNum);
748
+ if (meta !== null && meta.stream !== null && meta.value.t === 'dict') {
749
+ metaDict = meta.value.entries;
750
+ xmp = pdf.decodeStream(meta.value.entries, meta.stream).toString('latin1');
751
+ }
752
+ }
753
+ else {
754
+ metaNum = nextNum++;
755
+ const newRoot = new Map(root);
756
+ newRoot.set('Metadata', { t: 'ref', n: metaNum, g: 0 });
757
+ objects.push({ num: rootRef.n, gen: pdf.generationOf(rootRef.n), body: serialize({ t: 'dict', entries: newRoot }) });
758
+ }
759
+ const added = addPdfUaIdentifier(xmp);
760
+ result.pdfua_identifier_existed = added.existed;
761
+ if (!added.existed) {
762
+ objects.push({ num: metaNum, gen: pdf.generationOf(metaNum), body: streamObject(metaDict, Buffer.from(added.xmp, 'latin1'), false) });
763
+ }
764
+ result.pdfua_identifier_set = true;
765
+ }
766
+ if (objects.length > 0) {
767
+ const updated = incrementalUpdate(pdf, objects);
768
+ writeFileSync(path, updated);
769
+ result.bytes = updated.length;
770
+ }
771
+ return result;
772
+ }
773
+ /** Nur lesen: PDF/UA-Kennung und Zahl ungetaggter Pfade (fuer Tests und Pruefungen). */
774
+ export function inspectPdf(path) {
775
+ const pdf = new PdfFile(readFileSync(path));
776
+ const rootRef = pdf.trailer.get('Root');
777
+ const root = dictOf(rootRef === undefined ? undefined : pdf.resolve(rootRef), 'Catalog');
778
+ let part = null;
779
+ const metaRef = root.get('Metadata');
780
+ if (metaRef !== undefined && metaRef.t === 'ref') {
781
+ const meta = pdf.getObject(metaRef.n);
782
+ if (meta !== null && meta.stream !== null && meta.value.t === 'dict') {
783
+ const xmp = pdf.decodeStream(meta.value.entries, meta.stream).toString('latin1');
784
+ const m = /<pdfuaid:part>\s*(\d+)\s*<\/pdfuaid:part>|pdfuaid:part="(\d+)"/.exec(xmp);
785
+ if (m)
786
+ part = m[1] ?? m[2] ?? null;
787
+ }
788
+ }
789
+ const pages = pageList(pdf, root);
790
+ let untagged = 0;
791
+ for (const page of pages) {
792
+ const content = pageContent(pdf, page);
793
+ if (content !== null)
794
+ untagged += findUntaggedPaths(content).length;
795
+ }
796
+ return { pdfua_part: part, untagged_paths: untagged, pages: pages.length, tagged: root.has('StructTreeRoot') };
797
+ }
798
+ //# sourceMappingURL=pdf-postprocess.js.map