reamkit 1.24.0 → 1.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +22 -11
  2. package/dist/esm/core/converter/facade.d.ts +3 -3
  3. package/dist/esm/core/converter/facade.js +12 -0
  4. package/dist/esm/core/converter/ream.d.ts +48 -4
  5. package/dist/esm/core/converter/ream.js +25 -4
  6. package/dist/esm/core/document-model/index.d.ts +1 -1
  7. package/dist/esm/core/document-model/types.d.ts +17 -0
  8. package/dist/esm/core/outline.d.ts +17 -0
  9. package/dist/esm/core/outline.js +30 -0
  10. package/dist/esm/core/style-cascade/resolver.js +1 -0
  11. package/dist/esm/core/style-cascade/types.d.ts +3 -1
  12. package/dist/esm/excel/sheet-to-flow.d.ts +10 -0
  13. package/dist/esm/excel/sheet-to-flow.js +14 -1
  14. package/dist/esm/html/html-writer.js +3 -2
  15. package/dist/esm/index.d.ts +3 -0
  16. package/dist/esm/index.js +2 -1
  17. package/dist/esm/layout/page-doc.js +1 -1
  18. package/dist/esm/layout/styled-layout.js +41 -15
  19. package/dist/esm/markdown/markdown-writer.d.ts +41 -0
  20. package/dist/esm/markdown/markdown-writer.js +733 -0
  21. package/dist/esm/pdf/styled-page-emitter.js +20 -1
  22. package/dist/esm/pdf-reader/annots.d.ts +24 -0
  23. package/dist/esm/pdf-reader/annots.js +126 -0
  24. package/dist/esm/pdf-reader/content.d.ts +131 -5
  25. package/dist/esm/pdf-reader/content.js +169 -12
  26. package/dist/esm/pdf-reader/display.d.ts +56 -0
  27. package/dist/esm/pdf-reader/display.js +162 -0
  28. package/dist/esm/pdf-reader/document.d.ts +36 -1
  29. package/dist/esm/pdf-reader/document.js +92 -25
  30. package/dist/esm/pdf-reader/embedded-fonts.d.ts +31 -0
  31. package/dist/esm/pdf-reader/embedded-fonts.js +94 -0
  32. package/dist/esm/pdf-reader/flow-build.d.ts +61 -6
  33. package/dist/esm/pdf-reader/flow-build.js +128 -22
  34. package/dist/esm/pdf-reader/font.js +185 -4
  35. package/dist/esm/pdf-reader/image-decode.js +55 -4
  36. package/dist/esm/pdf-reader/images.d.ts +6 -0
  37. package/dist/esm/pdf-reader/images.js +25 -5
  38. package/dist/esm/pdf-reader/jpeg.d.ts +18 -0
  39. package/dist/esm/pdf-reader/jpeg.js +419 -0
  40. package/dist/esm/pdf-reader/layout.d.ts +1 -1
  41. package/dist/esm/pdf-reader/layout.js +221 -32
  42. package/dist/esm/pdf-reader/pattern-tint.d.ts +17 -0
  43. package/dist/esm/pdf-reader/pattern-tint.js +181 -0
  44. package/dist/esm/pdf-reader/reader.d.ts +9 -1
  45. package/dist/esm/pdf-reader/reader.js +23 -7
  46. package/dist/esm/pdf-reader/shading.d.ts +14 -0
  47. package/dist/esm/pdf-reader/shading.js +27 -1
  48. package/dist/esm/pdf-reader/tagged.js +156 -17
  49. package/dist/esm/pdf-reader/text.d.ts +13 -1
  50. package/dist/esm/pdf-reader/text.js +70 -3
  51. package/dist/esm/pdf-reader/vector.d.ts +25 -1
  52. package/dist/esm/pdf-reader/vector.js +168 -12
  53. package/dist/esm/pptx/slide-parser.js +5 -0
  54. package/dist/esm/word/docx-writer.js +79 -17
  55. package/dist/esm/word/drawing-parser.js +7 -1
  56. package/package.json +1 -1
@@ -1,6 +1,39 @@
1
1
  import { PDF_NULL, PdfHexString, PdfName } from "../pdf/objects.js";
2
2
  import { Lexer } from "./lexer.js";
3
3
  //#region src/pdf-reader/content.ts
4
+ /**
5
+ * A painted path emitted by the path-painting operators (§8.5.3), captured in
6
+ * page space (y-up). The optional fields record only what the paint mode set:
7
+ * a fill colour/gradient (EP10/EP16c) and/or a stroke colour + width (EP11),
8
+ * plus the enclosing structure id.
9
+ */
10
+ /** A path's page-space bounding box, or `undefined` when it names no point. */
11
+ function pathBox(segs) {
12
+ let minX = Infinity;
13
+ let minY = Infinity;
14
+ let maxX = -Infinity;
15
+ let maxY = -Infinity;
16
+ const add = (x, y) => {
17
+ minX = Math.min(minX, x);
18
+ minY = Math.min(minY, y);
19
+ maxX = Math.max(maxX, x);
20
+ maxY = Math.max(maxY, y);
21
+ };
22
+ for (const seg of segs) if (seg.op === "move" || seg.op === "line") add(seg.x, seg.y);
23
+ else if (seg.op === "cubic") {
24
+ add(seg.x1, seg.y1);
25
+ add(seg.x2, seg.y2);
26
+ add(seg.x, seg.y);
27
+ }
28
+ return Number.isFinite(minX) ? {
29
+ minX,
30
+ minY,
31
+ maxX,
32
+ maxY
33
+ } : void 0;
34
+ }
35
+ /** The area of a box, for choosing the smaller of two clip regions. */
36
+ var area = (b) => Math.max(0, b.maxX - b.minX) * Math.max(0, b.maxY - b.minY);
4
37
  /** The identity {@link Matrix} (no transform). */
5
38
  var IDENTITY = [
6
39
  1,
@@ -36,12 +69,15 @@ var FALLBACK_FONT = {
36
69
  decode: (codes) => codes.map((c) => String.fromCharCode(c)).join(""),
37
70
  width: () => 500
38
71
  };
72
+ /** Below this a baseline is upright: a rounded matrix is not a turned one. */
73
+ var UPRIGHT_TOLERANCE_DEG = .5;
39
74
  function initialState() {
40
75
  return {
41
76
  ctm: IDENTITY,
42
77
  fontKey: "",
43
78
  font: FALLBACK_FONT,
44
79
  fontSize: 0,
80
+ renderMode: 0,
45
81
  charSpacing: 0,
46
82
  wordSpacing: 0,
47
83
  hScale: 1,
@@ -50,7 +86,10 @@ function initialState() {
50
86
  fillColor: "000000",
51
87
  strokeColor: "000000",
52
88
  lineWidth: 1,
53
- fillGradient: void 0
89
+ fillGradient: void 0,
90
+ fillPattern: void 0,
91
+ fillAlpha: 1,
92
+ clip: void 0
54
93
  };
55
94
  }
56
95
  /**
@@ -65,12 +104,14 @@ function initialState() {
65
104
  * an unmapped key falls back to Latin-1 with a half-em advance.
66
105
  * @param initialCtm The starting CTM mapping user space to page space.
67
106
  * @param shadings Shading patterns by name, selected by `scn`/`sc` (EP16c).
107
+ * @param alphas Constant fill alphas by `/ExtGState` name, selected by `gs`.
68
108
  * @returns The extracted text runs, image placements and vector paths.
69
109
  */
70
- function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__PURE__ */ new Map()) {
110
+ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__PURE__ */ new Map(), alphas = /* @__PURE__ */ new Map()) {
71
111
  const runs = [];
72
112
  const images = [];
73
113
  const vectors = [];
114
+ const glyphs = [];
74
115
  const lexer = new Lexer(bytes);
75
116
  const stack = [];
76
117
  let state = initialState();
@@ -78,6 +119,8 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
78
119
  let tm = IDENTITY;
79
120
  let tlm = IDENTITY;
80
121
  let path = [];
122
+ let pendingClip = false;
123
+ let paintOrder = 0;
81
124
  let operands = [];
82
125
  const mcStack = [];
83
126
  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,8 +170,12 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
127
170
  if (path.length >= 2 && (fill || stroke)) {
128
171
  const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
129
172
  vectors.push({
173
+ order: paintOrder++,
130
174
  segs: path,
175
+ ...state.clip ? { clip: state.clip } : {},
176
+ ...fill && state.fillPattern !== void 0 ? { patternName: state.fillPattern } : {},
131
177
  ...fill ? { fillHex: state.fillColor } : {},
178
+ ...fill && state.fillAlpha < 1 ? { alpha: state.fillAlpha } : {},
132
179
  ...fill && state.fillGradient ? { gradient: state.fillGradient } : {},
133
180
  ...stroke ? {
134
181
  strokeHex: state.strokeColor,
@@ -137,6 +184,17 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
137
184
  ...mcid !== void 0 ? { mcid } : {}
138
185
  });
139
186
  }
187
+ if (pendingClip) {
188
+ pendingClip = false;
189
+ const box = pathBox(path);
190
+ if (box) {
191
+ const next = {
192
+ segs: path,
193
+ ...box
194
+ };
195
+ state.clip = state.clip && area(state.clip) <= area(next) ? state.clip : next;
196
+ }
197
+ }
140
198
  path = [];
141
199
  };
142
200
  const num = (i) => {
@@ -144,6 +202,26 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
144
202
  return typeof v === "number" ? v : 0;
145
203
  };
146
204
  const advanceGlyph = (code) => {
205
+ const type3 = state.font.type3;
206
+ if (type3) {
207
+ const stream = type3.proc(code);
208
+ if (stream) {
209
+ const scale = [
210
+ state.fontSize * state.hScale,
211
+ 0,
212
+ 0,
213
+ state.fontSize,
214
+ 0,
215
+ state.rise
216
+ ];
217
+ glyphs.push({
218
+ stream,
219
+ resources: type3.resources,
220
+ ctm: multiply(type3.matrix, multiply(scale, multiply(tm, state.ctm))),
221
+ order: paintOrder++
222
+ });
223
+ }
224
+ }
147
225
  const w0 = state.font.width(code) / 1e3;
148
226
  const isSpace = state.font.bytesPerCode === 1 && code === 32;
149
227
  tm = multiply(translation((w0 * state.fontSize + state.charSpacing + (isSpace ? state.wordSpacing : 0)) * state.hScale, 0), tm);
@@ -153,28 +231,41 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
153
231
  for (const code of codes) advanceGlyph(code);
154
232
  return state.font.decode(codes);
155
233
  };
156
- const emitAt = (origin, text) => {
234
+ const emitAt = (origin, shown, end) => {
235
+ const text = logicalOrder(shown);
157
236
  if (text.length === 0) return;
158
237
  const scaleY = Math.hypot(origin[2], origin[3]) || 1;
159
238
  const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
239
+ const angle = Math.atan2(origin[1], origin[0]) * 180 / Math.PI;
160
240
  runs.push({
161
241
  text,
162
242
  x: origin[4],
163
243
  y: origin[5],
244
+ endX: end[4],
245
+ endY: end[5],
246
+ ...Math.abs(angle) > UPRIGHT_TOLERANCE_DEG ? { angleDeg: angle } : {},
164
247
  fontSizePt: state.fontSize * scaleY,
165
248
  fontKey: state.fontKey,
249
+ ...state.font.name !== void 0 ? { fontName: state.font.name } : {},
250
+ ...state.font.type3 ? { type3: true } : {},
251
+ ...state.renderMode === 3 || state.renderMode === 7 ? { invisible: true } : {},
252
+ ...state.fillPattern !== void 0 ? { fillPatternName: state.fillPattern } : {},
253
+ ...strokesText(state.renderMode) ? {
254
+ outlineHex: state.strokeColor,
255
+ outlineWidthPt: ctmLineWidth()
256
+ } : {},
257
+ ...state.font.bold ? { bold: true } : {},
258
+ ...state.font.italic ? { italic: true } : {},
259
+ colorHex: state.fillColor,
166
260
  ...mcid !== void 0 ? { mcid } : {}
167
261
  });
168
262
  };
169
263
  const showString = (operand) => {
170
- emitAt(multiply(tm, state.ctm), consume(operand));
264
+ emitAt(multiply(tm, state.ctm), consume(operand), multiply(tm, state.ctm));
171
265
  };
172
266
  const showArray = (arr) => {
173
- const origin = multiply(tm, state.ctm);
174
- let text = "";
175
267
  for (const el of arr) if (typeof el === "number") tm = multiply(translation(-el / 1e3 * state.fontSize * state.hScale, 0), tm);
176
- else if (typeof el === "string" || el instanceof PdfHexString) text += consume(el);
177
- emitAt(origin, text);
268
+ else if (typeof el === "string" || el instanceof PdfHexString) emitAt(multiply(tm, state.ctm), consume(el), multiply(tm, state.ctm));
178
269
  };
179
270
  const exec = (op) => {
180
271
  switch (op) {
@@ -222,6 +313,9 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
222
313
  case "Tc":
223
314
  state.charSpacing = num(0);
224
315
  break;
316
+ case "Tr":
317
+ state.renderMode = num(0);
318
+ break;
225
319
  case "Tw":
226
320
  state.wordSpacing = num(0);
227
321
  break;
@@ -267,6 +361,7 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
267
361
  if (nm instanceof PdfName) {
268
362
  const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
269
363
  images.push({
364
+ order: paintOrder++,
270
365
  name: nm.value,
271
366
  ctm: state.ctm,
272
367
  ...mcid !== void 0 ? { mcid } : {}
@@ -277,19 +372,24 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
277
372
  case "rg":
278
373
  state.fillColor = rgbHex(num(0), num(1), num(2));
279
374
  state.fillGradient = void 0;
375
+ state.fillPattern = void 0;
280
376
  break;
281
377
  case "g":
282
378
  state.fillColor = grayHex(num(0));
283
379
  state.fillGradient = void 0;
380
+ state.fillPattern = void 0;
284
381
  break;
285
382
  case "k":
286
383
  state.fillColor = cmykHex(num(0), num(1), num(2), num(3));
287
384
  state.fillGradient = void 0;
385
+ state.fillPattern = void 0;
288
386
  break;
289
387
  case "scn":
290
388
  case "sc": {
291
389
  const last = operands[operands.length - 1];
292
- state.fillGradient = last instanceof PdfName ? shadings.get(last.value) : void 0;
390
+ 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;
293
393
  break;
294
394
  }
295
395
  case "RG":
@@ -343,8 +443,15 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
343
443
  case "n":
344
444
  paintPath(false, false);
345
445
  break;
446
+ case "gs": {
447
+ const nm = operands[operands.length - 1];
448
+ if (nm instanceof PdfName) state.fillAlpha = alphas.get(nm.value) ?? 1;
449
+ break;
450
+ }
346
451
  case "W":
347
- case "W*": break;
452
+ case "W*":
453
+ pendingClip = true;
454
+ break;
348
455
  default: break;
349
456
  }
350
457
  };
@@ -384,9 +491,59 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
384
491
  return {
385
492
  texts: runs,
386
493
  images,
387
- vectors
494
+ vectors,
495
+ glyphs
388
496
  };
389
497
  }
498
+ /** §9.3.6 — the rendering modes that put a line round the glyphs. */
499
+ function strokesText(mode) {
500
+ return mode === 1 || mode === 2 || mode === 5 || mode === 6;
501
+ }
502
+ /**
503
+ * §9.4 — a show operator paints its glyphs along the baseline, left to right,
504
+ * whatever the script. For Arabic or Hebrew that means the string a PDF holds
505
+ * is in VISUAL order: the first character of the word is the last one shown.
506
+ *
507
+ * Every reader turns this back into logical order before handing it on, because
508
+ * everything downstream — a search, a markdown file, a layout engine that does
509
+ * its own bidi — takes logical order and reverses it again for display.
510
+ * ArabicCIDTrueType.pdf came out mirrored for exactly that reason: the reader
511
+ * passed visual order through and the layout reversed it a second time.
512
+ *
513
+ * A run is reversed only when it is wholly right-to-left. Anything mixed — a
514
+ * number inside an Arabic sentence runs left to right — needs the full bidi
515
+ * algorithm, and guessing at it would be worse than leaving it alone.
516
+ */
517
+ function logicalOrder(text) {
518
+ return isRightToLeft(text) ? [...text].reverse().join("") : text;
519
+ }
520
+ /**
521
+ * Whether a string is wholly right-to-left: at least one letter of an RTL
522
+ * script and nothing of any other, spaces and joiners aside.
523
+ *
524
+ * Anything mixed — a number inside an Arabic sentence runs left to right —
525
+ * needs the full bidi algorithm, and guessing at it would be worse than
526
+ * leaving it alone.
527
+ *
528
+ * @param text The string to judge.
529
+ * @returns Whether it is one run of right-to-left script.
530
+ */
531
+ function isRightToLeft(text) {
532
+ let rtl = false;
533
+ for (const ch of text) {
534
+ const cp = ch.codePointAt(0);
535
+ if (isRtl(cp)) {
536
+ rtl = true;
537
+ continue;
538
+ }
539
+ if (cp !== 32 && cp !== 10 && cp !== 8204 && cp !== 8205) return false;
540
+ }
541
+ return rtl;
542
+ }
543
+ /** The right-to-left blocks: Hebrew, Arabic, Syriac, Thaana, and the forms. */
544
+ function isRtl(cp) {
545
+ return cp >= 1424 && cp <= 1535 || cp >= 1536 && cp <= 1791 || cp >= 1792 && cp <= 1871 || cp >= 1920 && cp <= 1983 || cp >= 2208 && cp <= 2303 || cp >= 64285 && cp <= 65023 || cp >= 65136 && cp <= 65279;
546
+ }
390
547
  function clamp255(v) {
391
548
  return Math.max(0, Math.min(255, Math.round(v * 255)));
392
549
  }
@@ -474,4 +631,4 @@ function skipInlineImage(lexer) {
474
631
  lexer.pos = ei < 0 ? lexer.length : ei + 2;
475
632
  }
476
633
  //#endregion
477
- export { IDENTITY, interpretContent, multiply };
634
+ export { IDENTITY, interpretContent, isRightToLeft, multiply };
@@ -0,0 +1,56 @@
1
+ import { TextRun } from './content.js';
2
+ import { PdfImage } from './images.js';
3
+ import { PdfPage } from './document.js';
4
+ import { PdfVector } from './vector.js';
5
+ /** A page's shown geometry: how big it is, and how to place a mark on it. */
6
+ export interface Display {
7
+ /** The shown page's width in points — the `/MediaBox`'s, swapped on a quarter turn. */
8
+ readonly width: number;
9
+ /** The shown page's height in points. */
10
+ readonly height: number;
11
+ /** How far every mark turns with the page, degrees counter-clockwise. */
12
+ readonly turnDeg: number;
13
+ /** Map a `/MediaBox`-space point into the shown page's own y-up frame. */
14
+ readonly place: (x: number, y: number) => {
15
+ x: number;
16
+ y: number;
17
+ };
18
+ }
19
+ /**
20
+ * The {@link Display} a page's `/MediaBox` and `/Rotate` describe.
21
+ *
22
+ * `/Rotate` turns the page CLOCKWISE when shown, so the content turns with it —
23
+ * counter-clockwise by the same amount as seen from the content's own frame,
24
+ * which is what `turnDeg` states.
25
+ *
26
+ * @param page The page whose shown geometry is wanted.
27
+ * @returns Its size, its turn, and the map onto it.
28
+ */
29
+ export declare function displayOf(page: PdfPage): Display;
30
+ /**
31
+ * The same runs, placed on the shown page.
32
+ *
33
+ * @param runs The runs as the content stream drew them.
34
+ * @param d The page's shown geometry.
35
+ * @returns The runs in the shown page's frame, each carrying the page's turn.
36
+ */
37
+ export declare function placeRuns(runs: ReadonlyArray<TextRun>, d: Display): Array<TextRun>;
38
+ /**
39
+ * The same pictures, placed on the shown page. A quarter turn swaps a picture's
40
+ * width and height and leaves it standing on its side, which the caller carries
41
+ * as the picture's own rotation.
42
+ *
43
+ * @param images The pictures as the content stream placed them.
44
+ * @param d The page's shown geometry.
45
+ * @returns The pictures in the shown page's frame.
46
+ */
47
+ export declare function placeImages(images: ReadonlyArray<PdfImage>, d: Display): Array<PdfImage>;
48
+ /**
49
+ * The same paths, placed on the shown page. A path is geometry, so every point
50
+ * of it moves and the bounding box is taken again from what comes out.
51
+ *
52
+ * @param vectors The paths as the content stream painted them.
53
+ * @param d The page's shown geometry.
54
+ * @returns The paths in the shown page's frame.
55
+ */
56
+ export declare function placeVectors(vectors: ReadonlyArray<PdfVector>, d: Display): Array<PdfVector>;
@@ -0,0 +1,162 @@
1
+ //#region src/pdf-reader/display.ts
2
+ /**
3
+ * The {@link Display} a page's `/MediaBox` and `/Rotate` describe.
4
+ *
5
+ * `/Rotate` turns the page CLOCKWISE when shown, so the content turns with it —
6
+ * counter-clockwise by the same amount as seen from the content's own frame,
7
+ * which is what `turnDeg` states.
8
+ *
9
+ * @param page The page whose shown geometry is wanted.
10
+ * @returns Its size, its turn, and the map onto it.
11
+ */
12
+ function displayOf(page) {
13
+ const [x0, y0, x1, y1] = page.mediaBox;
14
+ const left = Math.min(x0, x1);
15
+ const bottom = Math.min(y0, y1);
16
+ const w = Math.abs(x1 - x0);
17
+ const h = Math.abs(y1 - y0);
18
+ const quarter = page.rotate === 90 || page.rotate === 270;
19
+ const place = (x, y) => {
20
+ const mx = x - left;
21
+ const my = y - bottom;
22
+ switch (page.rotate) {
23
+ case 90: return {
24
+ x: my,
25
+ y: w - mx
26
+ };
27
+ case 180: return {
28
+ x: w - mx,
29
+ y: h - my
30
+ };
31
+ case 270: return {
32
+ x: h - my,
33
+ y: mx
34
+ };
35
+ default: return {
36
+ x: mx,
37
+ y: my
38
+ };
39
+ }
40
+ };
41
+ return {
42
+ width: quarter ? h : w,
43
+ height: quarter ? w : h,
44
+ turnDeg: page.rotate === 90 ? -90 : page.rotate === 180 ? 180 : page.rotate === 270 ? 90 : 0,
45
+ place
46
+ };
47
+ }
48
+ /**
49
+ * The same runs, placed on the shown page.
50
+ *
51
+ * @param runs The runs as the content stream drew them.
52
+ * @param d The page's shown geometry.
53
+ * @returns The runs in the shown page's frame, each carrying the page's turn.
54
+ */
55
+ function placeRuns(runs, d) {
56
+ return runs.map((r) => {
57
+ const origin = d.place(r.x, r.y);
58
+ const end = d.place(r.endX, r.endY);
59
+ const angle = ((r.angleDeg ?? 0) + d.turnDeg) % 360 + 0;
60
+ const { angleDeg: _was, ...rest } = r;
61
+ return {
62
+ ...rest,
63
+ x: origin.x,
64
+ y: origin.y,
65
+ endX: end.x,
66
+ endY: end.y,
67
+ ...Math.abs(angle) > .5 ? { angleDeg: angle } : {}
68
+ };
69
+ });
70
+ }
71
+ /**
72
+ * The same pictures, placed on the shown page. A quarter turn swaps a picture's
73
+ * width and height and leaves it standing on its side, which the caller carries
74
+ * as the picture's own rotation.
75
+ *
76
+ * @param images The pictures as the content stream placed them.
77
+ * @param d The page's shown geometry.
78
+ * @returns The pictures in the shown page's frame.
79
+ */
80
+ function placeImages(images, d) {
81
+ return images.map((img) => {
82
+ const corners = [
83
+ d.place(img.x, img.y),
84
+ d.place(img.x + img.widthPt, img.y),
85
+ d.place(img.x + img.widthPt, img.y + img.heightPt),
86
+ d.place(img.x, img.y + img.heightPt)
87
+ ];
88
+ const xs = corners.map((c) => c.x);
89
+ const ys = corners.map((c) => c.y);
90
+ const minX = Math.min(...xs);
91
+ const minY = Math.min(...ys);
92
+ return {
93
+ ...img,
94
+ x: minX,
95
+ y: minY,
96
+ widthPt: Math.max(...xs) - minX,
97
+ heightPt: Math.max(...ys) - minY
98
+ };
99
+ });
100
+ }
101
+ /**
102
+ * The same paths, placed on the shown page. A path is geometry, so every point
103
+ * of it moves and the bounding box is taken again from what comes out.
104
+ *
105
+ * @param vectors The paths as the content stream painted them.
106
+ * @param d The page's shown geometry.
107
+ * @returns The paths in the shown page's frame.
108
+ */
109
+ function placeVectors(vectors, d) {
110
+ return vectors.map((v) => {
111
+ const segs = v.segs.map((s) => {
112
+ switch (s.op) {
113
+ case "move":
114
+ case "line": {
115
+ const p = d.place(s.x, s.y);
116
+ return {
117
+ op: s.op,
118
+ x: p.x,
119
+ y: p.y
120
+ };
121
+ }
122
+ case "cubic": {
123
+ const c1 = d.place(s.x1, s.y1);
124
+ const c2 = d.place(s.x2, s.y2);
125
+ const p = d.place(s.x, s.y);
126
+ return {
127
+ op: "cubic",
128
+ x1: c1.x,
129
+ y1: c1.y,
130
+ x2: c2.x,
131
+ y2: c2.y,
132
+ x: p.x,
133
+ y: p.y
134
+ };
135
+ }
136
+ case "close": return s;
137
+ }
138
+ });
139
+ const xs = [];
140
+ const ys = [];
141
+ for (const s of segs) {
142
+ if (s.op === "close") continue;
143
+ xs.push(s.x);
144
+ ys.push(s.y);
145
+ if (s.op === "cubic") xs.push(s.x1, s.x2), ys.push(s.y1, s.y2);
146
+ }
147
+ if (xs.length === 0) return {
148
+ ...v,
149
+ segs
150
+ };
151
+ return {
152
+ ...v,
153
+ segs,
154
+ minX: Math.min(...xs),
155
+ minY: Math.min(...ys),
156
+ maxX: Math.max(...xs),
157
+ maxY: Math.max(...ys)
158
+ };
159
+ });
160
+ }
161
+ //#endregion
162
+ export { displayOf, placeImages, placeRuns, placeVectors };
@@ -5,6 +5,11 @@ 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.1 `/Rotate` — how far the page turns CLOCKWISE when it is shown,
10
+ * normalised to 0, 90, 180 or 270 and inherited down the page tree.
11
+ */
12
+ readonly rotate: 0 | 90 | 180 | 270;
8
13
  readonly resources: PdfDict | undefined;
9
14
  }
10
15
  /**
@@ -19,6 +24,8 @@ export declare class PdfFile {
19
24
  private readonly buf;
20
25
  private readonly xref;
21
26
  readonly trailer: PdfDict;
27
+ /** Caller-supplied decoders, by `/Filter` name (see {@link StreamFilters}). */
28
+ readonly filters: StreamFilters;
22
29
  private readonly cache;
23
30
  private readonly objStmCache;
24
31
  private decryptor;
@@ -31,9 +38,10 @@ export declare class PdfFile {
31
38
  * @param bytes The complete PDF file bytes.
32
39
  * @param password The user password for an encrypted source (EP14); the empty
33
40
  * string opens permissions-only encryption.
41
+ * @param filters Decoders for `/Filter` names this reader does not implement.
34
42
  * @returns A ready-to-query {@link PdfFile}.
35
43
  */
36
- static parse(bytes: Uint8Array, password?: string): PdfFile;
44
+ static parse(bytes: Uint8Array, password?: string, filters?: StreamFilters): PdfFile;
37
45
  /**
38
46
  * Build the decryptor from `/Encrypt` (§7.6). Runs before any other object is
39
47
  * resolved, so the `/Encrypt` dictionary itself is read in the clear; its object
@@ -78,6 +86,33 @@ export declare class PdfFile {
78
86
  * `/Predictor` supported; unknown filters pass through undecoded).
79
87
  */
80
88
  streamData(stream: PdfStream): Uint8Array;
89
+ /** Filter names met in this file that nothing here can undo. */
90
+ readonly unknownFilters: Set<string>;
81
91
  }
92
+ /**
93
+ * A decoder for one `/Filter` name, supplied by the caller.
94
+ *
95
+ * §7.4 leaves the filter set open, and a reader is not obliged to implement
96
+ * every one — but it cannot pretend, either: an undecoded stream is not the
97
+ * stream. Brotli-Prototype-FileA.pdf compresses all thirty of its streams with
98
+ * `/BrotliDecode` (PDF 2.0), including the cross-reference, so unread it is a
99
+ * document with no pages at all.
100
+ *
101
+ * Rather than carry a decoder for every filter anyone might write — Brotli
102
+ * alone is RFC 7932's context-modelled Huffman scheme and a 122 KB static
103
+ * dictionary, in every bundle, for a filter almost nothing produces — the
104
+ * reader takes one from whoever needs it:
105
+ *
106
+ * ```ts
107
+ * import { brotliDecompressSync } from 'node:zlib';
108
+ * Ream.parse(pdf, { filters: { BrotliDecode: (b) => brotliDecompressSync(b) } });
109
+ * ```
110
+ *
111
+ * A decoder that throws is treated as one that was never supplied: the filter
112
+ * is reported unreadable rather than its failure escaping into the parse.
113
+ */
114
+ export type StreamFilter = (bytes: Uint8Array) => Uint8Array;
115
+ /** Caller-supplied {@link StreamFilter}s, keyed by `/Filter` name (no slash). */
116
+ export type StreamFilters = Readonly<Record<string, StreamFilter | undefined>>;
82
117
  /** Re-export for callers that walk a resolved dict's array values. */
83
118
  export type { PdfArray };