reamkit 1.25.1 → 1.27.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 (52) hide show
  1. package/README.md +12 -5
  2. package/dist/esm/pdf-reader/annot-draw.d.ts +65 -0
  3. package/dist/esm/pdf-reader/annot-draw.js +374 -0
  4. package/dist/esm/pdf-reader/annots.d.ts +3 -1
  5. package/dist/esm/pdf-reader/annots.js +18 -4
  6. package/dist/esm/pdf-reader/ccitt.d.ts +18 -0
  7. package/dist/esm/pdf-reader/ccitt.js +70 -2
  8. package/dist/esm/pdf-reader/cie-color.d.ts +33 -0
  9. package/dist/esm/pdf-reader/cie-color.js +112 -0
  10. package/dist/esm/pdf-reader/content.d.ts +47 -17
  11. package/dist/esm/pdf-reader/content.js +175 -14
  12. package/dist/esm/pdf-reader/display.d.ts +1 -1
  13. package/dist/esm/pdf-reader/display.js +21 -6
  14. package/dist/esm/pdf-reader/document.d.ts +6 -0
  15. package/dist/esm/pdf-reader/document.js +29 -1
  16. package/dist/esm/pdf-reader/embedded-fonts.d.ts +12 -3
  17. package/dist/esm/pdf-reader/embedded-fonts.js +22 -3
  18. package/dist/esm/pdf-reader/flow-build.d.ts +12 -4
  19. package/dist/esm/pdf-reader/flow-build.js +102 -8
  20. package/dist/esm/pdf-reader/font.js +97 -16
  21. package/dist/esm/pdf-reader/function.d.ts +16 -0
  22. package/dist/esm/pdf-reader/function.js +414 -0
  23. package/dist/esm/pdf-reader/glyph-names.d.ts +6 -0
  24. package/dist/esm/pdf-reader/glyph-names.js +408 -0
  25. package/dist/esm/pdf-reader/image-decode.d.ts +8 -4
  26. package/dist/esm/pdf-reader/image-decode.js +224 -12
  27. package/dist/esm/pdf-reader/images.d.ts +12 -0
  28. package/dist/esm/pdf-reader/images.js +109 -9
  29. package/dist/esm/pdf-reader/jbig2.d.ts +109 -0
  30. package/dist/esm/pdf-reader/jbig2.js +2606 -0
  31. package/dist/esm/pdf-reader/layout.d.ts +32 -0
  32. package/dist/esm/pdf-reader/layout.js +189 -40
  33. package/dist/esm/pdf-reader/lexer.d.ts +2 -0
  34. package/dist/esm/pdf-reader/lexer.js +4 -0
  35. package/dist/esm/pdf-reader/optional-content.d.ts +36 -0
  36. package/dist/esm/pdf-reader/optional-content.js +93 -0
  37. package/dist/esm/pdf-reader/reader.d.ts +5 -2
  38. package/dist/esm/pdf-reader/reader.js +80 -7
  39. package/dist/esm/pdf-reader/shading.d.ts +71 -6
  40. package/dist/esm/pdf-reader/shading.js +185 -15
  41. package/dist/esm/pdf-reader/standard-metrics.d.ts +8 -0
  42. package/dist/esm/pdf-reader/standard-metrics.js +18 -0
  43. package/dist/esm/pdf-reader/standard-widths.d.ts +20 -0
  44. package/dist/esm/pdf-reader/standard-widths.js +62 -0
  45. package/dist/esm/pdf-reader/tagged.js +204 -32
  46. package/dist/esm/pdf-reader/text-rules.d.ts +16 -0
  47. package/dist/esm/pdf-reader/text-rules.js +112 -0
  48. package/dist/esm/pdf-reader/text.js +78 -4
  49. package/dist/esm/pdf-reader/vector.d.ts +5 -0
  50. package/dist/esm/pdf-reader/vector.js +39 -25
  51. package/dist/esm/word/docx-writer.js +213 -23
  52. package/package.json +1 -1
@@ -2,7 +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 { labToSrgb } from "./cie-color.js";
5
6
  import { decodeCcitt } from "./ccitt.js";
7
+ import { decodeJbig2 } from "./jbig2.js";
6
8
  import { decodeJpeg } from "./jpeg.js";
7
9
  import { unzlibSync } from "fflate";
8
10
  //#region src/pdf-reader/image-decode.ts
@@ -15,19 +17,23 @@ var MAX_PIXELS = 4e7;
15
17
  * Supports DeviceGray/RGB/CMYK, CalGray/CalRGB, ICCBased (by `/N`) and Indexed
16
18
  * colour spaces; Flate, LZW (EP12), RunLength, ASCII85, ASCIIHex and CCITT
17
19
  * Group 4 / Group 3 1-D fax (EP15) filters; PNG/TIFF predictors; bit depths
18
- * 1/2/4/8/16; and an `/SMask` folded in as the PNG alpha channel. Unsupported
19
- * inputs (stencil `/ImageMask`, Separation/DeviceN/Lab, JBIG2, CCITT Group 3
20
- * 2-D) return a typed failure so the caller records a loss.
20
+ * 1/2/4/8/16; an `/SMask` folded in as the PNG alpha channel; and a stencil
21
+ * `/ImageMask` (§8.9.6.2) given back as RGBA in `fillHex`. Unsupported inputs
22
+ * (Separation/DeviceN/Lab, CCITT Group 3 2-D) return a typed failure so the
23
+ * caller records a loss.
21
24
  *
25
+ * @param file The owning file.
26
+ * @param stream The image XObject, or an inline image wrapped as one.
27
+ * @param fillHex §8.9.6.2 — the non-stroking colour a stencil mask paints in.
22
28
  * @returns The decoded image, or `{ ok: false }` with the loss severity and reason.
23
29
  */
24
- function decodePdfImage(file, stream) {
30
+ function decodePdfImage(file, stream, fillHex) {
25
31
  const d = stream.dict;
26
32
  const width = intOf(file.get(d, "Width")) || intOf(file.get(d, "W"));
27
33
  const height = intOf(file.get(d, "Height")) || intOf(file.get(d, "H"));
28
34
  if (width <= 0 || height <= 0) return fail("dropped", "image with no dimensions");
29
35
  if (width * height > MAX_PIXELS) return fail("dropped", "image too large to decode");
30
- if (boolOf(d.get("ImageMask")) || boolOf(d.get("IM"))) return fail("dropped", "stencil image mask not reconstructed");
36
+ if (boolOf(d.get("ImageMask")) || boolOf(d.get("IM"))) return stencil(file, stream, width, height, fillHex);
31
37
  const filters = filterNames(file, d);
32
38
  const last = filters[filters.length - 1];
33
39
  if (last === "DCTDecode" || last === "DCT") {
@@ -52,7 +58,17 @@ function decodePdfImage(file, stream) {
52
58
  heightPx: height,
53
59
  degraded: "JPEG 2000 image — limited viewer support"
54
60
  };
55
- if (last === "JBIG2Decode") return fail("dropped", "JBIG2-encoded image not decoded");
61
+ if (last === "JBIG2Decode") {
62
+ const raster = decodeJbig2Image(file, stream, filters, width, height);
63
+ if (typeof raster === "string") return fail("dropped", raster);
64
+ return {
65
+ ok: true,
66
+ bytes: encodePng(width, height, raster.color, raster.samples),
67
+ format: "png",
68
+ widthPx: width,
69
+ heightPx: height
70
+ };
71
+ }
56
72
  const decoded = last === "CCITTFaxDecode" || last === "CCF" ? decodeCcittImage(file, stream, filters, width, height) : decodeToSamples(file, stream, filters, width, height);
57
73
  if (typeof decoded === "string") return fail("dropped", decoded);
58
74
  const alpha = decodeSMask(file, d, width, height);
@@ -75,6 +91,26 @@ function decodeToSamples(file, stream, filters, width, height) {
75
91
  const decodeArr = decodeArrayOf(file, d);
76
92
  return toColor(cs, unpackSamples(raw, width, height, cs.components, bpc), width * height, bpc, decodeArr);
77
93
  }
94
+ /**
95
+ * §7.4.7 — a `/JBIG2Decode` image: the stream's own segments, plus the shared
96
+ * ones a `/JBIG2Globals` holds (the symbol dictionary a scanned book stores
97
+ * once for all its pages).
98
+ */
99
+ function decodeJbig2Image(file, stream, filters, width, height) {
100
+ const parms = decodeParmsOf(file, stream.dict);
101
+ const globalsVal = parms ? file.get(parms, "JBIG2Globals") : void 0;
102
+ const globalsStream = globalsVal instanceof PdfStream ? globalsVal : void 0;
103
+ const globals = globalsStream ? file.streamData(globalsStream) : void 0;
104
+ const packed = decodeJbig2(applyChainExceptLast(filters, stream.data), globals, width, height);
105
+ if (!packed) return "JBIG2 image not decoded";
106
+ const rowBytes = width + 7 >> 3;
107
+ const samples = new Uint8Array(width * height);
108
+ for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) samples[y * width + x] = packed[y * rowBytes + (x >> 3)] >> 7 - (x & 7) & 1 ? 0 : 255;
109
+ return {
110
+ color: "gray",
111
+ samples
112
+ };
113
+ }
78
114
  function decodeCcittImage(file, stream, filters, width, height) {
79
115
  const parms = decodeParmsOf(file, stream.dict);
80
116
  const columns = (parms ? intOf(file.get(parms, "Columns")) : 0) || 1728;
@@ -85,13 +121,14 @@ function decodeCcittImage(file, stream, filters, width, height) {
85
121
  byteAlign: parms ? boolOf(file.get(parms, "EncodedByteAlign")) : false
86
122
  });
87
123
  if (!packed) return "CCITT fax image not decoded (Group 3 2-D or malformed)";
124
+ const blackIs1 = parms ? boolOf(file.get(parms, "BlackIs1")) : false;
88
125
  const rowBytes = columns + 7 >> 3;
89
126
  const samples = new Uint8Array(width * height);
90
127
  for (let y = 0; y < height; y++) {
91
128
  const rowOff = y * rowBytes;
92
129
  for (let x = 0; x < width; x++) {
93
- const black = x < columns ? packed[rowOff + (x >> 3)] >> 7 - (x & 7) & 1 : 0;
94
- samples[y * width + x] = black ? 0 : 255;
130
+ const bit = x < columns ? packed[rowOff + (x >> 3)] >> 7 - (x & 7) & 1 : 0;
131
+ samples[y * width + x] = bit === (blackIs1 ? 0 : 1) ? 0 : 255;
95
132
  }
96
133
  }
97
134
  return {
@@ -99,6 +136,89 @@ function decodeCcittImage(file, stream, filters, width, height) {
99
136
  samples
100
137
  };
101
138
  }
139
+ /**
140
+ * §8.9.6.2 — a stencil mask: one bit per pixel saying WHERE to paint, and the
141
+ * colour is whatever the page's non-stroking colour was at the `Do`.
142
+ *
143
+ * A sample of 0 paints and 1 leaves the page alone, which `/Decode [1 0]`
144
+ * swaps. Given back as an RGBA picture — the fill colour throughout, opaque
145
+ * where the stencil paints and clear where it does not — which is what the mask
146
+ * MEANS and what every format downstream can show.
147
+ * images_1bit_grayscale.pdf draws two of them and they were dropped entirely.
148
+ */
149
+ function stencil(file, stream, width, height, fillHex) {
150
+ const d = stream.dict;
151
+ const filters = filterNames(file, d);
152
+ const last = filters[filters.length - 1];
153
+ const packed = last === "CCITTFaxDecode" || last === "CCF" ? ccittBits(file, stream, filters, height) : last === "JBIG2Decode" ? jbig2Bits(file, stream, filters, width, height) : decodeChain(file, stream, filters);
154
+ if (!packed) return fail("dropped", "stencil image mask not decoded");
155
+ const decode = file.resolve(d.get("Decode") ?? d.get("D") ?? PDF_NULL);
156
+ const paintBit = Array.isArray(decode) && file.resolve(decode[0] ?? PDF_NULL) === 1 ? 1 : 0;
157
+ const rgb = fillHex !== void 0 ? parseHex(fillHex) : [
158
+ 0,
159
+ 0,
160
+ 0
161
+ ];
162
+ const rowBytes = width + 7 >> 3;
163
+ if (packed.length < rowBytes * height) return fail("dropped", "stencil image mask truncated");
164
+ const samples = new Uint8Array(width * height * 4);
165
+ for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
166
+ const bit = packed[y * rowBytes + (x >> 3)] >> 7 - (x & 7) & 1;
167
+ const at = (y * width + x) * 4;
168
+ samples[at] = rgb[0];
169
+ samples[at + 1] = rgb[1];
170
+ samples[at + 2] = rgb[2];
171
+ samples[at + 3] = bit === paintBit ? 255 : 0;
172
+ }
173
+ return {
174
+ ok: true,
175
+ bytes: encodePng(width, height, "rgba", samples),
176
+ format: "png",
177
+ widthPx: width,
178
+ heightPx: height
179
+ };
180
+ }
181
+ /** A 6-hex colour as its three bytes. */
182
+ function parseHex(hex) {
183
+ const v = Number.parseInt(hex, 16);
184
+ if (!Number.isFinite(v)) return [
185
+ 0,
186
+ 0,
187
+ 0
188
+ ];
189
+ return [
190
+ v >> 16 & 255,
191
+ v >> 8 & 255,
192
+ v & 255
193
+ ];
194
+ }
195
+ /** A stencil's bits where the file coded them as fax (§7.4.6). */
196
+ function ccittBits(file, stream, filters, height) {
197
+ const parms = decodeParmsOf(file, stream.dict);
198
+ const columns = (parms ? intOf(file.get(parms, "Columns")) : 0) || 1728;
199
+ const packed = decodeCcitt(applyChainExceptLast(filters, stream.data), {
200
+ k: parms ? intOf(file.get(parms, "K")) : 0,
201
+ columns,
202
+ rows: height,
203
+ byteAlign: parms ? boolOf(file.get(parms, "EncodedByteAlign")) : false
204
+ });
205
+ if (!packed) return void 0;
206
+ if (parms ? boolOf(file.get(parms, "BlackIs1")) : false) return packed;
207
+ const flipped = new Uint8Array(packed.length);
208
+ for (let i = 0; i < packed.length; i++) flipped[i] = ~packed[i] & 255;
209
+ return flipped;
210
+ }
211
+ /** …or as JBIG2, which codes 1 as black and so paints on 1. */
212
+ function jbig2Bits(file, stream, filters, width, height) {
213
+ const parms = decodeParmsOf(file, stream.dict);
214
+ const globalsVal = parms ? file.get(parms, "JBIG2Globals") : void 0;
215
+ const globals = globalsVal instanceof PdfStream ? file.streamData(globalsVal) : void 0;
216
+ const packed = decodeJbig2(applyChainExceptLast(filters, stream.data), globals, width, height);
217
+ if (!packed) return void 0;
218
+ const flipped = new Uint8Array(packed.length);
219
+ for (let i = 0; i < packed.length; i++) flipped[i] = ~packed[i] & 255;
220
+ return flipped;
221
+ }
102
222
  function resolveColorSpace(file, csVal) {
103
223
  const cs = file.resolve(csVal ?? PDF_NULL);
104
224
  if (cs instanceof PdfName) return namedColorSpace(cs.value);
@@ -147,6 +267,39 @@ function resolveColorSpace(file, csVal) {
147
267
  lookup
148
268
  };
149
269
  }
270
+ if (tag === "Lab") {
271
+ const params = file.resolve(cs[1] ?? PDF_NULL);
272
+ const dict = params instanceof Map ? params : void 0;
273
+ const nums = (v) => {
274
+ const r = v !== void 0 ? file.resolve(v) : void 0;
275
+ return Array.isArray(r) ? r.map((x) => typeof file.resolve(x) === "number" ? file.resolve(x) : 0) : [];
276
+ };
277
+ const white = dict ? nums(file.get(dict, "WhitePoint")) : [];
278
+ if (white.length < 3 || !(white[1] > 0)) return void 0;
279
+ const range = dict ? nums(file.get(dict, "Range")) : [];
280
+ return {
281
+ kind: "lab",
282
+ components: 3,
283
+ lab: {
284
+ white: [
285
+ white[0],
286
+ white[1],
287
+ white[2]
288
+ ],
289
+ range: range.length === 4 ? [
290
+ range[0],
291
+ range[1],
292
+ range[2],
293
+ range[3]
294
+ ] : [
295
+ -100,
296
+ 100,
297
+ -100,
298
+ 100
299
+ ]
300
+ }
301
+ };
302
+ }
150
303
  }
151
304
  function namedColorSpace(name) {
152
305
  if (name === "DeviceGray" || name === "G" || name === "CalGray") return {
@@ -210,13 +363,48 @@ function toColor(cs, s, px, bpc, decode) {
210
363
  samples: out
211
364
  };
212
365
  }
366
+ if (cs.kind === "lab") {
367
+ const lab = cs.lab;
368
+ const out = new Uint8Array(px * 3);
369
+ const span = (i) => decode && decode.length >= 6 ? [0, 1] : i === 0 ? [0, 100] : [lab.range[(i - 1) * 2], lab.range[(i - 1) * 2 + 1]];
370
+ for (let i = 0; i < px; i++) {
371
+ const comp = [
372
+ 0,
373
+ 1,
374
+ 2
375
+ ].map((k) => {
376
+ const t = c01(s[i * 3 + k], k);
377
+ const [lo, hi] = span(k);
378
+ return lo + t * (hi - lo);
379
+ });
380
+ const [r, g, b] = labToSrgb(lab.white, [
381
+ comp[0],
382
+ comp[1],
383
+ comp[2]
384
+ ]);
385
+ out[i * 3] = Math.round(r * 255);
386
+ out[i * 3 + 1] = Math.round(g * 255);
387
+ out[i * 3 + 2] = Math.round(b * 255);
388
+ }
389
+ return {
390
+ color: "rgb",
391
+ samples: out
392
+ };
393
+ }
213
394
  const base = cs.base;
214
395
  const lookup = cs.lookup;
215
396
  const hival = cs.hival ?? 255;
216
397
  const bn = base.components;
398
+ const index = (v) => {
399
+ if (!decode || decode.length < 2) return Math.min(v, hival);
400
+ const lo = decode[0];
401
+ const hi = decode[1];
402
+ const at = maxv > 0 ? lo + v / maxv * (hi - lo) : lo;
403
+ return Math.min(Math.max(0, Math.round(at)), hival);
404
+ };
217
405
  if (base.kind === "gray") {
218
406
  const out = new Uint8Array(px);
219
- for (let i = 0; i < px; i++) out[i] = lookup[Math.min(s[i], hival) * bn] ?? 0;
407
+ for (let i = 0; i < px; i++) out[i] = lookup[index(s[i]) * bn] ?? 0;
220
408
  return {
221
409
  color: "gray",
222
410
  samples: out
@@ -224,7 +412,7 @@ function toColor(cs, s, px, bpc, decode) {
224
412
  }
225
413
  const out = new Uint8Array(px * 3);
226
414
  for (let i = 0; i < px; i++) {
227
- const [r, g, b] = paletteRgb(base, lookup, Math.min(s[i], hival) * bn);
415
+ const [r, g, b] = paletteRgb(base, lookup, index(s[i]) * bn);
228
416
  out[i * 3] = r;
229
417
  out[i * 3 + 1] = g;
230
418
  out[i * 3 + 2] = b;
@@ -240,6 +428,20 @@ function paletteRgb(base, lookup, off) {
240
428
  lookup[off + 1] ?? 0,
241
429
  lookup[off + 2] ?? 0
242
430
  ];
431
+ if (base.kind === "lab" && base.lab) {
432
+ const r = base.lab.range;
433
+ const at = (i, lo, hi) => lo + (lookup[off + i] ?? 0) / 255 * (hi - lo);
434
+ const [sr, sg, sb] = labToSrgb(base.lab.white, [
435
+ at(0, 0, 100),
436
+ at(1, r[0], r[1]),
437
+ at(2, r[2], r[3])
438
+ ]);
439
+ return [
440
+ Math.round(sr * 255),
441
+ Math.round(sg * 255),
442
+ Math.round(sb * 255)
443
+ ];
444
+ }
243
445
  if (base.kind === "cmyk") {
244
446
  const c = (lookup[off] ?? 0) / 255;
245
447
  const m = (lookup[off + 1] ?? 0) / 255;
@@ -375,15 +577,25 @@ function combineAlpha(color, alpha) {
375
577
  };
376
578
  }
377
579
  function filterNames(file, d) {
378
- const f = file.resolve(d.get("Filter") ?? PDF_NULL);
580
+ const f = file.resolve(d.get("Filter") ?? d.get("F") ?? PDF_NULL);
379
581
  const arr = Array.isArray(f) ? f : [f];
380
582
  const out = [];
381
583
  for (const x of arr) {
382
584
  const r = file.resolve(x);
383
- if (r instanceof PdfName) out.push(r.value);
585
+ if (r instanceof PdfName) out.push(FILTER_ABBREVIATIONS[r.value] ?? r.value);
384
586
  }
385
587
  return out;
386
588
  }
589
+ /** §8.9.7 Table 93 — the short name an inline image writes a filter under. */
590
+ var FILTER_ABBREVIATIONS = {
591
+ AHx: "ASCIIHexDecode",
592
+ A85: "ASCII85Decode",
593
+ LZW: "LZWDecode",
594
+ Fl: "FlateDecode",
595
+ RL: "RunLengthDecode",
596
+ CCF: "CCITTFaxDecode",
597
+ DCT: "DCTDecode"
598
+ };
387
599
  function decodeChain(file, stream, filters) {
388
600
  let data = stream.data;
389
601
  let mayPredict = 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,6 +16,17 @@ 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;
20
32
  /**
@@ -1,8 +1,10 @@
1
1
  import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
2
2
  import { FEATURES } from "../core/ir/features.js";
3
3
  import { interpretContent, multiply } from "./content.js";
4
- import { decodePdfImage } from "./image-decode.js";
4
+ import { buildAlphaMap } from "./shading.js";
5
5
  import { collectPageAppearances } from "./annots.js";
6
+ import { hiddenProperties, hiddenXObject } from "./optional-content.js";
7
+ import { decodePdfImage } from "./image-decode.js";
6
8
  //#region src/pdf-reader/images.ts
7
9
  var NO_FONTS = /* @__PURE__ */ new Map();
8
10
  var MAX_FORM_DEPTH = 12;
@@ -20,6 +22,7 @@ function collectPageImages(file, page) {
20
22
  const images = [];
21
23
  const lossByDetail = /* @__PURE__ */ new Map();
22
24
  const visiting = /* @__PURE__ */ new Set();
25
+ const alphaCache = /* @__PURE__ */ new Map();
23
26
  const addLoss = (severity, detail) => {
24
27
  if (!lossByDetail.has(detail)) lossByDetail.set(detail, {
25
28
  severity,
@@ -30,7 +33,12 @@ function collectPageImages(file, page) {
30
33
  const walk = (resources, content, baseCtm, depth, inheritedMcid, prefix) => {
31
34
  const xobjects = resources ? file.get(resources, "XObject") : PDF_NULL;
32
35
  const xobjDict = xobjects instanceof Map ? xobjects : void 0;
33
- const result = interpretContent(content, NO_FONTS, baseCtm);
36
+ let paints = alphaCache.get(resources);
37
+ if (!paints) {
38
+ paints = buildAlphaMap(file, resources);
39
+ alphaCache.set(resources, paints);
40
+ }
41
+ const result = interpretContent(content, NO_FONTS, baseCtm, void 0, paints, void 0, hiddenProperties(file, resources));
34
42
  const patterns = resources ? file.get(resources, "Pattern") : PDF_NULL;
35
43
  const patternDict = patterns instanceof Map ? patterns : void 0;
36
44
  for (const vector of result.vectors) {
@@ -45,18 +53,32 @@ function collectPageImages(file, page) {
45
53
  }
46
54
  for (const placement of result.images) {
47
55
  if (images.length >= MAX_IMAGES) return;
56
+ if (placement.inline) {
57
+ const decoded = decodePdfImage(file, new PdfStream(placement.inline.dict, placement.inline.data), placement.fillHex);
58
+ if (decoded.ok) {
59
+ images.push({
60
+ ...geometry(placement.ctm, decoded, placement.mcid ?? inheritedMcid, placement.clip),
61
+ orderKey: [...prefix, placement.order]
62
+ });
63
+ if (decoded.degraded) addLoss("degraded", decoded.degraded);
64
+ } else addLoss(decoded.severity, decoded.detail);
65
+ continue;
66
+ }
48
67
  const stream = xobjDict ? file.resolve(xobjDict.get(placement.name) ?? PDF_NULL) : PDF_NULL;
49
68
  if (!(stream instanceof PdfStream)) continue;
69
+ if (hiddenXObject(file, stream)) continue;
50
70
  const subtype = nameOf(file.get(stream.dict, "Subtype"));
51
71
  const mcid = placement.mcid ?? inheritedMcid;
52
72
  if (subtype === "Image") {
53
- const decoded = decodePdfImage(file, stream);
73
+ const decoded = decodePdfImage(file, stream, placement.fillHex);
54
74
  if (decoded.ok) {
55
75
  images.push({
56
- ...geometry(placement.ctm, decoded, mcid),
76
+ ...geometry(placement.ctm, decoded, mcid, placement.clip),
57
77
  orderKey: [...prefix, placement.order]
58
78
  });
59
79
  if (decoded.degraded) addLoss("degraded", decoded.degraded);
80
+ 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`);
81
+ 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
82
  } else addLoss(decoded.severity, decoded.detail);
61
83
  } else if (subtype === "Form" && depth < MAX_FORM_DEPTH && !visiting.has(stream)) {
62
84
  visiting.add(stream);
@@ -82,17 +104,95 @@ function collectPageImages(file, page) {
82
104
  losses: [...lossByDetail.values()]
83
105
  };
84
106
  }
85
- function geometry(ctm, decoded, mcid) {
107
+ function geometry(ctm, decoded, mcid, clip) {
108
+ const widthPt = Math.hypot(ctm[0], ctm[1]) || 1;
109
+ const heightPt = Math.hypot(ctm[2], ctm[3]) || 1;
110
+ const angle = Math.atan2(ctm[1], ctm[0]) * 180 / Math.PI;
111
+ const box = clippedUnitBox(ctm, clip);
112
+ const u = (box.u0 + box.u1) / 2;
113
+ const v = (box.v0 + box.v1) / 2;
114
+ const cx = ctm[0] * u + ctm[2] * v + ctm[4];
115
+ const cy = ctm[1] * u + ctm[3] * v + ctm[5];
116
+ const shownW = widthPt * (box.u1 - box.u0);
117
+ const shownH = heightPt * (box.v1 - box.v0);
118
+ const crop = box.u0 > 0 || box.v0 > 0 || box.u1 < 1 || box.v1 < 1 ? {
119
+ left: box.u0,
120
+ right: 1 - box.u1,
121
+ top: 1 - box.v1,
122
+ bottom: box.v0
123
+ } : void 0;
86
124
  return {
87
125
  bytes: decoded.bytes,
88
126
  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],
127
+ widthPt: shownW,
128
+ heightPt: shownH,
129
+ x: cx - shownW / 2,
130
+ y: cy - shownH / 2,
131
+ ...crop ? { crop } : {},
132
+ ...Math.abs(angle) > .5 ? { rotationDeg: angle } : {},
93
133
  ...mcid !== void 0 ? { mcid } : {}
94
134
  };
95
135
  }
136
+ /** Below this the clip took a sliver off an edge and is not worth a crop. */
137
+ var CROPS = .01;
138
+ /**
139
+ * §8.5.4 — the part of the unit square a clip leaves showing, in the PICTURE's
140
+ * own axes.
141
+ *
142
+ * A clip is stated in the page's coordinates and a picture may be turned within
143
+ * them, so intersecting the two boxes as they stand crops along the wrong axes.
144
+ * Carried back through the placement matrix the clip lands in the unit square
145
+ * the image is drawn into, where a crop is what `a:srcRect` means: the fraction
146
+ * off each of the picture's own edges. image-rotated-black-white-ratio.pdf
147
+ * turns picture and clip together by thirty-one degrees, and in that space the
148
+ * clip is square on and takes the middle 53% of both sides.
149
+ *
150
+ * Where the clip is turned differently from the picture this bounds it rather
151
+ * than cutting it exactly — a rectangle is all `a:srcRect` can say.
152
+ */
153
+ function clippedUnitBox(ctm, clip) {
154
+ const whole = {
155
+ u0: 0,
156
+ v0: 0,
157
+ u1: 1,
158
+ v1: 1
159
+ };
160
+ if (!clip) return whole;
161
+ const det = ctm[0] * ctm[3] - ctm[1] * ctm[2];
162
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-9) return whole;
163
+ let u0 = Infinity;
164
+ let v0 = Infinity;
165
+ let u1 = -Infinity;
166
+ let v1 = -Infinity;
167
+ const add = (x, y) => {
168
+ const dx = x - ctm[4];
169
+ const dy = y - ctm[5];
170
+ const u = (ctm[3] * dx - ctm[2] * dy) / det;
171
+ const v = (ctm[0] * dy - ctm[1] * dx) / det;
172
+ u0 = Math.min(u0, u);
173
+ v0 = Math.min(v0, v);
174
+ u1 = Math.max(u1, u);
175
+ v1 = Math.max(v1, v);
176
+ };
177
+ for (const seg of clip.segs) {
178
+ if (seg.op === "close") continue;
179
+ if (seg.op === "cubic") {
180
+ add(seg.x1, seg.y1);
181
+ add(seg.x2, seg.y2);
182
+ }
183
+ add(seg.x, seg.y);
184
+ }
185
+ if (!Number.isFinite(u0) || !Number.isFinite(v0)) return whole;
186
+ const cut = {
187
+ u0: Math.min(1, Math.max(0, u0)),
188
+ v0: Math.min(1, Math.max(0, v0)),
189
+ u1: Math.min(1, Math.max(0, u1)),
190
+ v1: Math.min(1, Math.max(0, v1))
191
+ };
192
+ if (!(cut.u1 - cut.u0 > CROPS) || !(cut.v1 - cut.v0 > CROPS)) return whole;
193
+ if (cut.u1 - cut.u0 > 1 - CROPS && cut.v1 - cut.v0 > 1 - CROPS) return whole;
194
+ return cut;
195
+ }
96
196
  function matrixOf(file, dict) {
97
197
  const m = file.resolve(dict.get("Matrix") ?? PDF_NULL);
98
198
  if (Array.isArray(m) && m.length >= 6 && m.every((v) => typeof v === "number")) return [
@@ -0,0 +1,109 @@
1
+ /** A bilevel raster, one byte per pixel, 1 = black — JBIG2's own sense. */
2
+ export interface Jbig2Bitmap {
3
+ readonly width: number;
4
+ readonly height: number;
5
+ readonly data: Uint8Array;
6
+ }
7
+ /** A context's state: the Table E.1 index and which symbol is currently more probable. */
8
+ interface Cx {
9
+ i: Uint8Array;
10
+ mps: Uint8Array;
11
+ }
12
+ /** §E.3.5 — a fresh set of `size` contexts, all at state 0 with MPS 0. */
13
+ export declare function newContexts(size: number): Cx;
14
+ /**
15
+ * §E.3 — the MQ arithmetic decoder.
16
+ *
17
+ * Written as a class with the spec's own register names (`c`, `a`, `ct`)
18
+ * because the procedures below are transcribed from its flow charts, and a
19
+ * renaming makes them impossible to check against it.
20
+ */
21
+ export declare class MQDecoder {
22
+ private readonly data;
23
+ private bp;
24
+ private readonly end;
25
+ private c;
26
+ private a;
27
+ private ct;
28
+ constructor(data: Uint8Array, start?: number, end?: number);
29
+ private byteAt;
30
+ /** BYTEIN (§E.3.4) — 0xFF is the marker escape, so it feeds a shorter byte. */
31
+ private byteIn;
32
+ /**
33
+ * DECODE (§E.3.2) — one bit in context `k`.
34
+ *
35
+ * @param cx The context set the caller keeps for this decoding procedure.
36
+ * @param k Which context within it.
37
+ */
38
+ decode(cx: Cx, k: number): number;
39
+ }
40
+ /** An adaptive template pixel — a coordinate the coder may move (§6.2.5.3). */
41
+ interface At {
42
+ readonly x: number;
43
+ readonly y: number;
44
+ }
45
+ /**
46
+ * §6.2.5.7 — decode a generic region with the arithmetic coder.
47
+ *
48
+ * The context for a pixel is the pattern of already-decoded neighbours the
49
+ * template names, read MSB first in the order the figures give. `TPGDON` adds a
50
+ * per-row bit that says "this row is the same as the one above", which is what
51
+ * makes a page of white space nearly free.
52
+ *
53
+ * @param mq The decoder, positioned at the region's data.
54
+ * @param cx The 2^16 generic contexts (shared across a segment's regions).
55
+ * @param width Region width in pixels.
56
+ * @param height Region height in pixels.
57
+ * @param template Which of the four templates (0-3).
58
+ * @param at The adaptive pixels, one for templates 1-3 and four for 0.
59
+ * @param tpgdon Whether typical prediction is on.
60
+ * @param skip A bitmap of pixels to leave white without decoding (halftone).
61
+ */
62
+ export declare function decodeGenericRegion(mq: MQDecoder, cx: Cx, width: number, height: number, template: number, at: ReadonlyArray<At>, tpgdon: boolean, skip?: Jbig2Bitmap): Jbig2Bitmap;
63
+ /**
64
+ * §6.3.5.6 — refine `reference` into a bitmap of `width` × `height`.
65
+ *
66
+ * Refinement codes a bitmap against one already decoded — the same glyph at a
67
+ * lower resolution, or the same region in an earlier pass — so only where the
68
+ * two differ costs anything.
69
+ *
70
+ * @param dx Where the reference sits relative to the region (GRREFERENCEDX).
71
+ * @param dy Likewise, vertically.
72
+ */
73
+ export declare function decodeRefinement(mq: MQDecoder, cx: Cx, width: number, height: number, template: number, reference: Jbig2Bitmap, dx: number, dy: number, at: ReadonlyArray<At>, tpgron: boolean): Jbig2Bitmap;
74
+ /**
75
+ * §6.4.5 — draw a text region: a run of strips, each holding instances of the
76
+ * symbols in `symbols`, placed by running coordinates rather than absolute ones.
77
+ *
78
+ * This is what JBIG2 is for. A scanned page is not stored as pixels but as "the
79
+ * shape called 37, here; the shape called 12, four pixels on" — so the letter
80
+ * "e" costs its bitmap once and a few bits per occurrence after that.
81
+ */
82
+ /**
83
+ * §6.4.5 Table 34 — SBSYMCODELEN, how many bits a text region spends naming
84
+ * one of its symbols.
85
+ *
86
+ * `ceil(log2(SBNUMSYMS))`, and for ONE symbol that is ZERO: there is nothing to
87
+ * choose, so no bits are read and the id is always that symbol.
88
+ * Rounded up to one — which the HUFFMAN side of the same table does need — the
89
+ * decoder takes a bit belonging to the next field and reads the id as 1, which
90
+ * is no symbol at all: bitmap-symbol-big-segmentid.pdf places one instance of
91
+ * one symbol in each of two regions, and both came back empty.
92
+ *
93
+ * @param symbols How many symbols the region has to choose between.
94
+ * @returns The number of bits an id takes.
95
+ */
96
+ export declare function symbolCodeLength(symbols: number): number;
97
+ /**
98
+ * Decode an embedded JBIG2 image.
99
+ *
100
+ * @param data The `/JBIG2Decode` stream's own segments.
101
+ * @param globals The `/JBIG2Globals` stream's segments, when the image names one.
102
+ * @param width The image's `/Width`, which the page info may not state.
103
+ * @param height Its `/Height`.
104
+ * @returns A packed 1-bit-per-pixel bitmap (`rowBytes × height`, bit 1 = black,
105
+ * MSB first) — the same shape {@link decodeCcitt} returns — or `undefined`
106
+ * when nothing in the stream could be decoded.
107
+ */
108
+ export declare function decodeJbig2(data: Uint8Array, globals: Uint8Array | undefined, width: number, height: number): Uint8Array | undefined;
109
+ export {};