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.
- package/README.md +22 -11
- package/dist/esm/core/converter/facade.d.ts +3 -3
- package/dist/esm/core/converter/facade.js +12 -0
- package/dist/esm/core/converter/ream.d.ts +48 -4
- package/dist/esm/core/converter/ream.js +25 -4
- package/dist/esm/core/document-model/index.d.ts +1 -1
- package/dist/esm/core/document-model/types.d.ts +17 -0
- package/dist/esm/core/outline.d.ts +17 -0
- package/dist/esm/core/outline.js +30 -0
- package/dist/esm/core/style-cascade/resolver.js +1 -0
- package/dist/esm/core/style-cascade/types.d.ts +3 -1
- package/dist/esm/excel/sheet-to-flow.d.ts +10 -0
- package/dist/esm/excel/sheet-to-flow.js +14 -1
- package/dist/esm/html/html-writer.js +3 -2
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/layout/page-doc.js +1 -1
- package/dist/esm/layout/styled-layout.js +41 -15
- package/dist/esm/markdown/markdown-writer.d.ts +41 -0
- package/dist/esm/markdown/markdown-writer.js +733 -0
- package/dist/esm/pdf/styled-page-emitter.js +20 -1
- package/dist/esm/pdf-reader/annots.d.ts +24 -0
- package/dist/esm/pdf-reader/annots.js +126 -0
- package/dist/esm/pdf-reader/content.d.ts +131 -5
- package/dist/esm/pdf-reader/content.js +169 -12
- package/dist/esm/pdf-reader/display.d.ts +56 -0
- package/dist/esm/pdf-reader/display.js +162 -0
- package/dist/esm/pdf-reader/document.d.ts +36 -1
- package/dist/esm/pdf-reader/document.js +92 -25
- package/dist/esm/pdf-reader/embedded-fonts.d.ts +31 -0
- package/dist/esm/pdf-reader/embedded-fonts.js +94 -0
- package/dist/esm/pdf-reader/flow-build.d.ts +61 -6
- package/dist/esm/pdf-reader/flow-build.js +128 -22
- package/dist/esm/pdf-reader/font.js +185 -4
- package/dist/esm/pdf-reader/image-decode.js +55 -4
- package/dist/esm/pdf-reader/images.d.ts +6 -0
- package/dist/esm/pdf-reader/images.js +25 -5
- package/dist/esm/pdf-reader/jpeg.d.ts +18 -0
- package/dist/esm/pdf-reader/jpeg.js +419 -0
- package/dist/esm/pdf-reader/layout.d.ts +1 -1
- package/dist/esm/pdf-reader/layout.js +221 -32
- package/dist/esm/pdf-reader/pattern-tint.d.ts +17 -0
- package/dist/esm/pdf-reader/pattern-tint.js +181 -0
- package/dist/esm/pdf-reader/reader.d.ts +9 -1
- package/dist/esm/pdf-reader/reader.js +23 -7
- package/dist/esm/pdf-reader/shading.d.ts +14 -0
- package/dist/esm/pdf-reader/shading.js +27 -1
- package/dist/esm/pdf-reader/tagged.js +156 -17
- package/dist/esm/pdf-reader/text.d.ts +13 -1
- package/dist/esm/pdf-reader/text.js +70 -3
- package/dist/esm/pdf-reader/vector.d.ts +25 -1
- package/dist/esm/pdf-reader/vector.js +168 -12
- package/dist/esm/pptx/slide-parser.js +5 -0
- package/dist/esm/word/docx-writer.js +79 -17
- package/dist/esm/word/drawing-parser.js +7 -1
- 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 {
|
|
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
|
|
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
|
|
32
|
-
const
|
|
42
|
+
const display = shown[i];
|
|
43
|
+
const pageWidth = display.width;
|
|
44
|
+
const gutter = detectGutter(runs, pageWidth);
|
|
33
45
|
const blocks = [];
|
|
34
|
-
const addColumn = (
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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(
|
|
293
|
+
spans.push({
|
|
124
294
|
text: run.text,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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"
|
|
@@ -39,7 +51,7 @@ function readPdf(bytes, password = "") {
|
|
|
39
51
|
losses.push({
|
|
40
52
|
severity: "dropped",
|
|
41
53
|
feature: FEATURES.images,
|
|
42
|
-
detail: "PDF
|
|
54
|
+
detail: "PDF bare-shading (sh) vector regions are not reconstructed"
|
|
43
55
|
});
|
|
44
56
|
return {
|
|
45
57
|
doc: reconstruction.doc,
|
|
@@ -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 };
|