reamkit 1.26.0 → 1.28.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 (85) hide show
  1. package/README.md +14 -8
  2. package/dist/esm/core/converter/ream.d.ts +0 -9
  3. package/dist/esm/core/converter/ream.js +0 -1
  4. package/dist/esm/core/document-model/types.d.ts +5 -3
  5. package/dist/esm/core/font/index.d.ts +1 -0
  6. package/dist/esm/core/font/ligatures.d.ts +17 -0
  7. package/dist/esm/core/font/ligatures.js +49 -0
  8. package/dist/esm/core/font/ttf-parser.d.ts +2 -1
  9. package/dist/esm/core/font/ttf-parser.js +11 -2
  10. package/dist/esm/core/fonts/remote-fonts.d.ts +1 -1
  11. package/dist/esm/core/fonts/remote-fonts.js +47 -2
  12. package/dist/esm/core/fonts/scripts.js +10 -5
  13. package/dist/esm/excel/header-footer.js +53 -8
  14. package/dist/esm/layout/styled-layout.js +53 -3
  15. package/dist/esm/pdf/cid-font.js +35 -8
  16. package/dist/esm/pdf-reader/annot-draw.d.ts +65 -0
  17. package/dist/esm/pdf-reader/annot-draw.js +487 -0
  18. package/dist/esm/pdf-reader/annots.d.ts +0 -12
  19. package/dist/esm/pdf-reader/annots.js +30 -4
  20. package/dist/esm/pdf-reader/ccitt.d.ts +20 -3
  21. package/dist/esm/pdf-reader/ccitt.js +102 -6
  22. package/dist/esm/pdf-reader/cff-outline.d.ts +36 -0
  23. package/dist/esm/pdf-reader/cff-outline.js +1122 -0
  24. package/dist/esm/pdf-reader/cie-color.d.ts +33 -0
  25. package/dist/esm/pdf-reader/cie-color.js +112 -0
  26. package/dist/esm/pdf-reader/cmap.js +5 -2
  27. package/dist/esm/pdf-reader/content.d.ts +123 -3
  28. package/dist/esm/pdf-reader/content.js +232 -54
  29. package/dist/esm/pdf-reader/dingbats.d.ts +11 -0
  30. package/dist/esm/pdf-reader/dingbats.js +1033 -0
  31. package/dist/esm/pdf-reader/display.d.ts +1 -1
  32. package/dist/esm/pdf-reader/display.js +61 -6
  33. package/dist/esm/pdf-reader/document.d.ts +6 -0
  34. package/dist/esm/pdf-reader/document.js +55 -10
  35. package/dist/esm/pdf-reader/embedded-fonts.d.ts +23 -3
  36. package/dist/esm/pdf-reader/embedded-fonts.js +36 -4
  37. package/dist/esm/pdf-reader/encodings.d.ts +25 -0
  38. package/dist/esm/pdf-reader/encodings.js +110 -0
  39. package/dist/esm/pdf-reader/flow-build.d.ts +19 -5
  40. package/dist/esm/pdf-reader/flow-build.js +111 -11
  41. package/dist/esm/pdf-reader/font.js +403 -21
  42. package/dist/esm/pdf-reader/function.d.ts +16 -0
  43. package/dist/esm/pdf-reader/function.js +414 -0
  44. package/dist/esm/pdf-reader/glyf-outline.d.ts +43 -0
  45. package/dist/esm/pdf-reader/glyf-outline.js +351 -0
  46. package/dist/esm/pdf-reader/glyph-names.js +20 -0
  47. package/dist/esm/pdf-reader/icc.d.ts +10 -0
  48. package/dist/esm/pdf-reader/icc.js +210 -0
  49. package/dist/esm/pdf-reader/image-decode.d.ts +9 -4
  50. package/dist/esm/pdf-reader/image-decode.js +274 -72
  51. package/dist/esm/pdf-reader/images.d.ts +18 -0
  52. package/dist/esm/pdf-reader/images.js +154 -11
  53. package/dist/esm/pdf-reader/jbig2.d.ts +23 -0
  54. package/dist/esm/pdf-reader/jbig2.js +126 -32
  55. package/dist/esm/pdf-reader/layout.d.ts +32 -0
  56. package/dist/esm/pdf-reader/layout.js +1316 -64
  57. package/dist/esm/pdf-reader/lexer.d.ts +2 -0
  58. package/dist/esm/pdf-reader/lexer.js +4 -0
  59. package/dist/esm/pdf-reader/math-rows.d.ts +23 -0
  60. package/dist/esm/pdf-reader/math-rows.js +198 -0
  61. package/dist/esm/pdf-reader/optional-content.d.ts +36 -0
  62. package/dist/esm/pdf-reader/optional-content.js +93 -0
  63. package/dist/esm/pdf-reader/predefined-cmap.d.ts +21 -0
  64. package/dist/esm/pdf-reader/predefined-cmap.js +102 -0
  65. package/dist/esm/pdf-reader/reader.d.ts +6 -6
  66. package/dist/esm/pdf-reader/reader.js +102 -16
  67. package/dist/esm/pdf-reader/shading.d.ts +139 -8
  68. package/dist/esm/pdf-reader/shading.js +309 -39
  69. package/dist/esm/pdf-reader/standard-metrics.d.ts +8 -0
  70. package/dist/esm/pdf-reader/standard-metrics.js +18 -0
  71. package/dist/esm/pdf-reader/standard-widths.d.ts +20 -0
  72. package/dist/esm/pdf-reader/standard-widths.js +62 -0
  73. package/dist/esm/pdf-reader/stream-filters.d.ts +6 -0
  74. package/dist/esm/pdf-reader/stream-filters.js +67 -0
  75. package/dist/esm/pdf-reader/tagged.js +204 -32
  76. package/dist/esm/pdf-reader/text-rules.d.ts +16 -0
  77. package/dist/esm/pdf-reader/text-rules.js +185 -0
  78. package/dist/esm/pdf-reader/text.js +157 -4
  79. package/dist/esm/pdf-reader/type1-outline.d.ts +21 -0
  80. package/dist/esm/pdf-reader/type1-outline.js +576 -0
  81. package/dist/esm/pdf-reader/vector.d.ts +5 -0
  82. package/dist/esm/pdf-reader/vector.js +79 -27
  83. package/dist/esm/word/document-parser.js +4 -1
  84. package/dist/esm/word/docx-writer.js +74 -11
  85. package/package.json +1 -1
@@ -2,6 +2,9 @@ import { PDF_NULL, PdfHexString, PdfName, PdfStream } from "../pdf/objects.js";
2
2
  import { encodePng } from "../core/png-encode.js";
3
3
  import { lzwDecodeMsb } from "../core/lzw.js";
4
4
  import { reversePredictor } from "./predictor.js";
5
+ import { ascii85Decode, asciiHexDecode, runLengthDecode } from "./stream-filters.js";
6
+ import { labToSrgb } from "./cie-color.js";
7
+ import { readFunction } from "./function.js";
5
8
  import { decodeCcitt } from "./ccitt.js";
6
9
  import { decodeJbig2 } from "./jbig2.js";
7
10
  import { decodeJpeg } from "./jpeg.js";
@@ -16,19 +19,24 @@ var MAX_PIXELS = 4e7;
16
19
  * Supports DeviceGray/RGB/CMYK, CalGray/CalRGB, ICCBased (by `/N`) and Indexed
17
20
  * colour spaces; Flate, LZW (EP12), RunLength, ASCII85, ASCIIHex and CCITT
18
21
  * Group 4 / Group 3 1-D fax (EP15) filters; PNG/TIFF predictors; bit depths
19
- * 1/2/4/8/16; and an `/SMask` folded in as the PNG alpha channel. Unsupported
20
- * inputs (stencil `/ImageMask`, Separation/DeviceN/Lab, JBIG2, CCITT Group 3
21
- * 2-D) return a typed failure so the caller records a loss.
22
+ * 1/2/4/8/16; an `/SMask` folded in as the PNG alpha channel; and a stencil
23
+ * `/ImageMask` (§8.9.6.2) given back as RGBA in `fillHex`; `Lab` (§8.6.5.8) and
24
+ * a `Separation`/`DeviceN` through its own tint transform (§8.6.6.4).
25
+ * Unsupported inputs (CCITT Group 3 2-D) return a typed failure so the caller
26
+ * records a loss.
22
27
  *
28
+ * @param file The owning file.
29
+ * @param stream The image XObject, or an inline image wrapped as one.
30
+ * @param fillHex §8.9.6.2 — the non-stroking colour a stencil mask paints in.
23
31
  * @returns The decoded image, or `{ ok: false }` with the loss severity and reason.
24
32
  */
25
- function decodePdfImage(file, stream) {
33
+ function decodePdfImage(file, stream, fillHex) {
26
34
  const d = stream.dict;
27
35
  const width = intOf(file.get(d, "Width")) || intOf(file.get(d, "W"));
28
36
  const height = intOf(file.get(d, "Height")) || intOf(file.get(d, "H"));
29
37
  if (width <= 0 || height <= 0) return fail("dropped", "image with no dimensions");
30
38
  if (width * height > MAX_PIXELS) return fail("dropped", "image too large to decode");
31
- if (boolOf(d.get("ImageMask")) || boolOf(d.get("IM"))) return fail("dropped", "stencil image mask not reconstructed");
39
+ if (boolOf(d.get("ImageMask")) || boolOf(d.get("IM"))) return stencil(file, stream, width, height, fillHex);
32
40
  const filters = filterNames(file, d);
33
41
  const last = filters[filters.length - 1];
34
42
  if (last === "DCTDecode" || last === "DCT") {
@@ -116,13 +124,14 @@ function decodeCcittImage(file, stream, filters, width, height) {
116
124
  byteAlign: parms ? boolOf(file.get(parms, "EncodedByteAlign")) : false
117
125
  });
118
126
  if (!packed) return "CCITT fax image not decoded (Group 3 2-D or malformed)";
127
+ const blackIs1 = parms ? boolOf(file.get(parms, "BlackIs1")) : false;
119
128
  const rowBytes = columns + 7 >> 3;
120
129
  const samples = new Uint8Array(width * height);
121
130
  for (let y = 0; y < height; y++) {
122
131
  const rowOff = y * rowBytes;
123
132
  for (let x = 0; x < width; x++) {
124
- const black = x < columns ? packed[rowOff + (x >> 3)] >> 7 - (x & 7) & 1 : 0;
125
- samples[y * width + x] = black ? 0 : 255;
133
+ const bit = x < columns ? packed[rowOff + (x >> 3)] >> 7 - (x & 7) & 1 : 0;
134
+ samples[y * width + x] = bit === (blackIs1 ? 0 : 1) ? 0 : 255;
126
135
  }
127
136
  }
128
137
  return {
@@ -130,6 +139,89 @@ function decodeCcittImage(file, stream, filters, width, height) {
130
139
  samples
131
140
  };
132
141
  }
142
+ /**
143
+ * §8.9.6.2 — a stencil mask: one bit per pixel saying WHERE to paint, and the
144
+ * colour is whatever the page's non-stroking colour was at the `Do`.
145
+ *
146
+ * A sample of 0 paints and 1 leaves the page alone, which `/Decode [1 0]`
147
+ * swaps. Given back as an RGBA picture — the fill colour throughout, opaque
148
+ * where the stencil paints and clear where it does not — which is what the mask
149
+ * MEANS and what every format downstream can show.
150
+ * images_1bit_grayscale.pdf draws two of them and they were dropped entirely.
151
+ */
152
+ function stencil(file, stream, width, height, fillHex) {
153
+ const d = stream.dict;
154
+ const filters = filterNames(file, d);
155
+ const last = filters[filters.length - 1];
156
+ const packed = last === "CCITTFaxDecode" || last === "CCF" ? ccittBits(file, stream, filters, height) : last === "JBIG2Decode" ? jbig2Bits(file, stream, filters, width, height) : decodeChain(file, stream, filters);
157
+ if (!packed) return fail("dropped", "stencil image mask not decoded");
158
+ const decode = file.resolve(d.get("Decode") ?? d.get("D") ?? PDF_NULL);
159
+ const paintBit = Array.isArray(decode) && file.resolve(decode[0] ?? PDF_NULL) === 1 ? 1 : 0;
160
+ const rgb = fillHex !== void 0 ? parseHex(fillHex) : [
161
+ 0,
162
+ 0,
163
+ 0
164
+ ];
165
+ const rowBytes = width + 7 >> 3;
166
+ if (packed.length < rowBytes * height) return fail("dropped", "stencil image mask truncated");
167
+ const samples = new Uint8Array(width * height * 4);
168
+ for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
169
+ const bit = packed[y * rowBytes + (x >> 3)] >> 7 - (x & 7) & 1;
170
+ const at = (y * width + x) * 4;
171
+ samples[at] = rgb[0];
172
+ samples[at + 1] = rgb[1];
173
+ samples[at + 2] = rgb[2];
174
+ samples[at + 3] = bit === paintBit ? 255 : 0;
175
+ }
176
+ return {
177
+ ok: true,
178
+ bytes: encodePng(width, height, "rgba", samples),
179
+ format: "png",
180
+ widthPx: width,
181
+ heightPx: height
182
+ };
183
+ }
184
+ /** A 6-hex colour as its three bytes. */
185
+ function parseHex(hex) {
186
+ const v = Number.parseInt(hex, 16);
187
+ if (!Number.isFinite(v)) return [
188
+ 0,
189
+ 0,
190
+ 0
191
+ ];
192
+ return [
193
+ v >> 16 & 255,
194
+ v >> 8 & 255,
195
+ v & 255
196
+ ];
197
+ }
198
+ /** A stencil's bits where the file coded them as fax (§7.4.6). */
199
+ function ccittBits(file, stream, filters, height) {
200
+ const parms = decodeParmsOf(file, stream.dict);
201
+ const columns = (parms ? intOf(file.get(parms, "Columns")) : 0) || 1728;
202
+ const packed = decodeCcitt(applyChainExceptLast(filters, stream.data), {
203
+ k: parms ? intOf(file.get(parms, "K")) : 0,
204
+ columns,
205
+ rows: height,
206
+ byteAlign: parms ? boolOf(file.get(parms, "EncodedByteAlign")) : false
207
+ });
208
+ if (!packed) return void 0;
209
+ if (parms ? boolOf(file.get(parms, "BlackIs1")) : false) return packed;
210
+ const flipped = new Uint8Array(packed.length);
211
+ for (let i = 0; i < packed.length; i++) flipped[i] = ~packed[i] & 255;
212
+ return flipped;
213
+ }
214
+ /** …or as JBIG2, which codes 1 as black and so paints on 1. */
215
+ function jbig2Bits(file, stream, filters, width, height) {
216
+ const parms = decodeParmsOf(file, stream.dict);
217
+ const globalsVal = parms ? file.get(parms, "JBIG2Globals") : void 0;
218
+ const globals = globalsVal instanceof PdfStream ? file.streamData(globalsVal) : void 0;
219
+ const packed = decodeJbig2(applyChainExceptLast(filters, stream.data), globals, width, height);
220
+ if (!packed) return void 0;
221
+ const flipped = new Uint8Array(packed.length);
222
+ for (let i = 0; i < packed.length; i++) flipped[i] = ~packed[i] & 255;
223
+ return flipped;
224
+ }
133
225
  function resolveColorSpace(file, csVal) {
134
226
  const cs = file.resolve(csVal ?? PDF_NULL);
135
227
  if (cs instanceof PdfName) return namedColorSpace(cs.value);
@@ -178,6 +270,54 @@ function resolveColorSpace(file, csVal) {
178
270
  lookup
179
271
  };
180
272
  }
273
+ if (tag === "Lab") {
274
+ const params = file.resolve(cs[1] ?? PDF_NULL);
275
+ const dict = params instanceof Map ? params : void 0;
276
+ const nums = (v) => {
277
+ const r = v !== void 0 ? file.resolve(v) : void 0;
278
+ return Array.isArray(r) ? r.map((x) => typeof file.resolve(x) === "number" ? file.resolve(x) : 0) : [];
279
+ };
280
+ const white = dict ? nums(file.get(dict, "WhitePoint")) : [];
281
+ if (white.length < 3 || !(white[1] > 0)) return void 0;
282
+ const range = dict ? nums(file.get(dict, "Range")) : [];
283
+ return {
284
+ kind: "lab",
285
+ components: 3,
286
+ lab: {
287
+ white: [
288
+ white[0],
289
+ white[1],
290
+ white[2]
291
+ ],
292
+ range: range.length === 4 ? [
293
+ range[0],
294
+ range[1],
295
+ range[2],
296
+ range[3]
297
+ ] : [
298
+ -100,
299
+ 100,
300
+ -100,
301
+ 100
302
+ ]
303
+ }
304
+ };
305
+ }
306
+ if (tag === "Separation" || tag === "DeviceN" || tag === "I") {
307
+ const names = file.resolve(cs[1] ?? PDF_NULL);
308
+ const components = tag === "Separation" ? 1 : Array.isArray(names) ? names.length : 0;
309
+ const alternate = resolveColorSpace(file, cs[2]);
310
+ const transform = readFunction(file, cs[3]);
311
+ if (components < 1 || !alternate || alternate.kind === "tint" || !transform) return void 0;
312
+ return {
313
+ kind: "tint",
314
+ components,
315
+ tint: {
316
+ transform,
317
+ alternate
318
+ }
319
+ };
320
+ }
181
321
  }
182
322
  function namedColorSpace(name) {
183
323
  if (name === "DeviceGray" || name === "G" || name === "CalGray") return {
@@ -241,13 +381,71 @@ function toColor(cs, s, px, bpc, decode) {
241
381
  samples: out
242
382
  };
243
383
  }
384
+ if (cs.kind === "tint" && cs.tint) {
385
+ const { transform, alternate } = cs.tint;
386
+ const n = cs.components;
387
+ const cache = /* @__PURE__ */ new Map();
388
+ const out = new Uint8Array(px * 3);
389
+ for (let i = 0; i < px; i++) {
390
+ const comps = [];
391
+ for (let k = 0; k < n; k++) comps.push(c01(s[i * n + k], k));
392
+ const key = comps.join(",");
393
+ let rgb = cache.get(key);
394
+ if (!rgb) {
395
+ rgb = alternateRgb(alternate, transform(comps));
396
+ if (cache.size < MAX_TINT_CACHE) cache.set(key, rgb);
397
+ }
398
+ out[i * 3] = rgb[0];
399
+ out[i * 3 + 1] = rgb[1];
400
+ out[i * 3 + 2] = rgb[2];
401
+ }
402
+ return {
403
+ color: "rgb",
404
+ samples: out
405
+ };
406
+ }
407
+ if (cs.kind === "lab") {
408
+ const lab = cs.lab;
409
+ const out = new Uint8Array(px * 3);
410
+ const span = (i) => decode && decode.length >= 6 ? [0, 1] : i === 0 ? [0, 100] : [lab.range[(i - 1) * 2], lab.range[(i - 1) * 2 + 1]];
411
+ for (let i = 0; i < px; i++) {
412
+ const comp = [
413
+ 0,
414
+ 1,
415
+ 2
416
+ ].map((k) => {
417
+ const t = c01(s[i * 3 + k], k);
418
+ const [lo, hi] = span(k);
419
+ return lo + t * (hi - lo);
420
+ });
421
+ const [r, g, b] = labToSrgb(lab.white, [
422
+ comp[0],
423
+ comp[1],
424
+ comp[2]
425
+ ]);
426
+ out[i * 3] = Math.round(r * 255);
427
+ out[i * 3 + 1] = Math.round(g * 255);
428
+ out[i * 3 + 2] = Math.round(b * 255);
429
+ }
430
+ return {
431
+ color: "rgb",
432
+ samples: out
433
+ };
434
+ }
244
435
  const base = cs.base;
245
436
  const lookup = cs.lookup;
246
437
  const hival = cs.hival ?? 255;
247
438
  const bn = base.components;
439
+ const index = (v) => {
440
+ if (!decode || decode.length < 2) return Math.min(v, hival);
441
+ const lo = decode[0];
442
+ const hi = decode[1];
443
+ const at = maxv > 0 ? lo + v / maxv * (hi - lo) : lo;
444
+ return Math.min(Math.max(0, Math.round(at)), hival);
445
+ };
248
446
  if (base.kind === "gray") {
249
447
  const out = new Uint8Array(px);
250
- for (let i = 0; i < px; i++) out[i] = lookup[Math.min(s[i], hival) * bn] ?? 0;
448
+ for (let i = 0; i < px; i++) out[i] = lookup[index(s[i]) * bn] ?? 0;
251
449
  return {
252
450
  color: "gray",
253
451
  samples: out
@@ -255,7 +453,7 @@ function toColor(cs, s, px, bpc, decode) {
255
453
  }
256
454
  const out = new Uint8Array(px * 3);
257
455
  for (let i = 0; i < px; i++) {
258
- const [r, g, b] = paletteRgb(base, lookup, Math.min(s[i], hival) * bn);
456
+ const [r, g, b] = paletteRgb(base, lookup, index(s[i]) * bn);
259
457
  out[i * 3] = r;
260
458
  out[i * 3 + 1] = g;
261
459
  out[i * 3 + 2] = b;
@@ -265,12 +463,67 @@ function toColor(cs, s, px, bpc, decode) {
265
463
  samples: out
266
464
  };
267
465
  }
466
+ /** How many distinct tint tuples are remembered before the cache stops growing. */
467
+ var MAX_TINT_CACHE = 65536;
468
+ /** §8.6.6.4 — what the alternate space makes of the transform's output. */
469
+ function alternateRgb(alternate, out) {
470
+ const at = (i) => Math.min(1, Math.max(0, out[i] ?? 0));
471
+ const to255 = (v) => Math.round(v * 255);
472
+ if (alternate.kind === "cmyk") {
473
+ const k = at(3);
474
+ return [
475
+ to255((1 - at(0)) * (1 - k)),
476
+ to255((1 - at(1)) * (1 - k)),
477
+ to255((1 - at(2)) * (1 - k))
478
+ ];
479
+ }
480
+ if (alternate.kind === "lab" && alternate.lab) {
481
+ const r = alternate.lab.range;
482
+ const [sr, sg, sb] = labToSrgb(alternate.lab.white, [
483
+ at(0) * 100,
484
+ r[0] + at(1) * (r[1] - r[0]),
485
+ r[2] + at(2) * (r[3] - r[2])
486
+ ]);
487
+ return [
488
+ to255(sr),
489
+ to255(sg),
490
+ to255(sb)
491
+ ];
492
+ }
493
+ if (alternate.kind === "gray") {
494
+ const g = to255(at(0));
495
+ return [
496
+ g,
497
+ g,
498
+ g
499
+ ];
500
+ }
501
+ return [
502
+ to255(at(0)),
503
+ to255(at(1)),
504
+ to255(at(2))
505
+ ];
506
+ }
268
507
  function paletteRgb(base, lookup, off) {
269
508
  if (base.kind === "rgb") return [
270
509
  lookup[off] ?? 0,
271
510
  lookup[off + 1] ?? 0,
272
511
  lookup[off + 2] ?? 0
273
512
  ];
513
+ if (base.kind === "lab" && base.lab) {
514
+ const r = base.lab.range;
515
+ const at = (i, lo, hi) => lo + (lookup[off + i] ?? 0) / 255 * (hi - lo);
516
+ const [sr, sg, sb] = labToSrgb(base.lab.white, [
517
+ at(0, 0, 100),
518
+ at(1, r[0], r[1]),
519
+ at(2, r[2], r[3])
520
+ ]);
521
+ return [
522
+ Math.round(sr * 255),
523
+ Math.round(sg * 255),
524
+ Math.round(sb * 255)
525
+ ];
526
+ }
274
527
  if (base.kind === "cmyk") {
275
528
  const c = (lookup[off] ?? 0) / 255;
276
529
  const m = (lookup[off + 1] ?? 0) / 255;
@@ -406,15 +659,25 @@ function combineAlpha(color, alpha) {
406
659
  };
407
660
  }
408
661
  function filterNames(file, d) {
409
- const f = file.resolve(d.get("Filter") ?? PDF_NULL);
662
+ const f = file.resolve(d.get("Filter") ?? d.get("F") ?? PDF_NULL);
410
663
  const arr = Array.isArray(f) ? f : [f];
411
664
  const out = [];
412
665
  for (const x of arr) {
413
666
  const r = file.resolve(x);
414
- if (r instanceof PdfName) out.push(r.value);
667
+ if (r instanceof PdfName) out.push(FILTER_ABBREVIATIONS[r.value] ?? r.value);
415
668
  }
416
669
  return out;
417
670
  }
671
+ /** §8.9.7 Table 93 — the short name an inline image writes a filter under. */
672
+ var FILTER_ABBREVIATIONS = {
673
+ AHx: "ASCIIHexDecode",
674
+ A85: "ASCII85Decode",
675
+ LZW: "LZWDecode",
676
+ Fl: "FlateDecode",
677
+ RL: "RunLengthDecode",
678
+ CCF: "CCITTFaxDecode",
679
+ DCT: "DCTDecode"
680
+ };
418
681
  function decodeChain(file, stream, filters) {
419
682
  let data = stream.data;
420
683
  let mayPredict = false;
@@ -470,61 +733,6 @@ function lzwEarlyChange(file, d) {
470
733
  const ec = decodeParmsOf(file, d)?.get("EarlyChange");
471
734
  return ec !== void 0 && file.resolve(ec) === 0 ? 0 : 1;
472
735
  }
473
- function runLengthDecode(data) {
474
- const out = [];
475
- let i = 0;
476
- while (i < data.length) {
477
- const len = data[i++];
478
- if (len === 128) break;
479
- if (len < 128) for (let j = 0; j <= len && i < data.length; j++) out.push(data[i++]);
480
- else {
481
- const b = data[i++] ?? 0;
482
- for (let j = 0; j < 257 - len; j++) out.push(b);
483
- }
484
- }
485
- return Uint8Array.from(out);
486
- }
487
- function ascii85Decode(data) {
488
- const out = [];
489
- let tuple = 0;
490
- let count = 0;
491
- for (const c of data) {
492
- if (c === 126) break;
493
- if (c <= 32) continue;
494
- if (c === 122 && count === 0) {
495
- out.push(0, 0, 0, 0);
496
- continue;
497
- }
498
- if (c < 33 || c > 117) continue;
499
- tuple = tuple * 85 + (c - 33);
500
- if (++count === 5) {
501
- out.push(tuple >>> 24 & 255, tuple >>> 16 & 255, tuple >>> 8 & 255, tuple & 255);
502
- tuple = 0;
503
- count = 0;
504
- }
505
- }
506
- if (count > 0) {
507
- for (let i = count; i < 5; i++) tuple = tuple * 85 + 84;
508
- for (let i = 0; i < count - 1; i++) out.push(tuple >>> 24 - i * 8 & 255);
509
- }
510
- return Uint8Array.from(out);
511
- }
512
- function asciiHexDecode(data) {
513
- const out = [];
514
- let hi = -1;
515
- for (const c of data) {
516
- if (c === 62) break;
517
- const v = hexVal(c);
518
- if (v < 0) continue;
519
- if (hi < 0) hi = v;
520
- else {
521
- out.push(hi << 4 | v);
522
- hi = -1;
523
- }
524
- }
525
- if (hi >= 0) out.push(hi << 4);
526
- return Uint8Array.from(out);
527
- }
528
736
  function decodeParmsOf(file, d) {
529
737
  const p = file.resolve(d.get("DecodeParms") ?? d.get("DP") ?? PDF_NULL);
530
738
  if (p instanceof Map) return p;
@@ -561,12 +769,6 @@ function boolOf(v) {
561
769
  function clamp01(x) {
562
770
  return x < 0 ? 0 : x > 1 ? 1 : x;
563
771
  }
564
- function hexVal(c) {
565
- if (c >= 48 && c <= 57) return c - 48;
566
- if (c >= 65 && c <= 70) return c - 65 + 10;
567
- if (c >= 97 && c <= 102) return c - 97 + 10;
568
- return -1;
569
- }
570
772
  function fail(severity, detail) {
571
773
  return {
572
774
  ok: false,
@@ -1,3 +1,4 @@
1
+ import { ImageCrop } from '../core/document-model/types.js';
1
2
  import { Loss } from '../core/ir/index.js';
2
3
  import { PdfFile, PdfPage } from './document.js';
3
4
  /**
@@ -15,8 +16,25 @@ export interface PdfImage {
15
16
  /** Page-space lower-left corner (points, y-up). */
16
17
  readonly x: number;
17
18
  readonly y: number;
19
+ /**
20
+ * §8.9.5 — how far the CTM turns the picture, in degrees counter-clockwise.
21
+ * The box above is the picture's own, unturned; a turn spins it about its
22
+ * centre, which is what every downstream format does with one.
23
+ */
24
+ readonly rotationDeg?: number;
25
+ /**
26
+ * §8.5.4 — the fraction of each of the picture's OWN edges a clip cut away,
27
+ * where one bounded it: `a:srcRect` in DrawingML terms. Absent is whole.
28
+ */
29
+ readonly crop?: ImageCrop;
18
30
  /** Enclosing marked-content id, if the placement was inside a `/Figure`. */
19
31
  readonly mcid?: number;
32
+ /**
33
+ * §11.6.4.4 `/ca` — how opaque the picture is drawn, `0..1`. Absent is
34
+ * opaque. alphatrans.pdf lays a photograph in at half strength over three
35
+ * coloured squares, and drawn full-strength it hid all three.
36
+ */
37
+ readonly alpha?: number;
20
38
  /**
21
39
  * §8.5.3 — where this was painted, as the chain of positions leading to it.
22
40
  * The same key a lifted path carries, so the two can be ordered against each
@@ -1,10 +1,13 @@
1
1
  import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
2
2
  import { FEATURES } from "../core/ir/features.js";
3
+ import { encodePng } from "../core/png-encode.js";
4
+ import { buildAlphaMap, sampledShading } from "./shading.js";
3
5
  import { interpretContent, multiply } from "./content.js";
4
- import { decodePdfImage } from "./image-decode.js";
5
6
  import { collectPageAppearances } from "./annots.js";
7
+ import { hiddenProperties, hiddenXObject } from "./optional-content.js";
8
+ import { buildFonts } from "./text.js";
9
+ import { decodePdfImage } from "./image-decode.js";
6
10
  //#region src/pdf-reader/images.ts
7
- var NO_FONTS = /* @__PURE__ */ new Map();
8
11
  var MAX_FORM_DEPTH = 12;
9
12
  var MAX_IMAGES = 4096;
10
13
  /**
@@ -20,6 +23,15 @@ function collectPageImages(file, page) {
20
23
  const images = [];
21
24
  const lossByDetail = /* @__PURE__ */ new Map();
22
25
  const visiting = /* @__PURE__ */ new Set();
26
+ const alphaCache = /* @__PURE__ */ new Map();
27
+ const fontCache = /* @__PURE__ */ new Map();
28
+ const fontsOf = (resources) => {
29
+ const had = fontCache.get(resources);
30
+ if (had) return had;
31
+ const made = buildFonts(file, resources);
32
+ fontCache.set(resources, made);
33
+ return made;
34
+ };
23
35
  const addLoss = (severity, detail) => {
24
36
  if (!lossByDetail.has(detail)) lossByDetail.set(detail, {
25
37
  severity,
@@ -30,7 +42,12 @@ function collectPageImages(file, page) {
30
42
  const walk = (resources, content, baseCtm, depth, inheritedMcid, prefix) => {
31
43
  const xobjects = resources ? file.get(resources, "XObject") : PDF_NULL;
32
44
  const xobjDict = xobjects instanceof Map ? xobjects : void 0;
33
- const result = interpretContent(content, NO_FONTS, baseCtm);
45
+ let paints = alphaCache.get(resources);
46
+ if (!paints) {
47
+ paints = buildAlphaMap(file, resources);
48
+ alphaCache.set(resources, paints);
49
+ }
50
+ const result = interpretContent(content, fontsOf(resources), baseCtm, void 0, paints, void 0, hiddenProperties(file, resources));
34
51
  const patterns = resources ? file.get(resources, "Pattern") : PDF_NULL;
35
52
  const patternDict = patterns instanceof Map ? patterns : void 0;
36
53
  for (const vector of result.vectors) {
@@ -43,20 +60,67 @@ function collectPageImages(file, page) {
43
60
  walk(patternRes instanceof Map ? patternRes : resources, file.streamData(stream), multiply(matrixOf(file, stream.dict), baseCtm), depth + 1, inheritedMcid, [...prefix, vector.order]);
44
61
  visiting.delete(stream);
45
62
  }
63
+ for (const call of result.glyphs) {
64
+ if (depth >= MAX_FORM_DEPTH || visiting.has(call.stream)) continue;
65
+ visiting.add(call.stream);
66
+ walk(call.resources ?? resources, file.streamData(call.stream), call.ctm, depth + 1, inheritedMcid, [...prefix, call.order]);
67
+ visiting.delete(call.stream);
68
+ }
69
+ for (const paint of result.shadings) {
70
+ if (images.length >= MAX_IMAGES) return;
71
+ const dict = resources ? file.get(resources, "Shading") : PDF_NULL;
72
+ const shading = dict instanceof Map ? file.resolve(dict.get(paint.name) ?? PDF_NULL) : PDF_NULL;
73
+ const sh = shading instanceof PdfStream ? shading.dict : shading instanceof Map ? shading : void 0;
74
+ if (!sh) continue;
75
+ if (paint.masked) continue;
76
+ const sampled = sampledShading(file, sh);
77
+ if (!sampled) continue;
78
+ const placed = multiply(matrixOf(file, sh), paint.ctm);
79
+ const [dx0, dx1, dy0, dy1] = sampled.domain;
80
+ const unit = [
81
+ dx1 - dx0,
82
+ 0,
83
+ 0,
84
+ dy1 - dy0,
85
+ dx0,
86
+ dy0
87
+ ];
88
+ images.push({
89
+ ...geometry(multiply(unit, placed), {
90
+ bytes: encodePng(sampled.size, sampled.size, "rgb", sampled.rgb),
91
+ format: "png"
92
+ }, inheritedMcid, paint.clip),
93
+ orderKey: [...prefix, paint.order]
94
+ });
95
+ }
46
96
  for (const placement of result.images) {
47
97
  if (images.length >= MAX_IMAGES) return;
98
+ if (placement.inline) {
99
+ const decoded = decodePdfImage(file, new PdfStream(placement.inline.dict, placement.inline.data), placement.fillHex);
100
+ if (decoded.ok) {
101
+ images.push({
102
+ ...geometry(placement.ctm, decoded, placement.mcid ?? inheritedMcid, placement.clip, placement.alpha),
103
+ orderKey: [...prefix, placement.order]
104
+ });
105
+ if (decoded.degraded) addLoss("degraded", decoded.degraded);
106
+ } else addLoss(decoded.severity, decoded.detail);
107
+ continue;
108
+ }
48
109
  const stream = xobjDict ? file.resolve(xobjDict.get(placement.name) ?? PDF_NULL) : PDF_NULL;
49
110
  if (!(stream instanceof PdfStream)) continue;
111
+ if (hiddenXObject(file, stream)) continue;
50
112
  const subtype = nameOf(file.get(stream.dict, "Subtype"));
51
113
  const mcid = placement.mcid ?? inheritedMcid;
52
114
  if (subtype === "Image") {
53
- const decoded = decodePdfImage(file, stream);
115
+ const decoded = decodePdfImage(file, stream, placement.fillHex);
54
116
  if (decoded.ok) {
55
117
  images.push({
56
- ...geometry(placement.ctm, decoded, mcid),
118
+ ...geometry(placement.ctm, decoded, mcid, placement.clip, placement.alpha),
57
119
  orderKey: [...prefix, placement.order]
58
120
  });
59
121
  if (decoded.degraded) addLoss("degraded", decoded.degraded);
122
+ if (placement.blend !== void 0) addLoss("degraded", `PDF blend mode /${placement.blend} is not performed; the picture is drawn over what it was to blend with`);
123
+ if (placement.masked === true) addLoss("degraded", "PDF soft mask (/SMask in the graphics state) is not applied; the picture is drawn at full opacity throughout");
60
124
  } else addLoss(decoded.severity, decoded.detail);
61
125
  } else if (subtype === "Form" && depth < MAX_FORM_DEPTH && !visiting.has(stream)) {
62
126
  visiting.add(stream);
@@ -82,16 +146,95 @@ function collectPageImages(file, page) {
82
146
  losses: [...lossByDetail.values()]
83
147
  };
84
148
  }
85
- function geometry(ctm, decoded, mcid) {
149
+ function geometry(ctm, decoded, mcid, clip, alpha) {
150
+ const widthPt = Math.hypot(ctm[0], ctm[1]) || 1;
151
+ const heightPt = Math.hypot(ctm[2], ctm[3]) || 1;
152
+ const angle = Math.atan2(ctm[1], ctm[0]) * 180 / Math.PI;
153
+ const box = clippedUnitBox(ctm, clip);
154
+ const u = (box.u0 + box.u1) / 2;
155
+ const v = (box.v0 + box.v1) / 2;
156
+ const cx = ctm[0] * u + ctm[2] * v + ctm[4];
157
+ const cy = ctm[1] * u + ctm[3] * v + ctm[5];
158
+ const shownW = widthPt * (box.u1 - box.u0);
159
+ const shownH = heightPt * (box.v1 - box.v0);
160
+ const crop = box.u0 > 0 || box.v0 > 0 || box.u1 < 1 || box.v1 < 1 ? {
161
+ left: box.u0,
162
+ right: 1 - box.u1,
163
+ top: 1 - box.v1,
164
+ bottom: box.v0
165
+ } : void 0;
86
166
  return {
87
167
  bytes: decoded.bytes,
88
168
  format: decoded.format,
89
- widthPt: Math.hypot(ctm[0], ctm[1]) || 1,
90
- heightPt: Math.hypot(ctm[2], ctm[3]) || 1,
91
- x: ctm[4],
92
- y: ctm[5],
93
- ...mcid !== void 0 ? { mcid } : {}
169
+ widthPt: shownW,
170
+ heightPt: shownH,
171
+ x: cx - shownW / 2,
172
+ y: cy - shownH / 2,
173
+ ...crop ? { crop } : {},
174
+ ...Math.abs(angle) > .5 ? { rotationDeg: angle } : {},
175
+ ...mcid !== void 0 ? { mcid } : {},
176
+ ...alpha !== void 0 ? { alpha } : {}
177
+ };
178
+ }
179
+ /** Below this the clip took a sliver off an edge and is not worth a crop. */
180
+ var CROPS = .01;
181
+ /**
182
+ * §8.5.4 — the part of the unit square a clip leaves showing, in the PICTURE's
183
+ * own axes.
184
+ *
185
+ * A clip is stated in the page's coordinates and a picture may be turned within
186
+ * them, so intersecting the two boxes as they stand crops along the wrong axes.
187
+ * Carried back through the placement matrix the clip lands in the unit square
188
+ * the image is drawn into, where a crop is what `a:srcRect` means: the fraction
189
+ * off each of the picture's own edges. image-rotated-black-white-ratio.pdf
190
+ * turns picture and clip together by thirty-one degrees, and in that space the
191
+ * clip is square on and takes the middle 53% of both sides.
192
+ *
193
+ * Where the clip is turned differently from the picture this bounds it rather
194
+ * than cutting it exactly — a rectangle is all `a:srcRect` can say.
195
+ */
196
+ function clippedUnitBox(ctm, clip) {
197
+ const whole = {
198
+ u0: 0,
199
+ v0: 0,
200
+ u1: 1,
201
+ v1: 1
202
+ };
203
+ if (!clip) return whole;
204
+ const det = ctm[0] * ctm[3] - ctm[1] * ctm[2];
205
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-9) return whole;
206
+ let u0 = Infinity;
207
+ let v0 = Infinity;
208
+ let u1 = -Infinity;
209
+ let v1 = -Infinity;
210
+ const add = (x, y) => {
211
+ const dx = x - ctm[4];
212
+ const dy = y - ctm[5];
213
+ const u = (ctm[3] * dx - ctm[2] * dy) / det;
214
+ const v = (ctm[0] * dy - ctm[1] * dx) / det;
215
+ u0 = Math.min(u0, u);
216
+ v0 = Math.min(v0, v);
217
+ u1 = Math.max(u1, u);
218
+ v1 = Math.max(v1, v);
219
+ };
220
+ for (const seg of clip.segs) {
221
+ if (seg.op === "close") continue;
222
+ if (seg.op === "cubic") {
223
+ add(seg.x1, seg.y1);
224
+ add(seg.x2, seg.y2);
225
+ }
226
+ add(seg.x, seg.y);
227
+ }
228
+ if (!Number.isFinite(u0) || !Number.isFinite(v0)) return whole;
229
+ const cut = {
230
+ u0: Math.min(1, Math.max(0, u0)),
231
+ v0: Math.min(1, Math.max(0, v0)),
232
+ u1: Math.min(1, Math.max(0, u1)),
233
+ v1: Math.min(1, Math.max(0, v1))
94
234
  };
235
+ if (!(cut.u1 - cut.u0 > CROPS) || !(cut.v1 - cut.v0 > CROPS)) return whole;
236
+ if (cut.u1 - cut.u0 > 1 - CROPS && cut.v1 - cut.v0 > 1 - CROPS) return whole;
237
+ return cut;
95
238
  }
96
239
  function matrixOf(file, dict) {
97
240
  const m = file.resolve(dict.get("Matrix") ?? PDF_NULL);