reamkit 1.26.0 → 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 (50) hide show
  1. package/README.md +9 -4
  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 -3
  11. package/dist/esm/pdf-reader/content.js +108 -13
  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 +77 -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/image-decode.d.ts +8 -4
  24. package/dist/esm/pdf-reader/image-decode.js +192 -11
  25. package/dist/esm/pdf-reader/images.d.ts +12 -0
  26. package/dist/esm/pdf-reader/images.js +109 -9
  27. package/dist/esm/pdf-reader/jbig2.d.ts +23 -0
  28. package/dist/esm/pdf-reader/jbig2.js +23 -14
  29. package/dist/esm/pdf-reader/layout.d.ts +32 -0
  30. package/dist/esm/pdf-reader/layout.js +189 -40
  31. package/dist/esm/pdf-reader/lexer.d.ts +2 -0
  32. package/dist/esm/pdf-reader/lexer.js +4 -0
  33. package/dist/esm/pdf-reader/optional-content.d.ts +36 -0
  34. package/dist/esm/pdf-reader/optional-content.js +93 -0
  35. package/dist/esm/pdf-reader/reader.d.ts +5 -2
  36. package/dist/esm/pdf-reader/reader.js +80 -7
  37. package/dist/esm/pdf-reader/shading.d.ts +60 -8
  38. package/dist/esm/pdf-reader/shading.js +124 -20
  39. package/dist/esm/pdf-reader/standard-metrics.d.ts +8 -0
  40. package/dist/esm/pdf-reader/standard-metrics.js +18 -0
  41. package/dist/esm/pdf-reader/standard-widths.d.ts +20 -0
  42. package/dist/esm/pdf-reader/standard-widths.js +62 -0
  43. package/dist/esm/pdf-reader/tagged.js +204 -32
  44. package/dist/esm/pdf-reader/text-rules.d.ts +16 -0
  45. package/dist/esm/pdf-reader/text-rules.js +112 -0
  46. package/dist/esm/pdf-reader/text.js +78 -4
  47. package/dist/esm/pdf-reader/vector.d.ts +5 -0
  48. package/dist/esm/pdf-reader/vector.js +39 -26
  49. package/dist/esm/word/docx-writer.js +73 -11
  50. 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,4 +1,5 @@
1
- import { ColorSpaceInfo } from './shading.js';
1
+ import { ColorSpaceInfo, GsPaint } from './shading.js';
2
+ import { TextMarkup } from './annot-draw.js';
2
3
  import { ShapeGradient } from '../core/vector.js';
3
4
  import { PdfDict, PdfStream } from '../pdf/objects.js';
4
5
  /**
@@ -72,8 +73,21 @@ export interface TextRun {
72
73
  * than in a substitute (see `./embedded-fonts`).
73
74
  */
74
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;
75
82
  /** §9.8.1 — the face the glyphs were shown in is a bold one. */
76
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;
77
91
  /**
78
92
  * §8.6.6.2 — the glyphs are filled with a tiling PATTERN, named here for the
79
93
  * caller to resolve: a pattern is a content stream, not a colour, and the
@@ -120,13 +134,33 @@ export interface TextRun {
120
134
  * square to page space, so it carries both the placement and the size; the
121
135
  * `mcid` links the paint to its structure element (a `/Figure`, E-PDF EP6).
122
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
+ }
123
144
  export interface ImagePlacement {
124
145
  /** Where the `Do` fell in the stream's painting order — see {@link VectorPlacement.order}. */
125
146
  readonly order: number;
126
- /** XObject resource name (no leading slash). */
147
+ /** XObject resource name (no leading slash), empty for an inline image. */
127
148
  readonly name: string;
149
+ /** §8.9.7 — the image itself, where it was written into the stream. */
150
+ readonly inline?: InlineImage;
128
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;
129
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;
130
164
  }
131
165
  /**
132
166
  * One segment of a painted path (E-PDF EP10/EP11), in page space (y-up): a
@@ -178,6 +212,16 @@ export interface VectorPlacement {
178
212
  readonly gradient?: ShapeGradient;
179
213
  /** §11.6.4.4 `/ca` — how opaque the fill is, when the page asked for less. */
180
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;
181
225
  /**
182
226
  * §8.7.3 — the TILING pattern resource name the path is filled with. Its
183
227
  * content is a stream of its own, so what the fill actually shows is only
@@ -222,7 +266,7 @@ export type Matrix = readonly [number, number, number, number, number, number];
222
266
  export declare const IDENTITY: Matrix;
223
267
  /** Compose two {@link Matrix matrices}: `a` applied first, then `b`. */
224
268
  export declare function multiply(a: Matrix, b: Matrix): Matrix;
225
- export declare function interpretContent(bytes: Uint8Array, fonts: ReadonlyMap<string, ContentFont>, initialCtm?: Matrix, shadings?: ReadonlyMap<string, ShapeGradient>, alphas?: ReadonlyMap<string, number>, spaces?: ReadonlyMap<string, ColorSpaceInfo>): 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;
226
270
  /**
227
271
  * Whether a string is wholly right-to-left: at least one letter of an RTL
228
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
@@ -91,6 +92,9 @@ function initialState() {
91
92
  fillGradient: void 0,
92
93
  fillPattern: void 0,
93
94
  fillAlpha: 1,
95
+ fillDarkens: false,
96
+ blendMode: void 0,
97
+ softMask: false,
94
98
  clip: void 0
95
99
  };
96
100
  }
@@ -143,15 +147,24 @@ function spaceOf(operands, spaces) {
143
147
  function componentColor(operands, space) {
144
148
  const nums = operands.filter((o) => typeof o === "number");
145
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
+ }
146
154
  switch (space?.kind ?? (nums.length === 3 ? "rgb" : nums.length === 4 ? "cmyk" : void 0)) {
147
155
  case "rgb": return nums.length >= 3 ? rgbHex(nums[0], nums[1], nums[2]) : void 0;
148
156
  case "cmyk": return nums.length >= 4 ? cmykHex(nums[0], nums[1], nums[2], nums[3]) : void 0;
149
157
  case "gray": return grayHex(nums[0]);
150
- case "tint": return grayHex(1 - Math.max(...nums));
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));
151
164
  default: return;
152
165
  }
153
166
  }
154
- function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__PURE__ */ new Map(), alphas = /* @__PURE__ */ new Map(), spaces = /* @__PURE__ */ new Map()) {
167
+ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__PURE__ */ new Map(), alphas = /* @__PURE__ */ new Map(), spaces = /* @__PURE__ */ new Map(), hiddenOc = /* @__PURE__ */ new Set()) {
155
168
  const runs = [];
156
169
  const images = [];
157
170
  const vectors = [];
@@ -167,6 +180,9 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
167
180
  let paintOrder = 0;
168
181
  let operands = [];
169
182
  const mcStack = [];
183
+ const ocStack = [];
184
+ let hiddenDepth = 0;
185
+ const visible = () => hiddenDepth === 0;
170
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]];
171
187
  const moveTo = (x, y) => {
172
188
  const [px, py] = toPage(x, y);
@@ -211,7 +227,7 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
211
227
  return state.lineWidth * scale;
212
228
  };
213
229
  const paintPath = (fill, stroke) => {
214
- if (path.length >= 2 && (fill || stroke)) {
230
+ if (path.length >= 2 && (fill || stroke) && visible()) {
215
231
  const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
216
232
  vectors.push({
217
233
  order: paintOrder++,
@@ -220,6 +236,9 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
220
236
  ...fill && state.fillPattern !== void 0 ? { patternName: state.fillPattern } : {},
221
237
  ...fill ? { fillHex: state.fillColor } : {},
222
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 } : {},
223
242
  ...fill && state.fillGradient ? { gradient: state.fillGradient } : {},
224
243
  ...stroke ? {
225
244
  strokeHex: state.strokeColor,
@@ -249,7 +268,7 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
249
268
  const type3 = state.font.type3;
250
269
  if (type3) {
251
270
  const stream = type3.proc(code);
252
- if (stream) {
271
+ if (stream && visible()) {
253
272
  const scale = [
254
273
  state.fontSize * state.hScale,
255
274
  0,
@@ -279,8 +298,10 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
279
298
  const text = logicalOrder(shown);
280
299
  if (text.length === 0) return;
281
300
  const scaleY = Math.hypot(origin[2], origin[3]) || 1;
301
+ const scaleX = (Math.hypot(origin[0], origin[1]) || 1) * state.hScale;
282
302
  const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
283
303
  const angle = Math.atan2(origin[1], origin[0]) * 180 / Math.PI;
304
+ if (!visible()) return;
284
305
  runs.push({
285
306
  text,
286
307
  x: origin[4],
@@ -288,7 +309,8 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
288
309
  endX: end[4],
289
310
  endY: end[5],
290
311
  ...Math.abs(angle) > UPRIGHT_TOLERANCE_DEG ? { angleDeg: angle } : {},
291
- 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,
292
314
  fontKey: state.fontKey,
293
315
  ...state.font.name !== void 0 ? { fontName: state.font.name } : {},
294
316
  ...state.font.type3 ? { type3: true } : {},
@@ -392,23 +414,31 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
392
414
  const props = operands[operands.length - 1];
393
415
  const mcidVal = !(tag instanceof PdfName && tag.value === "Artifact") && props instanceof Map ? props.get("MCID") : void 0;
394
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++;
395
420
  break;
396
421
  }
397
422
  case "BMC":
398
423
  mcStack.push(void 0);
424
+ ocStack.push(false);
399
425
  break;
400
426
  case "EMC":
401
427
  mcStack.pop();
428
+ if (ocStack.pop() === true) hiddenDepth--;
402
429
  break;
403
430
  case "Do": {
404
431
  const nm = operands[0];
405
- if (nm instanceof PdfName) {
432
+ if (nm instanceof PdfName && visible()) {
406
433
  const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
407
434
  images.push({
408
435
  order: paintOrder++,
409
436
  name: nm.value,
410
437
  ctm: state.ctm,
411
- ...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 } : {}
412
442
  });
413
443
  }
414
444
  break;
@@ -511,7 +541,13 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
511
541
  break;
512
542
  case "gs": {
513
543
  const nm = operands[operands.length - 1];
514
- 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
+ }
515
551
  break;
516
552
  }
517
553
  case "W":
@@ -545,8 +581,19 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
545
581
  operands.push(readDict(lexer));
546
582
  break;
547
583
  case "keyword":
548
- if (tok.value === "BI") skipInlineImage(lexer);
549
- 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);
550
597
  operands = [];
551
598
  break;
552
599
  default:
@@ -687,14 +734,62 @@ function readValue(lexer) {
687
734
  default: return PDF_NULL;
688
735
  }
689
736
  }
690
- 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();
691
749
  for (;;) {
692
750
  const tok = lexer.nextToken();
693
- if (tok.kind === "eof") return;
751
+ if (tok.kind === "eof") return void 0;
694
752
  if (tok.kind === "keyword" && tok.value === "ID") break;
753
+ if (tok.kind === "name") dict.set(tok.value, readValue(lexer));
695
754
  }
696
- 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);
697
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;
698
793
  }
699
794
  //#endregion
700
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 };
@@ -1,5 +1,6 @@
1
1
  import { PdfDict } from '../pdf/objects.js';
2
2
  import { PdfFile, PdfPage } from './document.js';
3
+ import { Loss } from '../core/ir/index.js';
3
4
  import { FontRegistry } from '../core/font/index.js';
4
5
  /**
5
6
  * Every embedded TrueType program the pages use, by the name a run will ask for
@@ -15,11 +16,19 @@ import { FontRegistry } from '../core/font/index.js';
15
16
  * (`/FontFile`) are different formats that the layout's parser does not take;
16
17
  * those keep their substitute.
17
18
  *
18
- * @param file The owning file.
19
- * @param pages The pages whose fonts are wanted.
19
+ * And only one whose OUTLINES this pipeline can carry: a program with no
20
+ * `glyf`/`loca` cannot be subset, and a face offered here is one the writer
21
+ * will be asked to embed. bug1186827.pdf ships an OpenType/CFF program under
22
+ * `/FontFile2`, which parses like any other and then killed the whole
23
+ * conversion — "Subsetting requires a TrueType font with glyf+loca tables" —
24
+ * rather than losing one face.
25
+ *
26
+ * @param file The owning file.
27
+ * @param pages The pages whose fonts are wanted.
28
+ * @param losses Appended to for a face the page embeds and this cannot carry.
20
29
  * @returns Name → a one-face registry holding that program.
21
30
  */
22
- export declare function collectEmbeddedFonts(file: PdfFile, pages: ReadonlyArray<PdfPage>): Map<string, FontRegistry>;
31
+ export declare function collectEmbeddedFonts(file: PdfFile, pages: ReadonlyArray<PdfPage>, losses?: Array<Loss>): Map<string, FontRegistry>;
23
32
  /**
24
33
  * The name a run set in `fontDict` will ask for: its `/BaseFont` without the
25
34
  * six-capital subset prefix (§9.6.4), lowercased.
@@ -1,5 +1,7 @@
1
+ import { parseTtf } from "../core/font/ttf-parser.js";
1
2
  import { FontRegistry } from "../core/font/font-registry.js";
2
3
  import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
4
+ import { FEATURES } from "../core/ir/features.js";
3
5
  //#region src/pdf-reader/embedded-fonts.ts
4
6
  var MAX_FORM_DEPTH = 8;
5
7
  /**
@@ -16,11 +18,19 @@ var MAX_FORM_DEPTH = 8;
16
18
  * (`/FontFile`) are different formats that the layout's parser does not take;
17
19
  * those keep their substitute.
18
20
  *
19
- * @param file The owning file.
20
- * @param pages The pages whose fonts are wanted.
21
+ * And only one whose OUTLINES this pipeline can carry: a program with no
22
+ * `glyf`/`loca` cannot be subset, and a face offered here is one the writer
23
+ * will be asked to embed. bug1186827.pdf ships an OpenType/CFF program under
24
+ * `/FontFile2`, which parses like any other and then killed the whole
25
+ * conversion — "Subsetting requires a TrueType font with glyf+loca tables" —
26
+ * rather than losing one face.
27
+ *
28
+ * @param file The owning file.
29
+ * @param pages The pages whose fonts are wanted.
30
+ * @param losses Appended to for a face the page embeds and this cannot carry.
21
31
  * @returns Name → a one-face registry holding that program.
22
32
  */
23
- function collectEmbeddedFonts(file, pages) {
33
+ function collectEmbeddedFonts(file, pages, losses) {
24
34
  const out = /* @__PURE__ */ new Map();
25
35
  const seen = /* @__PURE__ */ new Set();
26
36
  const visiting = /* @__PURE__ */ new Set();
@@ -32,6 +42,15 @@ function collectEmbeddedFonts(file, pages) {
32
42
  const program = fontProgram(file, fontDict);
33
43
  if (!program) return;
34
44
  try {
45
+ const parsed = parseTtf(program);
46
+ if (!parsed.tables.has("glyf") || !parsed.tables.has("loca")) {
47
+ losses?.push({
48
+ severity: "degraded",
49
+ feature: FEATURES.text,
50
+ detail: `embedded font ${name} has no TrueType outlines (a CFF program under /FontFile2); its text is re-set in a substitute face`
51
+ });
52
+ return;
53
+ }
35
54
  out.set(name, FontRegistry.fromBytes({ regular: program }));
36
55
  } catch {}
37
56
  };