reamkit 1.23.0 → 1.25.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 (102) hide show
  1. package/README.md +22 -11
  2. package/dist/esm/core/bmp.d.ts +33 -0
  3. package/dist/esm/core/bmp.js +276 -0
  4. package/dist/esm/core/converter/facade.d.ts +3 -3
  5. package/dist/esm/core/converter/facade.js +12 -0
  6. package/dist/esm/core/converter/ream.d.ts +48 -4
  7. package/dist/esm/core/converter/ream.js +25 -4
  8. package/dist/esm/core/document-model/index.d.ts +1 -1
  9. package/dist/esm/core/document-model/types.d.ts +54 -0
  10. package/dist/esm/core/drawingml/chart-geometry.js +3 -2
  11. package/dist/esm/core/drawingml/diagram/colors.d.ts +52 -0
  12. package/dist/esm/core/drawingml/diagram/colors.js +125 -0
  13. package/dist/esm/core/drawingml/diagram/data-model.d.ts +47 -0
  14. package/dist/esm/core/drawingml/diagram/data-model.js +119 -0
  15. package/dist/esm/core/drawingml/diagram/layout-engine.d.ts +46 -0
  16. package/dist/esm/core/drawingml/diagram/layout-engine.js +948 -0
  17. package/dist/esm/core/drawingml/diagram/run.d.ts +37 -0
  18. package/dist/esm/core/drawingml/diagram/run.js +50 -0
  19. package/dist/esm/core/drawingml/diagram/to-drawing.d.ts +16 -0
  20. package/dist/esm/core/drawingml/diagram/to-drawing.js +86 -0
  21. package/dist/esm/core/drawingml/preset-geometry.js +116 -6
  22. package/dist/esm/core/drawingml/text-warp.d.ts +58 -0
  23. package/dist/esm/core/drawingml/text-warp.js +355 -0
  24. package/dist/esm/core/drawingml/theme-parser.js +9 -3
  25. package/dist/esm/core/font/measure.d.ts +12 -0
  26. package/dist/esm/core/font/measure.js +36 -0
  27. package/dist/esm/core/images.d.ts +30 -2
  28. package/dist/esm/core/images.js +121 -5
  29. package/dist/esm/core/metafile/emf.js +144 -16
  30. package/dist/esm/core/metafile/picture.d.ts +49 -0
  31. package/dist/esm/core/metafile/picture.js +70 -1
  32. package/dist/esm/core/metafile/wmf.js +53 -4
  33. package/dist/esm/core/ole/escher-blip.js +11 -1
  34. package/dist/esm/core/outline.d.ts +17 -0
  35. package/dist/esm/core/outline.js +30 -0
  36. package/dist/esm/core/style-cascade/resolver.js +1 -0
  37. package/dist/esm/core/style-cascade/types.d.ts +3 -1
  38. package/dist/esm/excel/print-model.js +2 -2
  39. package/dist/esm/excel/sheet-drawing.js +1 -1
  40. package/dist/esm/excel/sheet-to-flow.d.ts +10 -0
  41. package/dist/esm/excel/sheet-to-flow.js +30 -3
  42. package/dist/esm/html/html-writer.js +5 -3
  43. package/dist/esm/index.d.ts +3 -0
  44. package/dist/esm/index.js +2 -1
  45. package/dist/esm/layout/page-doc.d.ts +35 -0
  46. package/dist/esm/layout/page-doc.js +14 -1
  47. package/dist/esm/layout/styled-layout.js +217 -30
  48. package/dist/esm/markdown/markdown-writer.d.ts +41 -0
  49. package/dist/esm/markdown/markdown-writer.js +733 -0
  50. package/dist/esm/pdf/shading.js +8 -6
  51. package/dist/esm/pdf/styled-page-emitter.js +62 -4
  52. package/dist/esm/pdf/vector-graphics.js +1 -1
  53. package/dist/esm/pdf-reader/annots.d.ts +24 -0
  54. package/dist/esm/pdf-reader/annots.js +126 -0
  55. package/dist/esm/pdf-reader/content.d.ts +131 -5
  56. package/dist/esm/pdf-reader/content.js +169 -12
  57. package/dist/esm/pdf-reader/display.d.ts +56 -0
  58. package/dist/esm/pdf-reader/display.js +162 -0
  59. package/dist/esm/pdf-reader/document.d.ts +36 -1
  60. package/dist/esm/pdf-reader/document.js +92 -25
  61. package/dist/esm/pdf-reader/embedded-fonts.d.ts +31 -0
  62. package/dist/esm/pdf-reader/embedded-fonts.js +94 -0
  63. package/dist/esm/pdf-reader/flow-build.d.ts +61 -6
  64. package/dist/esm/pdf-reader/flow-build.js +128 -22
  65. package/dist/esm/pdf-reader/font.js +185 -4
  66. package/dist/esm/pdf-reader/image-decode.js +56 -5
  67. package/dist/esm/pdf-reader/images.d.ts +6 -0
  68. package/dist/esm/pdf-reader/images.js +25 -5
  69. package/dist/esm/pdf-reader/jpeg.d.ts +18 -0
  70. package/dist/esm/pdf-reader/jpeg.js +419 -0
  71. package/dist/esm/pdf-reader/layout.d.ts +1 -1
  72. package/dist/esm/pdf-reader/layout.js +221 -32
  73. package/dist/esm/pdf-reader/pattern-tint.d.ts +17 -0
  74. package/dist/esm/pdf-reader/pattern-tint.js +181 -0
  75. package/dist/esm/pdf-reader/reader.d.ts +9 -1
  76. package/dist/esm/pdf-reader/reader.js +22 -6
  77. package/dist/esm/pdf-reader/shading.d.ts +14 -0
  78. package/dist/esm/pdf-reader/shading.js +27 -1
  79. package/dist/esm/pdf-reader/tagged.js +156 -17
  80. package/dist/esm/pdf-reader/text.d.ts +13 -1
  81. package/dist/esm/pdf-reader/text.js +70 -3
  82. package/dist/esm/pdf-reader/vector.d.ts +25 -1
  83. package/dist/esm/pdf-reader/vector.js +168 -12
  84. package/dist/esm/pptx/ppt/ppt-reader.js +95 -27
  85. package/dist/esm/pptx/ppt/ppt-text.d.ts +41 -0
  86. package/dist/esm/pptx/ppt/ppt-text.js +327 -37
  87. package/dist/esm/pptx/pptx-reader.js +90 -10
  88. package/dist/esm/pptx/slide-parser.d.ts +4 -1
  89. package/dist/esm/pptx/slide-parser.js +104 -13
  90. package/dist/esm/pptx/sp-helpers.d.ts +7 -3
  91. package/dist/esm/pptx/sp-helpers.js +45 -7
  92. package/dist/esm/pptx/table-style.d.ts +7 -0
  93. package/dist/esm/pptx/table-style.js +59 -7
  94. package/dist/esm/svg/svg-writer.js +1 -0
  95. package/dist/esm/word/document-parser.d.ts +4 -1
  96. package/dist/esm/word/document-parser.js +4 -1
  97. package/dist/esm/word/docx-reader.js +26 -13
  98. package/dist/esm/word/docx-writer.js +15 -1
  99. package/dist/esm/word/drawing-parser.d.ts +20 -0
  100. package/dist/esm/word/drawing-parser.js +44 -5
  101. package/dist/esm/word/table-parser.js +3 -0
  102. package/package.json +1 -1
@@ -1,5 +1,10 @@
1
+ import { pt } from "../core/ir/units.js";
1
2
  import { ResourceStore } from "../core/ir/resources.js";
2
- import { buildFlowDoc, dedupeLosses, imageBlock, paragraphFromRuns, sectionFromPdfPages, shapeBlock } from "./flow-build.js";
3
+ import { FEATURES } from "../core/ir/features.js";
4
+ import { displayOf, placeImages, placeRuns, placeVectors } from "./display.js";
5
+ import { buildFlowDoc, dedupeLosses, imageBlock, paragraphFromRuns, positionedText, sectionFromPdfPages, shapeBlock } from "./flow-build.js";
6
+ import { collectEmbeddedFonts } from "./embedded-fonts.js";
7
+ import { isRightToLeft } from "./content.js";
3
8
  import { collectPageImages } from "./images.js";
4
9
  import { extractPageText } from "./text.js";
5
10
  import { collectPageVectors } from "./vector.js";
@@ -19,19 +24,40 @@ import { collectPageVectors } from "./vector.js";
19
24
  * @param file The PDF to reconstruct.
20
25
  * @returns The reconstructed {@link FlowDoc} plus any read-time losses.
21
26
  */
22
- function reconstructByLayout(file) {
27
+ function reconstructByLayout(file, mode = "flow") {
23
28
  const pages = file.pages();
24
- const pageRuns = pages.map((page) => extractPageText(file, page));
29
+ const shown = pages.map((page) => displayOf(page));
30
+ const pageRuns = pages.map((page, i) => placeRuns(extractPageText(file, page), shown[i]));
25
31
  const medianFont = median(pageRuns.flat().map((r) => r.fontSizePt).filter((s) => s > 0)) || 12;
26
32
  const resources = new ResourceStore();
27
33
  const losses = [];
34
+ if (pageRuns.some((page) => page.some((r) => r.fillPatternName !== void 0))) losses.push({
35
+ severity: "degraded",
36
+ feature: FEATURES.text,
37
+ detail: "text filled with a tiling pattern is drawn as a flat tint of the pattern’s colour, not as the pattern"
38
+ });
28
39
  const body = [];
29
40
  pages.forEach((page, i) => {
30
41
  const runs = pageRuns[i];
31
- const [px0, , px1] = page.mediaBox;
32
- const gutter = detectGutter(runs, Math.abs(px1 - px0));
42
+ const display = shown[i];
43
+ const pageWidth = display.width;
44
+ const gutter = detectGutter(runs, pageWidth);
33
45
  const blocks = [];
34
- const addColumn = (colRuns, col) => {
46
+ const addColumn = (allRuns, col) => {
47
+ const colRuns = mode === "positional" ? allRuns.filter((r) => r.type3 !== true && r.invisible !== true) : allRuns;
48
+ if (mode === "positional") {
49
+ for (const [angle, runs] of byAngle(colRuns)) for (const line of groupIntoLines(rotate(runs, -angle), true)) {
50
+ if (line.text.length === 0) continue;
51
+ const box = turnedBox(line, angle, pageWidth);
52
+ placed.push({
53
+ key: [Number.MAX_SAFE_INTEGER, placed.length],
54
+ col,
55
+ top: box.y + box.height,
56
+ make: (z) => positionedText(line.spans, box, frame, z, rotation60kOf(angle))
57
+ });
58
+ }
59
+ return;
60
+ }
35
61
  const lines = groupIntoLines(colRuns).filter((l) => l.text.length > 0);
36
62
  for (const para of groupIntoParagraphs(lines)) blocks.push({
37
63
  col,
@@ -39,36 +65,74 @@ function reconstructByLayout(file) {
39
65
  el: paragraphFromRuns(para.spans, headingLevel(para.fontSize, medianFont))
40
66
  });
41
67
  };
68
+ const placed = [];
42
69
  if (gutter !== void 0) {
43
70
  addColumn(runs.filter((r) => r.x < gutter), 0);
44
71
  addColumn(runs.filter((r) => r.x >= gutter), 1);
45
72
  } else addColumn(runs, 0);
46
73
  const colOf = (centerX) => gutter !== void 0 && centerX >= gutter ? 1 : 0;
47
- const imgs = collectPageImages(file, page);
74
+ const frame = {
75
+ left: 0,
76
+ top: display.height
77
+ };
78
+ const raw = collectPageImages(file, page);
79
+ const imgs = {
80
+ images: placeImages(raw.images, display),
81
+ losses: raw.losses
82
+ };
48
83
  losses.push(...imgs.losses);
49
- for (const img of imgs.images) blocks.push({
50
- col: colOf(img.x + img.widthPt / 2),
51
- top: img.y + img.heightPt,
52
- el: imageBlock(img, resources)
53
- });
54
- for (const v of collectPageVectors(file, page)) blocks.push({
55
- col: colOf((v.minX + v.maxX) / 2),
56
- top: v.maxY,
57
- el: shapeBlock(v)
84
+ const lifted = collectPageVectors(file, page, imgs.images.map((img) => ({
85
+ minX: img.x,
86
+ minY: img.y,
87
+ maxX: img.x + img.widthPt,
88
+ maxY: img.y + img.heightPt
89
+ })));
90
+ losses.push(...lifted.losses);
91
+ const vectors = placeVectors(lifted.vectors, display);
92
+ [
93
+ ...imgs.images.map((img) => ({
94
+ key: img.orderKey,
95
+ col: colOf(img.x + img.widthPt / 2),
96
+ top: img.y + img.heightPt,
97
+ make: (z) => imageBlock(img, resources, void 0, frame, z)
98
+ })),
99
+ ...vectors.map((v) => ({
100
+ key: v.orderKey,
101
+ col: colOf((v.minX + v.maxX) / 2),
102
+ top: v.maxY,
103
+ make: (z) => shapeBlock(v, frame, z)
104
+ })),
105
+ ...placed
106
+ ].sort((a, b) => compareOrder(a.key, b.key)).forEach((mark, z) => {
107
+ blocks.push({
108
+ col: mark.col,
109
+ top: mark.top,
110
+ el: mark.make(z)
111
+ });
58
112
  });
59
113
  blocks.sort((a, b) => a.col - b.col || b.top - a.top);
114
+ if (i > 0 && blocks.length > 0) body.push({
115
+ kind: "paragraph",
116
+ paragraph: {
117
+ properties: {
118
+ pageBreakBefore: true,
119
+ spacingLine: pt(0),
120
+ spacingLineRule: "exact"
121
+ },
122
+ runs: []
123
+ }
124
+ });
60
125
  for (const block of blocks) body.push(block.el);
61
126
  });
62
127
  return {
63
- doc: buildFlowDoc(body, resources, sectionFromPdfPages(pages)),
128
+ doc: buildFlowDoc(body, resources, sectionFromPdfPages(pages), collectEmbeddedFonts(file, pages)),
64
129
  losses: dedupeLosses(losses)
65
130
  };
66
131
  }
67
132
  function detectGutter(runs, pageWidth) {
68
133
  if (runs.length < 30 || pageWidth <= 0) return void 0;
69
134
  const fontSize = median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
70
- const right = (r) => r.x + Math.max(1, r.text.length) * (r.fontSizePt || fontSize) * .5;
71
- const intervals = runs.map((r) => [r.x, right(r)]).sort((a, b) => a[0] - b[0]);
135
+ const intervals = runs.map((r) => [r.x, Math.max(r.endX, r.x + 1)]).sort((a, b) => a[0] - b[0]);
72
136
  const minX = intervals[0][0];
73
137
  const span = Math.max(...intervals.map((iv) => iv[1])) - minX;
74
138
  if (span < pageWidth * .5) return void 0;
@@ -88,7 +152,96 @@ function detectGutter(runs, pageWidth) {
88
152
  if (left < runs.length * .25 || left > runs.length * .75) return void 0;
89
153
  return gapMid;
90
154
  }
91
- function groupIntoLines(runs) {
155
+ /**
156
+ * The gap, in ems, past which the placed reader cuts a line rather than write a
157
+ * space across it.
158
+ *
159
+ * It began at four ems, from a measurement on 160F-2019.pdf: the gaps between
160
+ * words there run 0.00–3.13 em and the gaps between COLUMNS 4.41–43.66 em, with
161
+ * nothing in between. That told a column from a word, which was the question at
162
+ * the time. It is not the question here.
163
+ *
164
+ * A placed piece is set down at the x it was measured at, so cutting costs
165
+ * nothing — and a space costs whatever the page's gap was not. A quarter of an
166
+ * em of type standing in for one em of pen leaves everything after it three
167
+ * quarters of an em short, and the error runs on down the line. So the reader
168
+ * cuts wherever {@link lineSpans} would otherwise write a space, and a placed
169
+ * page never stands a space in for a gap it measured. Only the placed reader
170
+ * does this — a flowing paragraph is meant to be read across.
171
+ */
172
+ var SPACE_GAP_EM = .25;
173
+ /** Runs by the direction of their baseline, upright first, each angle rounded. */
174
+ function byAngle(runs) {
175
+ const groups = /* @__PURE__ */ new Map();
176
+ for (const run of runs) {
177
+ const angle = Math.round(run.angleDeg ?? 0);
178
+ const group = groups.get(angle);
179
+ if (group) group.push(run);
180
+ else groups.set(angle, [run]);
181
+ }
182
+ return [...groups].sort((a, b) => Math.abs(a[0]) - Math.abs(b[0]));
183
+ }
184
+ /** The same runs seen from a frame turned by `deg`, where their baseline is flat. */
185
+ function rotate(runs, deg) {
186
+ if (deg === 0) return [...runs];
187
+ const rad = deg * Math.PI / 180;
188
+ const cos = Math.cos(rad);
189
+ const sin = Math.sin(rad);
190
+ return runs.map((r) => ({
191
+ ...r,
192
+ x: r.x * cos - r.y * sin,
193
+ y: r.x * sin + r.y * cos,
194
+ endX: r.endX * cos - r.endY * sin,
195
+ endY: r.endX * sin + r.endY * cos
196
+ }));
197
+ }
198
+ /**
199
+ * A turned line's page-space box: the rectangle the renderer is to place and
200
+ * then spin about its own centre, which is how a shape's rotation works
201
+ * (§20.1.7.6 `a:xfrm rot`).
202
+ *
203
+ * The box is measured in the line's own frame and only its CENTRE is carried
204
+ * back into page space — an axis-aligned box of the same size, centred there
205
+ * and turned, puts the words back along the baseline they were set on.
206
+ */
207
+ function turnedBox(line, angleDeg, pageWidth) {
208
+ const height = line.fontSize * 1.25;
209
+ const bottom = line.y - line.fontSize * .25;
210
+ const slack = line.width * 1.2 + line.fontSize;
211
+ const width = isRightToLeft(line.text) ? line.width : angleDeg === 0 ? Math.max(line.width, pageWidth - line.x) : slack;
212
+ if (angleDeg === 0) return {
213
+ x: line.x,
214
+ y: bottom,
215
+ width,
216
+ height
217
+ };
218
+ const rad = angleDeg * Math.PI / 180;
219
+ const cos = Math.cos(rad);
220
+ const sin = Math.sin(rad);
221
+ const cu = line.x + width / 2;
222
+ const cv = bottom + height / 2;
223
+ return {
224
+ x: cu * cos - cv * sin - width / 2,
225
+ y: cu * sin + cv * cos - height / 2,
226
+ width,
227
+ height
228
+ };
229
+ }
230
+ /** §20.1.7.6 — a shape turns CLOCKWISE in 1/60000°, and PDF measures the other way. */
231
+ function rotation60kOf(angleDeg) {
232
+ if (angleDeg === 0) return void 0;
233
+ return Math.round((-angleDeg % 360 + 360) % 360 * 6e4);
234
+ }
235
+ /**
236
+ * The step, in ems, past which a run does not share its neighbour's baseline
237
+ * but stands above or below it — a superscript, or the next row of a form.
238
+ *
239
+ * Measured across the readable pdfjs corpus: 123 runs sit exactly on their
240
+ * line's baseline, three within 0.02 em of it (rounding), one between 0.02 and
241
+ * 0.05, and thirty at 0.05 em or more. A twentieth of an em sits in that gap.
242
+ */
243
+ var BASELINE_STEP_EM = .05;
244
+ function groupIntoLines(runs, split = false) {
92
245
  const sorted = [...runs].sort((a, b) => b.y - a.y || a.x - b.x);
93
246
  const clusters = [];
94
247
  for (const run of sorted) {
@@ -103,28 +256,54 @@ function groupIntoLines(runs) {
103
256
  runs: [run]
104
257
  });
105
258
  }
106
- return clusters.map((c) => {
259
+ return clusters.flatMap((c) => {
107
260
  const ordered = c.runs.sort((a, b) => a.x - b.x);
108
261
  const fontSize = c.fontSize || 10;
109
- const spans = lineSpans(ordered, fontSize);
110
- return {
111
- y: c.y,
112
- fontSize,
113
- text: spans.map((s) => s.text).join("").replace(/\s+/g, " ").trim(),
114
- spans
115
- };
262
+ if (!split) return [lineOf(ordered, c.y, fontSize)];
263
+ const pieces = [[]];
264
+ for (const run of ordered) {
265
+ const prev = pieces[pieces.length - 1];
266
+ const last = prev[prev.length - 1];
267
+ const size = run.fontSizePt || fontSize;
268
+ if (last !== void 0 && (Math.abs(run.x - last.endX) > size * SPACE_GAP_EM || Math.abs(run.y - prev[0].y) > size * BASELINE_STEP_EM || isRightToLeft(run.text) || isRightToLeft(last.text))) pieces.push([]);
269
+ pieces[pieces.length - 1].push(run);
270
+ }
271
+ return pieces.map((piece) => lineOf(piece, piece[0].y, Math.max(...piece.map((r) => r.fontSizePt || 0)) || fontSize));
116
272
  });
117
273
  }
274
+ /** One run of runs, left to right on a shared baseline, as a {@link Line}. */
275
+ function lineOf(runs, y, fontSize) {
276
+ const ordered = lineSpans(runs, fontSize);
277
+ const spans = ordered.every((s) => s.text.trim() === "" || isRightToLeft(s.text)) ? [...ordered].reverse() : ordered;
278
+ const x = runs[0].x;
279
+ return {
280
+ x,
281
+ width: runs[runs.length - 1].endX - x,
282
+ y,
283
+ fontSize,
284
+ text: spans.map((s) => s.text).join("").replace(/\s+/g, " ").trim(),
285
+ spans
286
+ };
287
+ }
118
288
  function lineSpans(runs, fontSize) {
119
289
  const spans = [];
120
290
  let prevEnd;
121
291
  for (const run of runs) {
122
292
  if (prevEnd !== void 0 && run.x - prevEnd > fontSize * .25) spans.push({ text: " " });
123
- spans.push(run.href !== void 0 ? {
293
+ spans.push({
124
294
  text: run.text,
125
- href: run.href
126
- } : { text: run.text });
127
- prevEnd = run.x + run.text.length * (run.fontSizePt || fontSize) * .5;
295
+ sizePt: run.fontSizePt,
296
+ ...run.colorHex !== "000000" ? { colorHex: run.colorHex } : {},
297
+ ...run.fontName !== void 0 ? { fontName: run.fontName } : {},
298
+ ...run.outlineHex !== void 0 ? { outline: {
299
+ colorHex: run.outlineHex,
300
+ widthPt: pt(run.outlineWidthPt ?? 1)
301
+ } } : {},
302
+ ...run.bold ? { bold: true } : {},
303
+ ...run.italic ? { italic: true } : {},
304
+ ...run.href !== void 0 ? { href: run.href } : {}
305
+ });
306
+ prevEnd = run.endX;
128
307
  }
129
308
  return spans;
130
309
  }
@@ -152,5 +331,15 @@ function median(values) {
152
331
  const sorted = [...values].sort((a, b) => a - b);
153
332
  return sorted[Math.floor(sorted.length / 2)];
154
333
  }
334
+ /**
335
+ * Compare two painting-order keys (§8.5.3): position by position, and the
336
+ * shorter one first where they agree — a form's call comes before the marks it
337
+ * makes inside it.
338
+ */
339
+ function compareOrder(a, b) {
340
+ const n = Math.min(a.length, b.length);
341
+ for (let i = 0; i < n; i++) if (a[i] !== b[i]) return a[i] - b[i];
342
+ return a.length - b.length;
343
+ }
155
344
  //#endregion
156
345
  export { reconstructByLayout };
@@ -0,0 +1,17 @@
1
+ import { PdfDict } from '../pdf/objects.js';
2
+ import { PdfFile } from './document.js';
3
+ /** A tiling pattern read as a tint: the colour it paints, and how much of the cell. */
4
+ export interface PatternTint {
5
+ readonly colorHex: string;
6
+ /** 0–1 — the share of the cell the pattern's marks cover. */
7
+ readonly coverage: number;
8
+ }
9
+ /**
10
+ * Read a named tiling pattern as a tint.
11
+ *
12
+ * @param file The owning file.
13
+ * @param resources The resources the pattern is named in.
14
+ * @param name The pattern's resource name.
15
+ * @returns Its colour and coverage, or `undefined` when neither can be told.
16
+ */
17
+ export declare function patternTint(file: PdfFile, resources: PdfDict | undefined, name: string): PatternTint | undefined;
@@ -0,0 +1,181 @@
1
+ import { PDF_NULL, PdfStream } from "../pdf/objects.js";
2
+ import { interpretContent } from "./content.js";
3
+ import { buildFonts } from "./text.js";
4
+ //#region src/pdf-reader/pattern-tint.ts
5
+ /** How many points across the cell are asked. 48² is 2304 samples per mark. */
6
+ var GRID = 48;
7
+ /** How deep the walk follows a tile that draws its marks through Type 3 glyphs. */
8
+ var MAX_DEPTH = 4;
9
+ /**
10
+ * Read a named tiling pattern as a tint.
11
+ *
12
+ * @param file The owning file.
13
+ * @param resources The resources the pattern is named in.
14
+ * @param name The pattern's resource name.
15
+ * @returns Its colour and coverage, or `undefined` when neither can be told.
16
+ */
17
+ function patternTint(file, resources, name) {
18
+ if (!resources) return void 0;
19
+ const patterns = file.get(resources, "Pattern");
20
+ if (!(patterns instanceof Map)) return void 0;
21
+ const stream = file.resolve(patterns.get(name) ?? PDF_NULL);
22
+ if (!(stream instanceof PdfStream)) return void 0;
23
+ const cell = cellOf(file, stream.dict);
24
+ if (!cell) return void 0;
25
+ const marks = [];
26
+ const colours = [];
27
+ collect(file, stream, resources, marks, colours, 0, /* @__PURE__ */ new Set());
28
+ const colorHex = colours[0];
29
+ if (colorHex === void 0) return void 0;
30
+ return {
31
+ colorHex,
32
+ coverage: sample(marks, cell)
33
+ };
34
+ }
35
+ /** The cell a tile repeats on: `/XStep` × `/YStep` from the `/BBox`'s corner. */
36
+ function cellOf(file, dict) {
37
+ const bbox = numbers(file, dict.get("BBox"));
38
+ if (!bbox || bbox.length < 4) return void 0;
39
+ const x = Math.min(bbox[0], bbox[2]);
40
+ const y = Math.min(bbox[1], bbox[3]);
41
+ const bw = Math.abs(bbox[2] - bbox[0]);
42
+ const bh = Math.abs(bbox[3] - bbox[1]);
43
+ const w = Math.abs(asNumber(file.resolve(dict.get("XStep") ?? PDF_NULL))) || bw;
44
+ const h = Math.abs(asNumber(file.resolve(dict.get("YStep") ?? PDF_NULL))) || bh;
45
+ return w > 0 && h > 0 ? {
46
+ x,
47
+ y,
48
+ w,
49
+ h
50
+ } : void 0;
51
+ }
52
+ /** Every mark the tile paints, in pattern space, following its Type 3 glyphs. */
53
+ function collect(file, stream, fallback, marks, colours, depth, visiting) {
54
+ if (depth > MAX_DEPTH || visiting.has(stream)) return;
55
+ visiting.add(stream);
56
+ const own = file.get(stream.dict, "Resources");
57
+ const resources = own instanceof Map ? own : fallback;
58
+ const result = interpretContent(file.streamData(stream), buildFonts(file, resources));
59
+ for (const v of result.vectors) {
60
+ marks.push(v);
61
+ const hex = v.fillHex ?? v.strokeHex;
62
+ if (hex !== void 0 && hex !== "FFFFFF") colours.push(hex);
63
+ }
64
+ for (const run of result.texts) if (run.colorHex !== "FFFFFF") colours.push(run.colorHex);
65
+ for (const glyph of result.glyphs) {
66
+ const inner = interpretContent(file.streamData(glyph.stream), /* @__PURE__ */ new Map(), glyph.ctm);
67
+ for (const v of inner.vectors) {
68
+ marks.push(v);
69
+ const hex = v.fillHex ?? v.strokeHex;
70
+ if (hex !== void 0 && hex !== "FFFFFF") colours.push(hex);
71
+ }
72
+ }
73
+ visiting.delete(stream);
74
+ }
75
+ /** The share of the cell any mark covers, asked at a grid of points. */
76
+ function sample(marks, cell) {
77
+ if (marks.length === 0) return 0;
78
+ const flat = marks.map((m) => ({
79
+ lines: flatten(m.segs),
80
+ filled: m.fillHex !== void 0 || m.gradient !== void 0,
81
+ half: m.strokeHex !== void 0 ? Math.max(m.lineWidth ?? 1, .1) / 2 : 0
82
+ }));
83
+ let hit = 0;
84
+ for (let iy = 0; iy < GRID; iy++) {
85
+ const py = cell.y + (iy + .5) / GRID * cell.h;
86
+ for (let ix = 0; ix < GRID; ix++) {
87
+ const px = cell.x + (ix + .5) / GRID * cell.w;
88
+ if (flat.some((m) => covers(m, px, py))) hit++;
89
+ }
90
+ }
91
+ return hit / (GRID * GRID);
92
+ }
93
+ /** Whether one mark paints the point — inside its outline, or under its pen. */
94
+ function covers(mark, px, py) {
95
+ if (mark.half > 0) {
96
+ const limit = mark.half * mark.half;
97
+ for (const [x0, y0, x1, y1] of mark.lines) if (distanceSquared(px, py, x0, y0, x1, y1) <= limit) return true;
98
+ }
99
+ if (!mark.filled) return false;
100
+ let inside = false;
101
+ for (const [x0, y0, x1, y1] of mark.lines) if (y0 > py !== y1 > py && px < (x1 - x0) * (py - y0) / (y1 - y0) + x0) inside = !inside;
102
+ return inside;
103
+ }
104
+ /** The squared distance from a point to a segment. */
105
+ function distanceSquared(px, py, x0, y0, x1, y1) {
106
+ const dx = x1 - x0;
107
+ const dy = y1 - y0;
108
+ const len = dx * dx + dy * dy;
109
+ const t = len > 0 ? Math.max(0, Math.min(1, ((px - x0) * dx + (py - y0) * dy) / len)) : 0;
110
+ const qx = x0 + t * dx - px;
111
+ const qy = y0 + t * dy - py;
112
+ return qx * qx + qy * qy;
113
+ }
114
+ /** A path as straight segments; a curve becomes the chords of its control net. */
115
+ function flatten(segs) {
116
+ const out = [];
117
+ let sx = 0;
118
+ let sy = 0;
119
+ let cx = 0;
120
+ let cy = 0;
121
+ for (const seg of segs) switch (seg.op) {
122
+ case "move":
123
+ sx = cx = seg.x;
124
+ sy = cy = seg.y;
125
+ break;
126
+ case "line":
127
+ out.push([
128
+ cx,
129
+ cy,
130
+ seg.x,
131
+ seg.y
132
+ ]);
133
+ cx = seg.x;
134
+ cy = seg.y;
135
+ break;
136
+ case "cubic": {
137
+ const steps = 8;
138
+ let lx = cx;
139
+ let ly = cy;
140
+ for (let i = 1; i <= steps; i++) {
141
+ const t = i / steps;
142
+ const u = 1 - t;
143
+ const x = u * u * u * cx + 3 * u * u * t * seg.x1 + 3 * u * t * t * seg.x2 + t * t * t * seg.x;
144
+ const y = u * u * u * cy + 3 * u * u * t * seg.y1 + 3 * u * t * t * seg.y2 + t * t * t * seg.y;
145
+ out.push([
146
+ lx,
147
+ ly,
148
+ x,
149
+ y
150
+ ]);
151
+ lx = x;
152
+ ly = y;
153
+ }
154
+ cx = seg.x;
155
+ cy = seg.y;
156
+ break;
157
+ }
158
+ case "close":
159
+ out.push([
160
+ cx,
161
+ cy,
162
+ sx,
163
+ sy
164
+ ]);
165
+ cx = sx;
166
+ cy = sy;
167
+ break;
168
+ }
169
+ return out;
170
+ }
171
+ function numbers(file, value) {
172
+ const v = file.resolve(value ?? PDF_NULL);
173
+ if (!Array.isArray(v)) return void 0;
174
+ const out = v.map((n) => asNumber(file.resolve(n)));
175
+ return out.some((n) => !Number.isFinite(n)) ? void 0 : out;
176
+ }
177
+ function asNumber(v) {
178
+ return typeof v === "number" ? v : NaN;
179
+ }
180
+ //#endregion
181
+ export { patternTint };
@@ -1,3 +1,4 @@
1
+ import { StreamFilters } from './document.js';
1
2
  import { DocumentReader, ReadResult } from '../core/ir/adapters.js';
2
3
  import { FlowDoc } from '../core/ir/flow.js';
3
4
  /**
@@ -10,9 +11,16 @@ import { FlowDoc } from '../core/ir/flow.js';
10
11
  * @param bytes The complete PDF file bytes.
11
12
  * @param password The user password for an encrypted source; the empty string
12
13
  * opens permissions-only encryption.
14
+ * @param filters Decoders for `/Filter` names this reader does not implement
15
+ * (§7.4); see {@link StreamFilters}.
16
+ * @param layout `'flow'` reads a re-flowable document out of the page —
17
+ * paragraphs and tables in reading order, from the structure
18
+ * tree where there is one. `'positional'` keeps the page: every
19
+ * line stands where its glyphs do, beside the artwork, which is
20
+ * what a form or a drawing needs and what a paragraph cannot be.
13
21
  * @returns The reconstructed FlowDoc and its accumulated {@link Loss} report.
14
22
  */
15
- export declare function readPdf(bytes: Uint8Array, password?: string): ReadResult<FlowDoc>;
23
+ export declare function readPdf(bytes: Uint8Array, password?: string, layout?: 'flow' | 'positional', filters?: StreamFilters): ReadResult<FlowDoc>;
16
24
  /**
17
25
  * The `pdfReader` adapter: a {@link DocumentReader} that sniffs the `%PDF-`
18
26
  * header and parses the bytes into a {@link FlowDoc} (E-PDF EP5).
@@ -18,19 +18,31 @@ function sniffPdf(bytes) {
18
18
  * @param bytes The complete PDF file bytes.
19
19
  * @param password The user password for an encrypted source; the empty string
20
20
  * opens permissions-only encryption.
21
+ * @param filters Decoders for `/Filter` names this reader does not implement
22
+ * (§7.4); see {@link StreamFilters}.
23
+ * @param layout `'flow'` reads a re-flowable document out of the page —
24
+ * paragraphs and tables in reading order, from the structure
25
+ * tree where there is one. `'positional'` keeps the page: every
26
+ * line stands where its glyphs do, beside the artwork, which is
27
+ * what a form or a drawing needs and what a paragraph cannot be.
21
28
  * @returns The reconstructed FlowDoc and its accumulated {@link Loss} report.
22
29
  */
23
- function readPdf(bytes, password = "") {
24
- const file = PdfFile.parse(bytes, password);
30
+ function readPdf(bytes, password = "", layout = "flow", filters = {}) {
31
+ const file = PdfFile.parse(bytes, password, filters);
25
32
  const losses = [];
26
33
  if (file.encryptionUnsupported) losses.push({
27
34
  severity: "dropped",
28
35
  feature: FEATURES.text,
29
36
  detail: "encrypted PDF — the user password was missing or incorrect, or the handler is unsupported"
30
37
  });
31
- const tagged = reconstructTaggedPdf(file);
32
- const reconstruction = tagged ?? reconstructByLayout(file);
33
- if (!tagged) losses.push({
38
+ for (const name of file.unknownFilters) losses.push({
39
+ severity: "dropped",
40
+ feature: FEATURES.text,
41
+ detail: `PDF stream filter /${name} is not supported; streams using it are unreadable`
42
+ });
43
+ const tagged = layout === "positional" ? void 0 : reconstructTaggedPdf(file);
44
+ const reconstruction = tagged ?? reconstructByLayout(file, layout);
45
+ if (!tagged && layout !== "positional") losses.push({
34
46
  severity: "degraded",
35
47
  feature: FEATURES.text,
36
48
  detail: "untagged PDF — text and headings reconstructed heuristically from glyph positions; structure is approximate"
@@ -60,7 +72,11 @@ var pdfReader = {
60
72
  FEATURES.images
61
73
  ]),
62
74
  sniff: sniffPdf,
63
- read: (bytes, opts) => readPdf(bytes, typeof opts?.password === "string" ? opts.password : "")
75
+ read: (bytes, opts) => readPdf(bytes, typeof opts?.password === "string" ? opts.password : "", opts?.pdfLayout === "positional" ? "positional" : "flow", isFilters(opts?.filters) ? opts.filters : {})
64
76
  };
77
+ /** A caller's `filters` option, when it is the shape the reader can use. */
78
+ function isFilters(value) {
79
+ return typeof value === "object" && value !== null;
80
+ }
65
81
  //#endregion
66
82
  export { pdfReader };
@@ -11,3 +11,17 @@ import { PdfFile, PdfPage } from './document.js';
11
11
  * @returns A map from pattern resource name to its {@link ShapeGradient}.
12
12
  */
13
13
  export declare function buildShadingMap(file: PdfFile, page: PdfPage): Map<string, ShapeGradient>;
14
+ /**
15
+ * §11.6.4.4 — the constant fill alpha (`/ca`) of every `/ExtGState` the page
16
+ * names, by name.
17
+ *
18
+ * `gs` sets a whole graphics state at once, and one of the things in it is how
19
+ * opaque the paint is. 22060_A1_01_Plans.pdf marks its evacuation routes with
20
+ * a green band at `ca` 0.6, meant to be read THROUGH: painted solid, the floor
21
+ * plan under each band disappears.
22
+ *
23
+ * @param file The owning file.
24
+ * @param page The page whose `/ExtGState` resources are wanted.
25
+ * @returns Name → fill alpha, for the states that state one below 1.
26
+ */
27
+ export declare function buildAlphaMap(file: PdfFile, page: PdfPage): Map<string, number>;
@@ -153,5 +153,31 @@ function pushStop(stops, offset, colorHex) {
153
153
  colorHex
154
154
  });
155
155
  }
156
+ /**
157
+ * §11.6.4.4 — the constant fill alpha (`/ca`) of every `/ExtGState` the page
158
+ * names, by name.
159
+ *
160
+ * `gs` sets a whole graphics state at once, and one of the things in it is how
161
+ * opaque the paint is. 22060_A1_01_Plans.pdf marks its evacuation routes with
162
+ * a green band at `ca` 0.6, meant to be read THROUGH: painted solid, the floor
163
+ * plan under each band disappears.
164
+ *
165
+ * @param file The owning file.
166
+ * @param page The page whose `/ExtGState` resources are wanted.
167
+ * @returns Name → fill alpha, for the states that state one below 1.
168
+ */
169
+ function buildAlphaMap(file, page) {
170
+ const out = /* @__PURE__ */ new Map();
171
+ if (!page.resources) return out;
172
+ const states = file.get(page.resources, "ExtGState");
173
+ if (!(states instanceof Map)) return out;
174
+ for (const [name, value] of states) {
175
+ const state = file.resolve(value);
176
+ if (!(state instanceof Map)) continue;
177
+ const ca = file.resolve(state.get("ca") ?? PDF_NULL);
178
+ if (typeof ca === "number" && ca >= 0 && ca < 1) out.set(name, ca);
179
+ }
180
+ return out;
181
+ }
156
182
  //#endregion
157
- export { buildShadingMap };
183
+ export { buildAlphaMap, buildShadingMap };