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
@@ -0,0 +1,112 @@
1
+ //#region src/pdf-reader/cie-color.ts
2
+ /**
3
+ * One colour in a CIE-based space, as sRGB in 0..1.
4
+ *
5
+ * @param space The space's own parameters.
6
+ * @param abc Its components: one for a grey space, three for an RGB one.
7
+ * @returns The sRGB triple a viewer shows for it.
8
+ */
9
+ function cieToSrgb(space, abc) {
10
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
11
+ if (space.matrix === void 0 || abc.length < 3) {
12
+ const v = srgbTransfer(clamp01(abc[0] ?? 0) ** (space.gamma[0] ?? 1));
13
+ return [
14
+ v,
15
+ v,
16
+ v
17
+ ];
18
+ }
19
+ const m = space.matrix;
20
+ const a = clamp01(abc[0]) ** (space.gamma[0] ?? 1);
21
+ const b = clamp01(abc[1]) ** (space.gamma[1] ?? 1);
22
+ const c = clamp01(abc[2]) ** (space.gamma[2] ?? 1);
23
+ const [ax, ay, az] = adaptToD65((m[0] ?? 0) * a + (m[3] ?? 0) * b + (m[6] ?? 0) * c, (m[1] ?? 0) * a + (m[4] ?? 0) * b + (m[7] ?? 0) * c, (m[2] ?? 0) * a + (m[5] ?? 0) * b + (m[8] ?? 0) * c, space.white);
24
+ const r = 3.2404542 * ax - 1.5371385 * ay - .4985314 * az;
25
+ const g = -.969266 * ax + 1.8760108 * ay + .041556 * az;
26
+ const bl = .0556434 * ax - .2040259 * ay + 1.0572252 * az;
27
+ return [
28
+ srgbTransfer(clamp01(r)),
29
+ srgbTransfer(clamp01(g)),
30
+ srgbTransfer(clamp01(bl))
31
+ ];
32
+ }
33
+ /**
34
+ * §8.6.5.8 — one colour in a `Lab` space, as sRGB in 0..1.
35
+ *
36
+ * Lab is the one CIE space a file states in absolute terms: `L*` from 0 to 100
37
+ * is lightness and `a*`/`b*` are the two opponent axes, and unlike `CalRGB`
38
+ * (see the note above) there is no argument about the way out — every renderer
39
+ * takes it through XYZ against the stated white and adapts to the screen's.
40
+ * issue10339_reduced.pdf paints two grids of blue swatches through an `Indexed`
41
+ * palette whose base is one, and read as anything else the page came back
42
+ * blank.
43
+ *
44
+ * @param white The space's `/WhitePoint`, in XYZ.
45
+ * @param lab `L*`, `a*`, `b*`.
46
+ * @returns The sRGB triple a viewer shows for it.
47
+ */
48
+ function labToSrgb(white, lab) {
49
+ const m = (Math.min(100, Math.max(0, lab[0])) + 16) / 116;
50
+ const l = m + lab[1] / 500;
51
+ const n = m - lab[2] / 200;
52
+ const [ax, ay, az] = adaptToD65(white[0] * inverseTransfer(l), white[1] * inverseTransfer(m), white[2] * inverseTransfer(n), white);
53
+ const r = 3.2404542 * ax - 1.5371385 * ay - .4985314 * az;
54
+ const g = -.969266 * ax + 1.8760108 * ay + .041556 * az;
55
+ const b = .0556434 * ax - .2040259 * ay + 1.0572252 * az;
56
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
57
+ return [
58
+ srgbTransfer(clamp01(r)),
59
+ srgbTransfer(clamp01(g)),
60
+ srgbTransfer(clamp01(b))
61
+ ];
62
+ }
63
+ /** §8.6.5.8 — the `g` of the Lab definition, which is a cube with a linear toe. */
64
+ function inverseTransfer(v) {
65
+ return v >= 6 / 29 ? v ** 3 : 108 / 841 * (v - 4 / 29);
66
+ }
67
+ /** The D65 white every sRGB screen is referred to. */
68
+ var D65 = [
69
+ .9505,
70
+ 1,
71
+ 1.089
72
+ ];
73
+ /**
74
+ * Bradford chromatic adaptation from the space's own white to D65.
75
+ *
76
+ * A file may state any illuminant — calrgb.pdf states several — and a colour
77
+ * measured against one white shown against another is the wrong colour. The
78
+ * Bradford transform is the one every colour-managed renderer uses for this.
79
+ */
80
+ function adaptToD65(x, y, z, white) {
81
+ const [sx, sy, sz] = coneResponse(white[0], white[1], white[2]);
82
+ const [dx, dy, dz] = coneResponse(D65[0], D65[1], D65[2]);
83
+ if (sx === 0 || sy === 0 || sz === 0) return [
84
+ x,
85
+ y,
86
+ z
87
+ ];
88
+ const [cx, cy, cz] = coneResponse(x, y, z);
89
+ return fromConeResponse(cx * dx / sx, cy * dy / sy, cz * dz / sz);
90
+ }
91
+ /** XYZ into the Bradford cone-response space. */
92
+ function coneResponse(x, y, z) {
93
+ return [
94
+ .8951 * x + .2664 * y - .1614 * z,
95
+ -.7502 * x + 1.7135 * y + .0367 * z,
96
+ .0389 * x - .0685 * y + 1.0296 * z
97
+ ];
98
+ }
99
+ /** …and back out of it. */
100
+ function fromConeResponse(l, m, s) {
101
+ return [
102
+ .9869929 * l - .1470543 * m + .1599627 * s,
103
+ .4323053 * l + .5183603 * m + .0492912 * s,
104
+ -.0085287 * l + .0400428 * m + .9684867 * s
105
+ ];
106
+ }
107
+ /** IEC 61966-2-1 — linear light to the sRGB signal a screen takes. */
108
+ function srgbTransfer(v) {
109
+ return v <= .0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - .055;
110
+ }
111
+ //#endregion
112
+ export { cieToSrgb, labToSrgb };
@@ -1,3 +1,5 @@
1
+ import { ColorSpaceInfo, GsPaint } from './shading.js';
2
+ import { TextMarkup } from './annot-draw.js';
1
3
  import { ShapeGradient } from '../core/vector.js';
2
4
  import { PdfDict, PdfStream } from '../pdf/objects.js';
3
5
  /**
@@ -71,8 +73,21 @@ export interface TextRun {
71
73
  * than in a substitute (see `./embedded-fonts`).
72
74
  */
73
75
  readonly fontName?: string;
76
+ /**
77
+ * §9.4.4 — the advance of this face's SPACE at the size the run was shown
78
+ * at, which is what says whether a gap between two runs was a word space.
79
+ * Absent where the face states no width for it.
80
+ */
81
+ readonly spaceWidthPt?: number;
74
82
  /** §9.8.1 — the face the glyphs were shown in is a bold one. */
75
83
  readonly bold?: boolean;
84
+ /**
85
+ * §12.5.6.10 — a text-markup annotation marks these glyphs: highlighted,
86
+ * underlined, struck through. Applied after extraction, from the page's
87
+ * `/Annots` (see `./text`), because it is stated about the words rather than
88
+ * painted among them.
89
+ */
90
+ readonly markup?: TextMarkup;
76
91
  /**
77
92
  * §8.6.6.2 — the glyphs are filled with a tiling PATTERN, named here for the
78
93
  * caller to resolve: a pattern is a content stream, not a colour, and the
@@ -119,13 +134,33 @@ export interface TextRun {
119
134
  * square to page space, so it carries both the placement and the size; the
120
135
  * `mcid` links the paint to its structure element (a `/Figure`, E-PDF EP6).
121
136
  */
137
+ /** §8.9.7 — an image written into the content stream rather than named. */
138
+ export interface InlineImage {
139
+ /** Its dictionary, keys as written — the abbreviated ones (`/W`, `/CS`, `/F`). */
140
+ readonly dict: PdfDict;
141
+ /** Its bytes, still filtered as the dictionary says. */
142
+ readonly data: Uint8Array;
143
+ }
122
144
  export interface ImagePlacement {
123
145
  /** Where the `Do` fell in the stream's painting order — see {@link VectorPlacement.order}. */
124
146
  readonly order: number;
125
- /** XObject resource name (no leading slash). */
147
+ /** XObject resource name (no leading slash), empty for an inline image. */
126
148
  readonly name: string;
149
+ /** §8.9.7 — the image itself, where it was written into the stream. */
150
+ readonly inline?: InlineImage;
127
151
  readonly ctm: Matrix;
152
+ /** §8.5.4 — the clip in force when it was painted, when there was one. */
153
+ readonly clip?: ClipRegion;
154
+ /**
155
+ * §8.9.6.2 — the non-stroking colour in force. A stencil `/ImageMask` carries
156
+ * no colour of its own: it says only WHERE to paint, and this is what.
157
+ */
158
+ readonly fillHex: string;
128
159
  readonly mcid?: number;
160
+ /** §11.3.5 `/BM` — a blend the page asked for that nothing here performs. */
161
+ readonly blend?: string;
162
+ /** §11.6.5 `/SMask` — the paint faded from place to place; nothing here does. */
163
+ readonly masked?: boolean;
129
164
  }
130
165
  /**
131
166
  * One segment of a painted path (E-PDF EP10/EP11), in page space (y-up): a
@@ -177,6 +212,16 @@ export interface VectorPlacement {
177
212
  readonly gradient?: ShapeGradient;
178
213
  /** §11.6.4.4 `/ca` — how opaque the fill is, when the page asked for less. */
179
214
  readonly alpha?: number;
215
+ /**
216
+ * §11.3.5 `/BM` — the fill only DARKENS what it covers (`Multiply`,
217
+ * `Darken`), so the marks under it read through. A highlighter is this and
218
+ * nothing else.
219
+ */
220
+ readonly darkens?: boolean;
221
+ /** §11.3.5 `/BM` — a blend the page asked for that nothing here performs. */
222
+ readonly blend?: string;
223
+ /** §11.6.5 `/SMask` — the paint faded from place to place; nothing here does. */
224
+ readonly masked?: boolean;
180
225
  /**
181
226
  * §8.7.3 — the TILING pattern resource name the path is filled with. Its
182
227
  * content is a stream of its own, so what the fill actually shows is only
@@ -221,22 +266,7 @@ export type Matrix = readonly [number, number, number, number, number, number];
221
266
  export declare const IDENTITY: Matrix;
222
267
  /** Compose two {@link Matrix matrices}: `a` applied first, then `b`. */
223
268
  export declare function multiply(a: Matrix, b: Matrix): Matrix;
224
- /**
225
- * Walk a page's decoded content stream (§9.4) and extract its positioned text,
226
- * painted XObjects and painted paths. Tracks the graphics state (CTM via
227
- * `q`/`Q`/`cm`) and text state (text/line matrices, font, size, spacing), and
228
- * emits one {@link TextRun} per show operator (`Tj` / `TJ` / `'` / `"`) in page
229
- * space (points).
230
- *
231
- * @param bytes The page's decoded content-stream bytes.
232
- * @param fonts Page fonts by resource key (`Tf` name), for decoding + widths;
233
- * an unmapped key falls back to Latin-1 with a half-em advance.
234
- * @param initialCtm The starting CTM mapping user space to page space.
235
- * @param shadings Shading patterns by name, selected by `scn`/`sc` (EP16c).
236
- * @param alphas Constant fill alphas by `/ExtGState` name, selected by `gs`.
237
- * @returns The extracted text runs, image placements and vector paths.
238
- */
239
- export declare function interpretContent(bytes: Uint8Array, fonts: ReadonlyMap<string, ContentFont>, initialCtm?: Matrix, shadings?: ReadonlyMap<string, ShapeGradient>, alphas?: ReadonlyMap<string, number>): InterpretResult;
269
+ export declare function interpretContent(bytes: Uint8Array, fonts: ReadonlyMap<string, ContentFont>, initialCtm?: Matrix, shadings?: ReadonlyMap<string, ShapeGradient>, alphas?: ReadonlyMap<string, GsPaint>, spaces?: ReadonlyMap<string, ColorSpaceInfo>, hiddenOc?: ReadonlySet<string>): InterpretResult;
240
270
  /**
241
271
  * Whether a string is wholly right-to-left: at least one letter of an RTL
242
272
  * script and nothing of any other, spaces and joiners aside.
@@ -1,5 +1,6 @@
1
1
  import { PDF_NULL, PdfHexString, PdfName } from "../pdf/objects.js";
2
2
  import { Lexer } from "./lexer.js";
3
+ import { cieToSrgb } from "./cie-color.js";
3
4
  //#region src/pdf-reader/content.ts
4
5
  /**
5
6
  * A painted path emitted by the path-painting operators (§8.5.3), captured in
@@ -73,6 +74,8 @@ var FALLBACK_FONT = {
73
74
  var UPRIGHT_TOLERANCE_DEG = .5;
74
75
  function initialState() {
75
76
  return {
77
+ fillSpace: void 0,
78
+ strokeSpace: void 0,
76
79
  ctm: IDENTITY,
77
80
  fontKey: "",
78
81
  font: FALLBACK_FONT,
@@ -89,6 +92,9 @@ function initialState() {
89
92
  fillGradient: void 0,
90
93
  fillPattern: void 0,
91
94
  fillAlpha: 1,
95
+ fillDarkens: false,
96
+ blendMode: void 0,
97
+ softMask: false,
92
98
  clip: void 0
93
99
  };
94
100
  }
@@ -107,7 +113,58 @@ function initialState() {
107
113
  * @param alphas Constant fill alphas by `/ExtGState` name, selected by `gs`.
108
114
  * @returns The extracted text runs, image placements and vector paths.
109
115
  */
110
- function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__PURE__ */ new Map(), alphas = /* @__PURE__ */ new Map()) {
116
+ /** §8.6.8 — the space a `cs` / `CS` operand names, if it is one we read. */
117
+ function spaceOf(operands, spaces) {
118
+ const name = operands[operands.length - 1];
119
+ if (!(name instanceof PdfName)) return void 0;
120
+ const direct = spaces.get(name.value);
121
+ if (direct) return direct;
122
+ switch (name.value) {
123
+ case "DeviceGray": return {
124
+ kind: "gray",
125
+ components: 1
126
+ };
127
+ case "DeviceRGB": return {
128
+ kind: "rgb",
129
+ components: 3
130
+ };
131
+ case "DeviceCMYK": return {
132
+ kind: "cmyk",
133
+ components: 4
134
+ };
135
+ default: return;
136
+ }
137
+ }
138
+ /**
139
+ * §8.6.8 — the colour a run of `sc` / `scn` components comes to.
140
+ *
141
+ * The space in force decides, and where it was not read the COUNT is the next
142
+ * best witness: three numbers are RGB and four are CMYK on every device space
143
+ * there is. One number is the ambiguous case — grey in a device space, but the
144
+ * strength of a colorant in a Separation, where 1 is the ink at full and reads
145
+ * dark — so a lone component is only taken where the space said what it means.
146
+ */
147
+ function componentColor(operands, space) {
148
+ const nums = operands.filter((o) => typeof o === "number");
149
+ if (nums.length === 0) return void 0;
150
+ if (space?.cie) {
151
+ const [r, g, b] = cieToSrgb(space.cie, nums);
152
+ return rgbHex(r, g, b);
153
+ }
154
+ switch (space?.kind ?? (nums.length === 3 ? "rgb" : nums.length === 4 ? "cmyk" : void 0)) {
155
+ case "rgb": return nums.length >= 3 ? rgbHex(nums[0], nums[1], nums[2]) : void 0;
156
+ case "cmyk": return nums.length >= 4 ? cmykHex(nums[0], nums[1], nums[2], nums[3]) : void 0;
157
+ case "gray": return grayHex(nums[0]);
158
+ case "tint":
159
+ if (space?.tint) {
160
+ const hex = componentColor(space.tint.transform(nums), space.tint.alternate);
161
+ if (hex !== void 0) return hex;
162
+ }
163
+ return grayHex(1 - Math.max(...nums));
164
+ default: return;
165
+ }
166
+ }
167
+ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__PURE__ */ new Map(), alphas = /* @__PURE__ */ new Map(), spaces = /* @__PURE__ */ new Map(), hiddenOc = /* @__PURE__ */ new Set()) {
111
168
  const runs = [];
112
169
  const images = [];
113
170
  const vectors = [];
@@ -123,6 +180,9 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
123
180
  let paintOrder = 0;
124
181
  let operands = [];
125
182
  const mcStack = [];
183
+ const ocStack = [];
184
+ let hiddenDepth = 0;
185
+ const visible = () => hiddenDepth === 0;
126
186
  const toPage = (x, y) => [x * state.ctm[0] + y * state.ctm[2] + state.ctm[4], x * state.ctm[1] + y * state.ctm[3] + state.ctm[5]];
127
187
  const moveTo = (x, y) => {
128
188
  const [px, py] = toPage(x, y);
@@ -167,7 +227,7 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
167
227
  return state.lineWidth * scale;
168
228
  };
169
229
  const paintPath = (fill, stroke) => {
170
- if (path.length >= 2 && (fill || stroke)) {
230
+ if (path.length >= 2 && (fill || stroke) && visible()) {
171
231
  const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
172
232
  vectors.push({
173
233
  order: paintOrder++,
@@ -176,6 +236,9 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
176
236
  ...fill && state.fillPattern !== void 0 ? { patternName: state.fillPattern } : {},
177
237
  ...fill ? { fillHex: state.fillColor } : {},
178
238
  ...fill && state.fillAlpha < 1 ? { alpha: state.fillAlpha } : {},
239
+ ...fill && state.fillDarkens ? { darkens: true } : {},
240
+ ...state.blendMode !== void 0 ? { blend: state.blendMode } : {},
241
+ ...state.softMask ? { masked: true } : {},
179
242
  ...fill && state.fillGradient ? { gradient: state.fillGradient } : {},
180
243
  ...stroke ? {
181
244
  strokeHex: state.strokeColor,
@@ -205,7 +268,7 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
205
268
  const type3 = state.font.type3;
206
269
  if (type3) {
207
270
  const stream = type3.proc(code);
208
- if (stream) {
271
+ if (stream && visible()) {
209
272
  const scale = [
210
273
  state.fontSize * state.hScale,
211
274
  0,
@@ -235,8 +298,10 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
235
298
  const text = logicalOrder(shown);
236
299
  if (text.length === 0) return;
237
300
  const scaleY = Math.hypot(origin[2], origin[3]) || 1;
301
+ const scaleX = (Math.hypot(origin[0], origin[1]) || 1) * state.hScale;
238
302
  const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
239
303
  const angle = Math.atan2(origin[1], origin[0]) * 180 / Math.PI;
304
+ if (!visible()) return;
240
305
  runs.push({
241
306
  text,
242
307
  x: origin[4],
@@ -244,7 +309,8 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
244
309
  endX: end[4],
245
310
  endY: end[5],
246
311
  ...Math.abs(angle) > UPRIGHT_TOLERANCE_DEG ? { angleDeg: angle } : {},
247
- fontSizePt: state.fontSize * scaleY,
312
+ fontSizePt: Math.abs(state.fontSize) * scaleY,
313
+ spaceWidthPt: (state.font.width(32) / 1e3 * state.fontSize + state.charSpacing + state.wordSpacing) * scaleX,
248
314
  fontKey: state.fontKey,
249
315
  ...state.font.name !== void 0 ? { fontName: state.font.name } : {},
250
316
  ...state.font.type3 ? { type3: true } : {},
@@ -348,23 +414,31 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
348
414
  const props = operands[operands.length - 1];
349
415
  const mcidVal = !(tag instanceof PdfName && tag.value === "Artifact") && props instanceof Map ? props.get("MCID") : void 0;
350
416
  mcStack.push(typeof mcidVal === "number" ? mcidVal : void 0);
417
+ const isOc = tag instanceof PdfName && tag.value === "OC" && props instanceof PdfName && hiddenOc.has(props.value);
418
+ ocStack.push(isOc);
419
+ if (isOc) hiddenDepth++;
351
420
  break;
352
421
  }
353
422
  case "BMC":
354
423
  mcStack.push(void 0);
424
+ ocStack.push(false);
355
425
  break;
356
426
  case "EMC":
357
427
  mcStack.pop();
428
+ if (ocStack.pop() === true) hiddenDepth--;
358
429
  break;
359
430
  case "Do": {
360
431
  const nm = operands[0];
361
- if (nm instanceof PdfName) {
432
+ if (nm instanceof PdfName && visible()) {
362
433
  const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
363
434
  images.push({
364
435
  order: paintOrder++,
365
436
  name: nm.value,
366
437
  ctm: state.ctm,
367
- ...mcid !== void 0 ? { mcid } : {}
438
+ ...state.clip ? { clip: state.clip } : {},
439
+ fillHex: state.fillColor,
440
+ ...mcid !== void 0 ? { mcid } : {},
441
+ ...state.blendMode !== void 0 ? { blend: state.blendMode } : {}
368
442
  });
369
443
  }
370
444
  break;
@@ -384,12 +458,34 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
384
458
  state.fillGradient = void 0;
385
459
  state.fillPattern = void 0;
386
460
  break;
461
+ case "cs":
462
+ state.fillSpace = spaceOf(operands, spaces);
463
+ break;
464
+ case "CS":
465
+ state.strokeSpace = spaceOf(operands, spaces);
466
+ break;
387
467
  case "scn":
388
468
  case "sc": {
389
469
  const last = operands[operands.length - 1];
390
470
  const named = last instanceof PdfName ? last.value : void 0;
391
- state.fillGradient = named !== void 0 ? shadings.get(named) : void 0;
392
- state.fillPattern = named !== void 0 && !state.fillGradient ? named : void 0;
471
+ if (named !== void 0) {
472
+ state.fillGradient = shadings.get(named);
473
+ state.fillPattern = state.fillGradient ? void 0 : named;
474
+ break;
475
+ }
476
+ const hex = componentColor(operands, state.fillSpace);
477
+ if (hex !== void 0) {
478
+ state.fillColor = hex;
479
+ state.fillGradient = void 0;
480
+ state.fillPattern = void 0;
481
+ }
482
+ break;
483
+ }
484
+ case "SCN":
485
+ case "SC": {
486
+ if (operands[operands.length - 1] instanceof PdfName) break;
487
+ const hex = componentColor(operands, state.strokeSpace);
488
+ if (hex !== void 0) state.strokeColor = hex;
393
489
  break;
394
490
  }
395
491
  case "RG":
@@ -445,7 +541,13 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
445
541
  break;
446
542
  case "gs": {
447
543
  const nm = operands[operands.length - 1];
448
- if (nm instanceof PdfName) state.fillAlpha = alphas.get(nm.value) ?? 1;
544
+ if (nm instanceof PdfName) {
545
+ const paint = alphas.get(nm.value);
546
+ state.fillAlpha = paint?.alpha ?? 1;
547
+ state.fillDarkens = paint?.darkens ?? false;
548
+ state.blendMode = paint?.blend;
549
+ state.softMask = paint?.masked ?? false;
550
+ }
449
551
  break;
450
552
  }
451
553
  case "W":
@@ -479,8 +581,19 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
479
581
  operands.push(readDict(lexer));
480
582
  break;
481
583
  case "keyword":
482
- if (tok.value === "BI") skipInlineImage(lexer);
483
- else exec(tok.value);
584
+ if (tok.value === "BI") {
585
+ const inline = readInlineImage(lexer);
586
+ if (inline && visible()) images.push({
587
+ order: paintOrder++,
588
+ name: "",
589
+ inline,
590
+ ctm: state.ctm,
591
+ ...state.clip ? { clip: state.clip } : {},
592
+ fillHex: state.fillColor,
593
+ ...mcStack.length > 0 ? { mcid: mcStack[mcStack.length - 1] } : {},
594
+ ...state.blendMode !== void 0 ? { blend: state.blendMode } : {}
595
+ });
596
+ } else exec(tok.value);
484
597
  operands = [];
485
598
  break;
486
599
  default:
@@ -621,14 +734,62 @@ function readValue(lexer) {
621
734
  default: return PDF_NULL;
622
735
  }
623
736
  }
624
- function skipInlineImage(lexer) {
737
+ /**
738
+ * §8.9.7 — an inline image: its dictionary between `BI` and `ID`, then its
739
+ * bytes up to `EI`.
740
+ *
741
+ * The bytes are binary and may hold `EI` themselves, so the end is found by
742
+ * MEASURING where the dictionary says how much there is — an unfiltered image
743
+ * is exactly `ceil(W · BPC · components / 8) · H` bytes — and only searched for
744
+ * where a filter makes the length unknowable. images_1bit_grayscale.pdf draws
745
+ * two of them and both were skipped over.
746
+ */
747
+ function readInlineImage(lexer) {
748
+ const dict = /* @__PURE__ */ new Map();
625
749
  for (;;) {
626
750
  const tok = lexer.nextToken();
627
- if (tok.kind === "eof") return;
751
+ if (tok.kind === "eof") return void 0;
628
752
  if (tok.kind === "keyword" && tok.value === "ID") break;
753
+ if (tok.kind === "name") dict.set(tok.value, readValue(lexer));
629
754
  }
630
- const ei = lexer.indexOfAscii("EI", lexer.pos);
755
+ const start = lexer.pos + 1;
756
+ const measured = inlineLength(dict);
757
+ let end = measured !== void 0 ? start + measured : -1;
758
+ if (end < 0 || end > lexer.length) {
759
+ end = lexer.indexOfAscii("EI", start);
760
+ if (end < 0) {
761
+ lexer.pos = lexer.length;
762
+ return;
763
+ }
764
+ }
765
+ const data = lexer.slice(start, Math.min(end, lexer.length));
766
+ const ei = lexer.indexOfAscii("EI", end);
631
767
  lexer.pos = ei < 0 ? lexer.length : ei + 2;
768
+ return {
769
+ dict,
770
+ data
771
+ };
772
+ }
773
+ /** How many bytes an UNFILTERED inline image's samples take, if that is known. */
774
+ function inlineLength(dict) {
775
+ if (dict.has("F") || dict.has("Filter")) return void 0;
776
+ const num = (...keys) => {
777
+ for (const k of keys) {
778
+ const v = dict.get(k);
779
+ if (typeof v === "number") return v;
780
+ }
781
+ return 0;
782
+ };
783
+ const w = num("W", "Width");
784
+ const h = num("H", "Height");
785
+ if (w <= 0 || h <= 0) return void 0;
786
+ const mask = dict.get("IM") === true || dict.get("ImageMask") === true;
787
+ const bpc = mask ? 1 : num("BPC", "BitsPerComponent") || 8;
788
+ const cs = dict.get("CS") ?? dict.get("ColorSpace");
789
+ const name = cs instanceof PdfName ? cs.value : "";
790
+ const comps = mask ? 1 : /^(RGB|DeviceRGB|CalRGB)$/u.test(name) ? 3 : /^(CMYK|DeviceCMYK)$/u.test(name) ? 4 : /^(G|DeviceGray|CalGray|I|Indexed)$/u.test(name) || name === "" ? 1 : 0;
791
+ if (comps === 0) return void 0;
792
+ return ((w * comps * bpc + 7) / 8 | 0) * h;
632
793
  }
633
794
  //#endregion
634
795
  export { IDENTITY, interpretContent, isRightToLeft, multiply };
@@ -17,7 +17,7 @@ export interface Display {
17
17
  };
18
18
  }
19
19
  /**
20
- * The {@link Display} a page's `/MediaBox` and `/Rotate` describe.
20
+ * The {@link Display} a page's shown box and `/Rotate` describe.
21
21
  *
22
22
  * `/Rotate` turns the page CLOCKWISE when shown, so the content turns with it —
23
23
  * counter-clockwise by the same amount as seen from the content's own frame,
@@ -1,6 +1,6 @@
1
1
  //#region src/pdf-reader/display.ts
2
2
  /**
3
- * The {@link Display} a page's `/MediaBox` and `/Rotate` describe.
3
+ * The {@link Display} a page's shown box and `/Rotate` describe.
4
4
  *
5
5
  * `/Rotate` turns the page CLOCKWISE when shown, so the content turns with it —
6
6
  * counter-clockwise by the same amount as seen from the content's own frame,
@@ -10,7 +10,7 @@
10
10
  * @returns Its size, its turn, and the map onto it.
11
11
  */
12
12
  function displayOf(page) {
13
- const [x0, y0, x1, y1] = page.mediaBox;
13
+ const [x0, y0, x1, y1] = page.cropBox;
14
14
  const left = Math.min(x0, x1);
15
15
  const bottom = Math.min(y0, y1);
16
16
  const w = Math.abs(x1 - x0);
@@ -66,7 +66,20 @@ function placeRuns(runs, d) {
66
66
  endY: end.y,
67
67
  ...Math.abs(angle) > .5 ? { angleDeg: angle } : {}
68
68
  };
69
- });
69
+ }).filter((r) => shows(d, Math.min(r.x, r.endX), Math.min(r.y, r.endY) - r.fontSizePt * .25, Math.max(r.x, r.endX), Math.max(r.y, r.endY) + r.fontSizePt * .85));
70
+ }
71
+ /**
72
+ * §14.11.2 — whether a box falls inside the region the page SHOWS.
73
+ *
74
+ * The crop box is the region "to which the contents of the page shall be
75
+ * clipped", and a page cropped to less than its sheet is generally a page with
76
+ * something outside the crop: freeculture.pdf carries a printer's slug —
77
+ * "14773_07_347-348_r4jm.qxd 2/10/04 4:45 PM Page 347" — in the 42 points the
78
+ * crop cuts off the top, and lifted with the rest it stood at the head of the
79
+ * page and pushed the sheet taller to hold it.
80
+ */
81
+ function shows(d, minX, minY, maxX, maxY) {
82
+ return maxX > 0 && minX < d.width && maxY > 0 && minY < d.height;
70
83
  }
71
84
  /**
72
85
  * The same pictures, placed on the shown page. A quarter turn swaps a picture's
@@ -89,14 +102,16 @@ function placeImages(images, d) {
89
102
  const ys = corners.map((c) => c.y);
90
103
  const minX = Math.min(...xs);
91
104
  const minY = Math.min(...ys);
105
+ const turned = (img.rotationDeg ?? 0) + d.turnDeg;
92
106
  return {
93
107
  ...img,
94
108
  x: minX,
95
109
  y: minY,
96
110
  widthPt: Math.max(...xs) - minX,
97
- heightPt: Math.max(...ys) - minY
111
+ heightPt: Math.max(...ys) - minY,
112
+ ...Math.abs(turned) > .5 ? { rotationDeg: turned } : {}
98
113
  };
99
- });
114
+ }).filter((img) => shows(d, img.x, img.y, img.x + img.widthPt, img.y + img.heightPt));
100
115
  }
101
116
  /**
102
117
  * The same paths, placed on the shown page. A path is geometry, so every point
@@ -156,7 +171,7 @@ function placeVectors(vectors, d) {
156
171
  maxX: Math.max(...xs),
157
172
  maxY: Math.max(...ys)
158
173
  };
159
- });
174
+ }).filter((v) => shows(d, v.minX, v.minY, v.maxX, v.maxY));
160
175
  }
161
176
  //#endregion
162
177
  export { displayOf, placeImages, placeRuns, placeVectors };
@@ -5,6 +5,12 @@ export type Rectangle = readonly [number, number, number, number];
5
5
  export interface PdfPage {
6
6
  readonly dict: PdfDict;
7
7
  readonly mediaBox: Rectangle;
8
+ /**
9
+ * §14.11.2 `/CropBox` — the region of the page a viewer SHOWS, which is what
10
+ * a page's size means to anyone looking at it. Defaults to the media box and
11
+ * is clipped to it; inherited down the page tree like the media box is.
12
+ */
13
+ readonly cropBox: Rectangle;
8
14
  /**
9
15
  * §14.11.1 `/Rotate` — how far the page turns CLOCKWISE when it is shown,
10
16
  * normalised to 0, 90, 180 or 270 and inherited down the page tree.
@@ -162,6 +162,7 @@ var PdfFile = class PdfFile {
162
162
  if (out.length >= MAX_PAGES || seen.has(node)) return;
163
163
  seen.add(node);
164
164
  const mediaBox = readRectangle(this.get(node, "MediaBox")) ?? inherited.mediaBox;
165
+ const cropBox = readRectangle(this.get(node, "CropBox")) ?? inherited.cropBox;
165
166
  const resourcesVal = this.get(node, "Resources");
166
167
  const resources = resourcesVal instanceof Map ? resourcesVal : inherited.resources;
167
168
  const rotate = readRotate(this.get(node, "Rotate")) ?? inherited.rotate;
@@ -172,6 +173,7 @@ var PdfFile = class PdfFile {
172
173
  const kidNode = this.resolve(kid);
173
174
  if (kidNode instanceof Map) this.walkPageTree(kidNode, {
174
175
  mediaBox,
176
+ cropBox,
175
177
  resources,
176
178
  rotate
177
179
  }, out, seen);
@@ -179,9 +181,11 @@ var PdfFile = class PdfFile {
179
181
  }
180
182
  return;
181
183
  }
184
+ const media = mediaBox ?? DEFAULT_MEDIA_BOX;
182
185
  out.push({
183
186
  dict: node,
184
- mediaBox: mediaBox ?? DEFAULT_MEDIA_BOX,
187
+ mediaBox: media,
188
+ cropBox: shownBox(media, cropBox),
185
189
  rotate: rotate ?? 0,
186
190
  resources
187
191
  });
@@ -526,5 +530,29 @@ function lastIndexOfAscii(buf, needle) {
526
530
  }
527
531
  return -1;
528
532
  }
533
+ /**
534
+ * §14.11.2 — the box a viewer SHOWS: the crop box where there is one, clipped
535
+ * to the media box, and the media box where there is not.
536
+ *
537
+ * A crop box that states nothing usable — zero-sized, or outside the sheet
538
+ * altogether — is a crop box no viewer honours, and the media box stands.
539
+ * bug1844576.pdf is 612×792 of paper cropped to 181×53.75, and read as its
540
+ * media box the form on it arrived a fifth of the way down a letter page.
541
+ */
542
+ function shownBox(media, crop) {
543
+ if (!crop) return media;
544
+ const [mx0, my0, mx1, my1] = media;
545
+ const [cx0, cy0, cx1, cy1] = crop;
546
+ const x0 = Math.max(Math.min(mx0, mx1), Math.min(cx0, cx1));
547
+ const y0 = Math.max(Math.min(my0, my1), Math.min(cy0, cy1));
548
+ const x1 = Math.min(Math.max(mx0, mx1), Math.max(cx0, cx1));
549
+ const y1 = Math.min(Math.max(my0, my1), Math.max(cy0, cy1));
550
+ return x1 - x0 > 1 && y1 - y0 > 1 ? [
551
+ x0,
552
+ y0,
553
+ x1,
554
+ y1
555
+ ] : media;
556
+ }
529
557
  //#endregion
530
558
  export { PdfFile };