reamkit 1.28.0 → 1.29.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.
- package/dist/esm/core/converter/ream.d.ts +8 -0
- package/dist/esm/pdf-reader/cmap.js +11 -4
- package/dist/esm/pdf-reader/content.d.ts +15 -0
- package/dist/esm/pdf-reader/content.js +14 -1
- package/dist/esm/pdf-reader/document.d.ts +8 -0
- package/dist/esm/pdf-reader/flow-build.d.ts +7 -2
- package/dist/esm/pdf-reader/flow-build.js +11 -4
- package/dist/esm/pdf-reader/layout.js +400 -27
- package/dist/esm/pdf-reader/tagged.js +1 -1
- package/dist/esm/pdf-reader/vector.d.ts +2 -0
- package/dist/esm/pdf-reader/vector.js +1 -0
- package/dist/esm/word/docx-writer.js +27 -11
- package/package.json +1 -1
|
@@ -29,6 +29,14 @@ export interface ReamParseOptions {
|
|
|
29
29
|
* carrying one for every filter anyone might write:
|
|
30
30
|
*
|
|
31
31
|
* ```ts
|
|
32
|
+
* // browser: any wasm/JS decoder you ship — brotli-dec-wasm is ~200 KB
|
|
33
|
+
* import brotliPromise from 'brotli-dec-wasm';
|
|
34
|
+
* const brotli = await brotliPromise;
|
|
35
|
+
* Ream.parse(pdf, { filters: { BrotliDecode: (b) => brotli.decompress(b) } });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* // node, where the runtime already carries one
|
|
32
40
|
* import { brotliDecompressSync } from 'node:zlib';
|
|
33
41
|
* Ream.parse(pdf, { filters: { BrotliDecode: (b) => brotliDecompressSync(b) } });
|
|
34
42
|
* ```
|
|
@@ -32,7 +32,8 @@ function parseToUnicodeCMap(bytes) {
|
|
|
32
32
|
if (src.kind !== "hexstr") continue;
|
|
33
33
|
const dst = lexer.nextToken();
|
|
34
34
|
if (dst.kind !== "hexstr") continue;
|
|
35
|
-
|
|
35
|
+
const text = stated(utf16be(dst.bytes));
|
|
36
|
+
if (text !== void 0) map.set(bytesToInt(src.bytes), text);
|
|
36
37
|
}
|
|
37
38
|
else if (tok.value === "beginbfrange") for (;;) {
|
|
38
39
|
const lo = lexer.nextToken();
|
|
@@ -44,14 +45,17 @@ function parseToUnicodeCMap(bytes) {
|
|
|
44
45
|
const hiN = bytesToInt(hi.bytes);
|
|
45
46
|
const dst = lexer.nextToken();
|
|
46
47
|
if (dst.kind === "hexstr") {
|
|
47
|
-
const base = utf16be(dst.bytes);
|
|
48
|
-
for (let i = 0; loN + i <= hiN && i < MAX_RANGE; i++) map.set(loN + i, incString(base, i));
|
|
48
|
+
const base = stated(utf16be(dst.bytes));
|
|
49
|
+
if (base !== void 0) for (let i = 0; loN + i <= hiN && i < MAX_RANGE; i++) map.set(loN + i, incString(base, i));
|
|
49
50
|
} else if (dst.kind === "arrayOpen") {
|
|
50
51
|
let i = 0;
|
|
51
52
|
for (;;) {
|
|
52
53
|
const el = lexer.nextToken();
|
|
53
54
|
if (el.kind === "arrayClose" || el.kind === "eof") break;
|
|
54
|
-
if (el.kind
|
|
55
|
+
if (el.kind !== "hexstr") continue;
|
|
56
|
+
const text = stated(utf16be(el.bytes));
|
|
57
|
+
if (text !== void 0) map.set(loN + i, text);
|
|
58
|
+
i++;
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
}
|
|
@@ -61,6 +65,9 @@ function parseToUnicodeCMap(bytes) {
|
|
|
61
65
|
codeBytes
|
|
62
66
|
};
|
|
63
67
|
}
|
|
68
|
+
function stated(text) {
|
|
69
|
+
for (const ch of text) if (ch !== "\0") return text;
|
|
70
|
+
}
|
|
64
71
|
function bytesToInt(bytes) {
|
|
65
72
|
let n = 0;
|
|
66
73
|
for (const b of bytes) n = n * 256 + b;
|
|
@@ -328,7 +328,22 @@ export interface Type3Call {
|
|
|
328
328
|
readonly ctm: Matrix;
|
|
329
329
|
/** §8.5.3 — its place in the stream's painting order, as a form call has. */
|
|
330
330
|
readonly order: number;
|
|
331
|
+
/**
|
|
332
|
+
* §9.6.5/§9.10 — the font SAYS what character this glyph stands for. A
|
|
333
|
+
* flowing reading re-sets that character in a face of its own, so drawing the
|
|
334
|
+
* procedure as well puts two copies of the letter on the page.
|
|
335
|
+
*/
|
|
336
|
+
readonly readable: boolean;
|
|
331
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* §9.10 — whether the font says what character a glyph stands for: text that is
|
|
340
|
+
* neither empty nor the replacement character a reader writes where nothing is
|
|
341
|
+
* stated.
|
|
342
|
+
*
|
|
343
|
+
* @param text What the font's decode gave for the code.
|
|
344
|
+
* @returns Whether a flowing reading can write it as a letter.
|
|
345
|
+
*/
|
|
346
|
+
export declare function statesACharacter(text: string): boolean;
|
|
332
347
|
/**
|
|
333
348
|
* 2D affine matrix `[a b c d e f]`, row-vector convention (`[x y 1] · M`):
|
|
334
349
|
* ```
|
|
@@ -35,6 +35,18 @@ function pathBox(segs) {
|
|
|
35
35
|
}
|
|
36
36
|
/** The area of a box, for choosing the smaller of two clip regions. */
|
|
37
37
|
var area = (b) => Math.max(0, b.maxX - b.minX) * Math.max(0, b.maxY - b.minY);
|
|
38
|
+
/**
|
|
39
|
+
* §9.10 — whether the font says what character a glyph stands for: text that is
|
|
40
|
+
* neither empty nor the replacement character a reader writes where nothing is
|
|
41
|
+
* stated.
|
|
42
|
+
*
|
|
43
|
+
* @param text What the font's decode gave for the code.
|
|
44
|
+
* @returns Whether a flowing reading can write it as a letter.
|
|
45
|
+
*/
|
|
46
|
+
function statesACharacter(text) {
|
|
47
|
+
const trimmed = text.trim();
|
|
48
|
+
return trimmed.length > 0 && !trimmed.includes("�");
|
|
49
|
+
}
|
|
38
50
|
/** The identity {@link Matrix} (no transform). */
|
|
39
51
|
var IDENTITY = [
|
|
40
52
|
1,
|
|
@@ -284,7 +296,8 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
|
|
|
284
296
|
stream,
|
|
285
297
|
resources: type3.resources,
|
|
286
298
|
ctm: multiply(type3.matrix, multiply(scale, multiply(tm, state.ctm))),
|
|
287
|
-
order: paintOrder
|
|
299
|
+
order: paintOrder++,
|
|
300
|
+
readable: statesACharacter(state.font.decode([code]))
|
|
288
301
|
});
|
|
289
302
|
}
|
|
290
303
|
}
|
|
@@ -110,6 +110,14 @@ export declare class PdfFile {
|
|
|
110
110
|
* reader takes one from whoever needs it:
|
|
111
111
|
*
|
|
112
112
|
* ```ts
|
|
113
|
+
* // browser: any wasm/JS decoder you ship — brotli-dec-wasm is ~200 KB
|
|
114
|
+
* import brotliPromise from 'brotli-dec-wasm';
|
|
115
|
+
* const brotli = await brotliPromise;
|
|
116
|
+
* Ream.parse(pdf, { filters: { BrotliDecode: (b) => brotli.decompress(b) } });
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* ```ts
|
|
120
|
+
* // node, where the runtime already carries one
|
|
113
121
|
* import { brotliDecompressSync } from 'node:zlib';
|
|
114
122
|
* Ream.parse(pdf, { filters: { BrotliDecode: (b) => brotliDecompressSync(b) } });
|
|
115
123
|
* ```
|
|
@@ -66,7 +66,7 @@ export interface TextSpan {
|
|
|
66
66
|
* survives as its own run) and squashing whitespace. With no hrefs this
|
|
67
67
|
* collapses to a single run — the same shape {@link paragraphBlock} produces.
|
|
68
68
|
*/
|
|
69
|
-
export declare function paragraphFromRuns(spans: ReadonlyArray<TextSpan>, outlineLevel?: number, placement?: Pick<ParagraphProperties, 'alignment' | 'spacingBefore' | 'indentLeft' | 'indentFirstLine'>): BodyElement;
|
|
69
|
+
export declare function paragraphFromRuns(spans: ReadonlyArray<TextSpan>, outlineLevel?: number, placement?: Pick<ParagraphProperties, 'alignment' | 'spacingBefore' | 'indentLeft' | 'indentFirstLine' | 'tabs'>): BodyElement;
|
|
70
70
|
/**
|
|
71
71
|
* Store a {@link PdfImage}'s bytes (content-addressed dedup) and build the image
|
|
72
72
|
* {@link BodyElement} that references them, sized in points from the placement
|
|
@@ -126,7 +126,12 @@ export declare function sectionFromPdfPages(pages: ReadonlyArray<PdfPage>): Sect
|
|
|
126
126
|
export declare function withMeasuredMargins(section: SectionProperties | undefined, shown: ReadonlyArray<{
|
|
127
127
|
width: number;
|
|
128
128
|
height: number;
|
|
129
|
-
}>, pageRuns: ReadonlyArray<ReadonlyArray<TextRun
|
|
129
|
+
}>, pageRuns: ReadonlyArray<ReadonlyArray<TextRun>>, pageMarks?: ReadonlyArray<ReadonlyArray<{
|
|
130
|
+
x: number;
|
|
131
|
+
y: number;
|
|
132
|
+
widthPt: number;
|
|
133
|
+
heightPt: number;
|
|
134
|
+
}>>): SectionProperties | undefined;
|
|
130
135
|
/**
|
|
131
136
|
* Assemble the final {@link FlowDoc} for a reconstruction: the body elements
|
|
132
137
|
* with their styles resolved against the empty style sheet, the lifted-image
|
|
@@ -333,7 +333,7 @@ var DEEPEST_MARGIN = .5;
|
|
|
333
333
|
var ASCENDER = .8;
|
|
334
334
|
/** And its descender below — the two together are a little over one em. */
|
|
335
335
|
var DESCENDER = .22;
|
|
336
|
-
function withMeasuredMargins(section, shown, pageRuns) {
|
|
336
|
+
function withMeasuredMargins(section, shown, pageRuns, pageMarks = []) {
|
|
337
337
|
if (!section?.pageSize) return section;
|
|
338
338
|
const width = section.pageSize.width;
|
|
339
339
|
const height = section.pageSize.height;
|
|
@@ -352,8 +352,10 @@ function withMeasuredMargins(section, shown, pageRuns) {
|
|
|
352
352
|
let bottomSize = 0;
|
|
353
353
|
for (const r of runs) {
|
|
354
354
|
if (!Number.isFinite(r.x) || !Number.isFinite(r.y)) continue;
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
if (r.text.trim() !== "") {
|
|
356
|
+
minX = Math.min(minX, r.x);
|
|
357
|
+
maxX = Math.max(maxX, r.endX);
|
|
358
|
+
}
|
|
357
359
|
if (r.y < minY) {
|
|
358
360
|
minY = r.y;
|
|
359
361
|
bottomSize = r.fontSizePt;
|
|
@@ -363,6 +365,11 @@ function withMeasuredMargins(section, shown, pageRuns) {
|
|
|
363
365
|
topSize = r.fontSizePt;
|
|
364
366
|
}
|
|
365
367
|
}
|
|
368
|
+
for (const mark of pageMarks[i] ?? []) {
|
|
369
|
+
if (!Number.isFinite(mark.x) || !Number.isFinite(mark.y)) continue;
|
|
370
|
+
maxX = Math.max(maxX, mark.x + mark.widthPt);
|
|
371
|
+
minY = Math.min(minY, mark.y);
|
|
372
|
+
}
|
|
366
373
|
if (!Number.isFinite(minX) || !Number.isFinite(minY)) return;
|
|
367
374
|
lefts.push(minX);
|
|
368
375
|
rights.push(page.width - maxX);
|
|
@@ -378,7 +385,7 @@ function withMeasuredMargins(section, shown, pageRuns) {
|
|
|
378
385
|
return {
|
|
379
386
|
...section,
|
|
380
387
|
margins: {
|
|
381
|
-
left: clamp(
|
|
388
|
+
left: clamp(Math.min(...lefts), width),
|
|
382
389
|
right: clamp(median(rights) - width * SLACK, width),
|
|
383
390
|
top: clamp(Math.min(...tops), height, DEEPEST_MARGIN),
|
|
384
391
|
bottom: clamp(Math.min(...bottoms) - height * SLACK, height, DEEPEST_MARGIN)
|
|
@@ -36,6 +36,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
36
36
|
const foot = mode === "positional" ? void 0 : runningFoot(allRuns, shown, "foot");
|
|
37
37
|
const head = mode === "positional" ? void 0 : runningFoot(allRuns, shown, "head");
|
|
38
38
|
const pageRuns = foot || head ? allRuns.map((runs, i) => runs.filter((r) => foot?.lift[i]?.has(r) !== true && head?.lift[i]?.has(r) !== true)) : allRuns;
|
|
39
|
+
const pageMarks = pages.map(() => []);
|
|
39
40
|
const medianFont = median(pageRuns.flat().map((r) => r.fontSizePt).filter((s) => s > 0)) || 12;
|
|
40
41
|
const resources = new ResourceStore();
|
|
41
42
|
const losses = [];
|
|
@@ -71,12 +72,13 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
71
72
|
const pageWidth = display.width;
|
|
72
73
|
const stepped = stepsBetweenWords(runs);
|
|
73
74
|
const blocks = [];
|
|
74
|
-
const split = gutters.length > 0 ? assignColumns(runs, gutters) : void 0;
|
|
75
75
|
const textEdges = pageTextEdges(runs);
|
|
76
|
+
const inColumns = proseColumns(runs, gutters, textEdges);
|
|
77
|
+
const split = gutters.length > 0 && inColumns ? assignColumns(runs, gutters) : void 0;
|
|
76
78
|
const bandEpsilon = (median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10) / 2;
|
|
77
79
|
const bandAt = (top) => bandOf(split?.breaks ?? [], top, bandEpsilon);
|
|
78
80
|
const addColumn = (allRuns, col) => {
|
|
79
|
-
const colRuns =
|
|
81
|
+
const colRuns = allRuns.filter((r) => r.type3 !== true && (mode !== "positional" || r.invisible !== true));
|
|
80
82
|
if (mode === "positional") {
|
|
81
83
|
for (const [angle, runs] of byAngle(colRuns)) for (const line of groupIntoLines(rotate(runs, -angle), true, stepped)) {
|
|
82
84
|
if (line.text.length === 0) continue;
|
|
@@ -121,17 +123,38 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
121
123
|
}
|
|
122
124
|
});
|
|
123
125
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
el: paragraphFromRuns(para.spans, headingLevel(para.fontSize, medianFont), {
|
|
129
|
-
...para.alignment !== void 0 ? { alignment: para.alignment } : {},
|
|
130
|
-
...para.spacingBefore !== void 0 ? { spacingBefore: pt(para.spacingBefore) } : {},
|
|
131
|
-
...para.indentLeft !== void 0 ? { indentLeft: pt(para.indentLeft) } : {},
|
|
132
|
-
...para.indentFirstLine !== void 0 ? { indentFirstLine: pt(para.indentFirstLine) } : {}
|
|
133
|
-
})
|
|
126
|
+
const paras = groupIntoParagraphs(lines, measure, display.height);
|
|
127
|
+
const asRows = tabbedRows(paras, measure && {
|
|
128
|
+
left: measure.left,
|
|
129
|
+
right: Math.max(measure.right, textEdges?.right ?? measure.right)
|
|
134
130
|
});
|
|
131
|
+
for (const [at, para] of paras.entries()) {
|
|
132
|
+
const table = asRows.get(at);
|
|
133
|
+
if (table !== void 0) {
|
|
134
|
+
if (table !== null) blocks.push({
|
|
135
|
+
band: bandAt(para.top),
|
|
136
|
+
col,
|
|
137
|
+
top: para.top,
|
|
138
|
+
el: table
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
blocks.push({
|
|
143
|
+
band: bandAt(para.top),
|
|
144
|
+
col,
|
|
145
|
+
top: para.top,
|
|
146
|
+
el: paragraphFromRuns(para.spans, headingLevel(para.fontSize, medianFont), {
|
|
147
|
+
...para.stops !== void 0 && para.stops.length > 0 ? { tabs: para.stops.filter((x) => x > 0).map((x) => ({
|
|
148
|
+
positionPt: pt(x),
|
|
149
|
+
alignment: "left"
|
|
150
|
+
})) } : {},
|
|
151
|
+
...para.alignment !== void 0 ? { alignment: para.alignment } : {},
|
|
152
|
+
...para.spacingBefore !== void 0 ? { spacingBefore: pt(para.spacingBefore) } : {},
|
|
153
|
+
...para.indentLeft !== void 0 ? { indentLeft: pt(para.indentLeft) } : {},
|
|
154
|
+
...para.indentFirstLine !== void 0 ? { indentFirstLine: pt(para.indentFirstLine) } : {}
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
}
|
|
135
158
|
};
|
|
136
159
|
const placed = [];
|
|
137
160
|
const colOf = (centerX) => gutters.filter((g) => centerX >= g.mid).length;
|
|
@@ -145,6 +168,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
145
168
|
losses: raw.losses
|
|
146
169
|
};
|
|
147
170
|
losses.push(...imgs.losses);
|
|
171
|
+
pageMarks[i] = imgs.images;
|
|
148
172
|
const lifted = collectPageVectors(file, page, imgs.images.map((img) => ({
|
|
149
173
|
minX: img.x,
|
|
150
174
|
minY: img.y,
|
|
@@ -154,7 +178,8 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
154
178
|
losses.push(...lifted.losses);
|
|
155
179
|
const placedVectors = placeVectors(lifted.vectors, display);
|
|
156
180
|
const ruled = markDrawnRules(runs, placedVectors);
|
|
157
|
-
const
|
|
181
|
+
const strayGlyphs = strayMarks(placedVectors, runs);
|
|
182
|
+
const vectors = placedVectors.filter((v) => !ruled.consumed.has(v)).filter((v) => !(mode !== "positional" && strayGlyphs.has(v)));
|
|
158
183
|
const ruledIntoColumns = mode !== "positional" && looksRuled(ruled.runs, gutters, textEdges);
|
|
159
184
|
const asTable = ruledIntoColumns && textEdges ? tableFrom(ruled.runs, gutters, textEdges, stepped) : void 0;
|
|
160
185
|
if (asTable) for (const block of asTable) blocks.push({
|
|
@@ -169,6 +194,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
169
194
|
for (const col of [SPANNING_COLUMN, ...columns]) addColumn(ruled.runs.filter((r) => columnFor(r) === col), col);
|
|
170
195
|
} else addColumn(ruled.runs, 0);
|
|
171
196
|
const under = mode !== "positional";
|
|
197
|
+
const givenAway = mode !== "positional" ? ruleBorders(vectors, blocks, display.width) : void 0;
|
|
172
198
|
[
|
|
173
199
|
...imgs.images.map((img) => ({
|
|
174
200
|
key: img.orderKey,
|
|
@@ -176,7 +202,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
176
202
|
top: img.y + img.heightPt,
|
|
177
203
|
make: (z) => imageBlock(img, resources, void 0, frame, z, under)
|
|
178
204
|
})),
|
|
179
|
-
...vectors.map((v) => ({
|
|
205
|
+
...vectors.filter((v) => givenAway?.has(v) !== true).map((v) => ({
|
|
180
206
|
key: v.orderKey,
|
|
181
207
|
col: colOf((v.minX + v.maxX) / 2),
|
|
182
208
|
top: v.maxY,
|
|
@@ -218,7 +244,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
218
244
|
runs: []
|
|
219
245
|
}
|
|
220
246
|
});
|
|
221
|
-
const columnsHere = ruledIntoColumns ? 1 : gutters.length + 1;
|
|
247
|
+
const columnsHere = ruledIntoColumns || !proseColumns(runs, gutters, textEdges) ? 1 : gutters.length + 1;
|
|
222
248
|
const spacePt = columnsHere > 1 ? median(gutters.map((g) => g.to - g.from)) : 0;
|
|
223
249
|
for (const block of blocks) {
|
|
224
250
|
const count = block.col === SPANNING_COLUMN ? 1 : columnsHere;
|
|
@@ -241,7 +267,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
241
267
|
});
|
|
242
268
|
const setUp = (from, to) => {
|
|
243
269
|
const own = sectionFromPdfPages(pages.slice(from, to));
|
|
244
|
-
return mode === "positional" ? own : withMeasuredMargins(own, shown.slice(from, to), pageRuns.slice(from, to));
|
|
270
|
+
return mode === "positional" ? own : withMeasuredMargins(own, shown.slice(from, to), pageRuns.slice(from, to), pageMarks.slice(from, to));
|
|
245
271
|
};
|
|
246
272
|
sectionEnds.push({
|
|
247
273
|
at: body.length,
|
|
@@ -302,9 +328,9 @@ function detectGutters(runs, pageWidth) {
|
|
|
302
328
|
if (span < pageWidth * .5) return [];
|
|
303
329
|
const sorted = [...spans].sort((a, b) => a[0] - b[0]);
|
|
304
330
|
let curEnd = sorted[0][1];
|
|
305
|
-
const
|
|
331
|
+
const bands = [];
|
|
306
332
|
for (const [l, r] of sorted) {
|
|
307
|
-
if (l - curEnd >= fontSize * 3)
|
|
333
|
+
if (l - curEnd >= fontSize * 3) bands.push({
|
|
308
334
|
mid: (curEnd + l) / 2,
|
|
309
335
|
from: curEnd,
|
|
310
336
|
to: l
|
|
@@ -315,6 +341,17 @@ function detectGutters(runs, pageWidth) {
|
|
|
315
341
|
const ink = runInk(r);
|
|
316
342
|
return ink ? [ink] : [];
|
|
317
343
|
}).sort((a, b) => a[0] - b[0]));
|
|
344
|
+
const straddling = (band) => {
|
|
345
|
+
let n = 0;
|
|
346
|
+
for (const row of rows) {
|
|
347
|
+
const before = row.filter(([, r]) => r <= band.from);
|
|
348
|
+
const after = row.filter(([l]) => l >= band.to);
|
|
349
|
+
if (before.length === 0 || after.length === 0) continue;
|
|
350
|
+
if (Math.min(...after.map(([l]) => l)) - Math.max(...before.map(([, r]) => r)) >= fontSize * MIN_GUTTER_EM) n++;
|
|
351
|
+
}
|
|
352
|
+
return n;
|
|
353
|
+
};
|
|
354
|
+
const empty = bands.filter((b) => straddling(b) >= MIN_EMPTY_BAND_ROWS);
|
|
318
355
|
const voted = [];
|
|
319
356
|
for (const x of sample(minX + span * .1, minX + span * .9, 1)) {
|
|
320
357
|
let columned = 0;
|
|
@@ -442,6 +479,13 @@ var MIN_SHARED_ROWS = 3;
|
|
|
442
479
|
/** How many lines have to be split at the same place before the page is in columns. */
|
|
443
480
|
var MIN_COLUMNED_ROWS = 12;
|
|
444
481
|
/**
|
|
482
|
+
* How many lines a band NO run crosses must separate before it is a gutter
|
|
483
|
+
* rather than an empty half of a page. Lower than the vote's bar, because a
|
|
484
|
+
* band nothing crosses is stronger evidence than a band most lines avoid — but
|
|
485
|
+
* not zero, which is what an invoice's blank left half offers.
|
|
486
|
+
*/
|
|
487
|
+
var MIN_EMPTY_BAND_ROWS = 8;
|
|
488
|
+
/**
|
|
445
489
|
* The page's runs grouped by baseline.
|
|
446
490
|
*
|
|
447
491
|
* Swept in order rather than bucketed: a superscript sits a few points above
|
|
@@ -781,7 +825,7 @@ function inkSpan(runs) {
|
|
|
781
825
|
}
|
|
782
826
|
/** One run of runs, left to right on a shared baseline, as a {@link Line}. */
|
|
783
827
|
function lineOf(runs, y, fontSize, stepped) {
|
|
784
|
-
const ordered = lineSpans(runs, fontSize, stepped || runs.length > 1 && !runs.some((r) => SPACE.test(r.text)));
|
|
828
|
+
const { spans: ordered, stops } = lineSpans(runs, fontSize, stepped || runs.length > 1 && !runs.some((r) => SPACE.test(r.text)));
|
|
785
829
|
const spans = ordered.every((s) => s.text.trim() === "" || isRightToLeft(s.text)) ? [...ordered].reverse() : ordered;
|
|
786
830
|
const ink = inkSpan(runs);
|
|
787
831
|
return {
|
|
@@ -790,6 +834,7 @@ function lineOf(runs, y, fontSize, stepped) {
|
|
|
790
834
|
y,
|
|
791
835
|
fontSize,
|
|
792
836
|
...tabbed(runs, fontSize) ? { tabbed: true } : {},
|
|
837
|
+
...stops.length > 0 ? { stops } : {},
|
|
793
838
|
text: spans.map((s) => s.text).join("").replace(/\s+/g, " ").trim(),
|
|
794
839
|
spans
|
|
795
840
|
};
|
|
@@ -887,11 +932,15 @@ function stepsBetweenWords(runs) {
|
|
|
887
932
|
}
|
|
888
933
|
function lineSpans(runs, fontSize, stepped) {
|
|
889
934
|
const spans = [];
|
|
935
|
+
const stops = [];
|
|
890
936
|
const body = runs.filter((r) => (r.fontSizePt || fontSize) > fontSize * SCRIPT_SIZE);
|
|
891
937
|
const baseline = median((body.length > 0 ? body : runs).map((r) => r.y));
|
|
892
938
|
let prev;
|
|
893
939
|
for (const run of runs) {
|
|
894
|
-
if (prev !== void 0 && run.x - prev.endX > spaceGap(prev, fontSize, stepped) && !(isLeader(prev.text) && prev.text === run.text))
|
|
940
|
+
if (prev !== void 0 && run.x - prev.endX > spaceGap(prev, fontSize, stepped) && !(isLeader(prev.text) && prev.text === run.text)) if (run.x - prev.endX >= (run.fontSizePt || fontSize) * TAB_GAP_EM) {
|
|
941
|
+
spans.push({ text: " " });
|
|
942
|
+
stops.push(run.x);
|
|
943
|
+
} else spans.push({ text: " " });
|
|
895
944
|
spans.push({
|
|
896
945
|
text: run.text.replaceAll("�", ""),
|
|
897
946
|
sizePt: run.fontSizePt,
|
|
@@ -909,7 +958,10 @@ function lineSpans(runs, fontSize, stepped) {
|
|
|
909
958
|
});
|
|
910
959
|
prev = run;
|
|
911
960
|
}
|
|
912
|
-
return
|
|
961
|
+
return {
|
|
962
|
+
spans,
|
|
963
|
+
stops
|
|
964
|
+
};
|
|
913
965
|
}
|
|
914
966
|
/**
|
|
915
967
|
* §17.3.2.42 — a run set off the line's baseline, and smaller, as the script it
|
|
@@ -960,6 +1012,24 @@ function carriesLeader(line) {
|
|
|
960
1012
|
}
|
|
961
1013
|
/** Three of the same leader character in a row is a leader and not punctuation. */
|
|
962
1014
|
var LEADER_RUN = /([.\u00b7_])\1{2,}/u;
|
|
1015
|
+
/**
|
|
1016
|
+
* Whether the line is a RULE drawn out of characters — a row of hyphens, dots
|
|
1017
|
+
* or underscores and nothing else.
|
|
1018
|
+
*
|
|
1019
|
+
* A page with no rule to draw types one. It is not a sentence, it does not run
|
|
1020
|
+
* on into the line under it and the line over it does not run on into it.
|
|
1021
|
+
*
|
|
1022
|
+
* @param line The line.
|
|
1023
|
+
* @returns Whether it is a rule rather than words.
|
|
1024
|
+
*/
|
|
1025
|
+
function ruleOfCharacters(line) {
|
|
1026
|
+
const text = line.text.trim();
|
|
1027
|
+
if (text.length < RULE_CHARS) return false;
|
|
1028
|
+
for (const ch of text) if (!LEADER_CHARS.has(ch)) return false;
|
|
1029
|
+
return true;
|
|
1030
|
+
}
|
|
1031
|
+
/** How many of them it takes before a row of marks is a rule. */
|
|
1032
|
+
var RULE_CHARS = 3;
|
|
963
1033
|
function groupIntoParagraphs(lines, column, pageHeight = 0) {
|
|
964
1034
|
const groups = [];
|
|
965
1035
|
const gaps = [];
|
|
@@ -967,7 +1037,7 @@ function groupIntoParagraphs(lines, column, pageHeight = 0) {
|
|
|
967
1037
|
for (const line of lines) {
|
|
968
1038
|
const gap = prev !== void 0 ? prev.y - line.y : 0;
|
|
969
1039
|
const opened = prev !== void 0 && gap > line.fontSize * 1.5;
|
|
970
|
-
if (groups.length === 0 || opened || prev !== void 0 && (endedParagraph(prev, line, column) || carriesLeader(prev) || prev.tabbed === true)) {
|
|
1040
|
+
if (groups.length === 0 || opened || ruleOfCharacters(line) || prev !== void 0 && (ruleOfCharacters(prev) || endedParagraph(prev, line, column) || carriesLeader(prev) || prev.tabbed === true)) {
|
|
971
1041
|
groups.push([]);
|
|
972
1042
|
gaps.push(prev === void 0 ? 0 : gap);
|
|
973
1043
|
}
|
|
@@ -982,10 +1052,12 @@ function groupIntoParagraphs(lines, column, pageHeight = 0) {
|
|
|
982
1052
|
const opened = (gaps[i] ?? 0) - fontSize * 1.2;
|
|
983
1053
|
const most = pageHeight > 0 ? pageHeight / 3 : fontSize * 3;
|
|
984
1054
|
const spacingBefore = opened > fontSize * .3 ? Math.min(opened, most) : void 0;
|
|
1055
|
+
const stops = g.length === 1 ? first.stops ?? [] : [];
|
|
985
1056
|
return {
|
|
986
1057
|
spans: joinLines(g),
|
|
987
1058
|
fontSize,
|
|
988
1059
|
top: first.y,
|
|
1060
|
+
...stops.length > 0 ? { stops: stops.map((x) => x - (column?.left ?? columnLeft)) } : {},
|
|
989
1061
|
...spacingBefore !== void 0 ? { spacingBefore } : {},
|
|
990
1062
|
...alignmentOf(g, column),
|
|
991
1063
|
...indentOf(g, columnLeft, alignmentOf(g, column).alignment)
|
|
@@ -1204,11 +1276,21 @@ function edgeLine(runs, pageHeight, where) {
|
|
|
1204
1276
|
if (runs.length < 4 || pageHeight <= 0) return void 0;
|
|
1205
1277
|
const fontSize = median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
|
|
1206
1278
|
const rows = rowsOf(runs, fontSize);
|
|
1207
|
-
if (rows.length
|
|
1279
|
+
if (rows.length === 0) return void 0;
|
|
1208
1280
|
const at = (i) => where === "foot" ? rows[rows.length - 1 - i] : rows[i];
|
|
1209
1281
|
const edgeY = (row) => where === "foot" ? Math.max(...row.map((r) => r.y)) : Math.min(...row.map((r) => r.y));
|
|
1210
1282
|
const inside = (row) => where === "foot" ? Math.min(...row.map((r) => r.y)) : Math.max(...row.map((r) => r.y));
|
|
1211
1283
|
const y = edgeY(at(0));
|
|
1284
|
+
if (rows.length < 4) {
|
|
1285
|
+
const inked = rows.filter((row) => row.some((r) => r.text.trim() !== ""));
|
|
1286
|
+
const inBand = inked.length > 0 && inked.every((row) => where === "foot" ? edgeY(row) <= pageHeight * FOOT_BAND : edgeY(row) >= pageHeight * (1 - FOOT_BAND));
|
|
1287
|
+
const all = inked.flat();
|
|
1288
|
+
const text = all.map((r) => r.text).join("").trim();
|
|
1289
|
+
return inBand && text.length > 0 && text.length <= FOOT_CHARS * inked.length ? {
|
|
1290
|
+
y: edgeY(inked[inked.length - 1] ?? []),
|
|
1291
|
+
runs: all
|
|
1292
|
+
} : void 0;
|
|
1293
|
+
}
|
|
1212
1294
|
let found;
|
|
1213
1295
|
let group = [];
|
|
1214
1296
|
const most = where === "foot" ? FOOT_ROWS : 1;
|
|
@@ -1252,6 +1334,25 @@ function pageNumbered(run) {
|
|
|
1252
1334
|
const number = found[2];
|
|
1253
1335
|
const before = run.text.slice(0, at);
|
|
1254
1336
|
const after = run.text.slice(at + number.length);
|
|
1337
|
+
const total = /(^|\s)(\d{1,4})(\s|$)/u.exec(after);
|
|
1338
|
+
const tail = total === null ? after === "" ? [] : [{
|
|
1339
|
+
...run,
|
|
1340
|
+
text: after
|
|
1341
|
+
}] : [
|
|
1342
|
+
...after.slice(0, total.index + total[1].length) === "" ? [] : [{
|
|
1343
|
+
...run,
|
|
1344
|
+
text: after.slice(0, total.index + total[1].length)
|
|
1345
|
+
}],
|
|
1346
|
+
{
|
|
1347
|
+
...run,
|
|
1348
|
+
text: total[2],
|
|
1349
|
+
field: "NUMPAGES"
|
|
1350
|
+
},
|
|
1351
|
+
...after.slice(total.index + total[1].length + total[2].length) === "" ? [] : [{
|
|
1352
|
+
...run,
|
|
1353
|
+
text: after.slice(total.index + total[1].length + total[2].length)
|
|
1354
|
+
}]
|
|
1355
|
+
];
|
|
1255
1356
|
return [
|
|
1256
1357
|
...before === "" ? [] : [{
|
|
1257
1358
|
...run,
|
|
@@ -1262,10 +1363,7 @@ function pageNumbered(run) {
|
|
|
1262
1363
|
text: number,
|
|
1263
1364
|
field: "PAGE"
|
|
1264
1365
|
},
|
|
1265
|
-
...
|
|
1266
|
-
...run,
|
|
1267
|
-
text: after
|
|
1268
|
-
}]
|
|
1366
|
+
...tail
|
|
1269
1367
|
];
|
|
1270
1368
|
}
|
|
1271
1369
|
/**
|
|
@@ -1404,6 +1502,281 @@ function looksRuled(runs, gutters, edges) {
|
|
|
1404
1502
|
return (fills[Math.floor(fills.length / 2)] ?? 1) <= TABLE_CELL_FILL;
|
|
1405
1503
|
}
|
|
1406
1504
|
/**
|
|
1505
|
+
* Whether the regions the gutters cut the page into are COLUMNS OF PROSE — the
|
|
1506
|
+
* only thing worth re-setting a document in columns for.
|
|
1507
|
+
*
|
|
1508
|
+
* A gutter is evidence about the page's white space, and white space is not
|
|
1509
|
+
* enough: an invoice sets its labels along the left and its amounts against the
|
|
1510
|
+
* right margin, and a dozen lines then break at the same x. Read as two columns
|
|
1511
|
+
* the amounts became a column of their own and every label was indented past
|
|
1512
|
+
* the strip it was given — "Total excluding tax" came back one letter per line.
|
|
1513
|
+
*
|
|
1514
|
+
* What tells them apart is the same thing that tells a cell from a line of
|
|
1515
|
+
* prose: a column of prose FILLS its measure, over and over, because that is
|
|
1516
|
+
* what wrapping does. A column of values never fills anything — its lines are
|
|
1517
|
+
* short and it is their right edges that line up, not their left. So every
|
|
1518
|
+
* region must hold lines of its own and they must reach across it.
|
|
1519
|
+
*
|
|
1520
|
+
* @param runs The page's runs.
|
|
1521
|
+
* @param gutters The page's gutters.
|
|
1522
|
+
* @param edges Where the page's text starts and ends.
|
|
1523
|
+
* @returns Whether the page is set in columns of prose.
|
|
1524
|
+
*/
|
|
1525
|
+
function proseColumns(runs, gutters, edges) {
|
|
1526
|
+
if (gutters.length === 0) return false;
|
|
1527
|
+
if (!edges) return true;
|
|
1528
|
+
const rows = rowsOf(runs, median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10).filter((row) => row.length > 0);
|
|
1529
|
+
if (rows.length === 0) return true;
|
|
1530
|
+
const bounds = columnBounds(runs, gutters, edges);
|
|
1531
|
+
const regions = bounds.slice(0, -1).map((lo, i) => [lo, bounds[i + 1]]);
|
|
1532
|
+
const regionOf = (x) => {
|
|
1533
|
+
for (let i = regions.length - 1; i >= 0; i--) if (x >= regions[i][0]) return i;
|
|
1534
|
+
return 0;
|
|
1535
|
+
};
|
|
1536
|
+
const edgesIn = regions.map(() => []);
|
|
1537
|
+
for (const row of rows) {
|
|
1538
|
+
const byRegion = regions.map(() => []);
|
|
1539
|
+
for (const run of row) byRegion[regionOf(run.x)].push(run);
|
|
1540
|
+
byRegion.forEach((inRegion, i) => {
|
|
1541
|
+
if (inRegion.length === 0) return;
|
|
1542
|
+
edgesIn[i].push({
|
|
1543
|
+
left: Math.min(...inRegion.map((r) => r.x)),
|
|
1544
|
+
right: Math.max(...inRegion.map((r) => r.endX))
|
|
1545
|
+
});
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
const agreeing = (xs) => {
|
|
1549
|
+
let most = 0;
|
|
1550
|
+
for (const x of xs) most = Math.max(most, xs.filter((y) => Math.abs(y - x) <= FLUSH_PT).length);
|
|
1551
|
+
return most;
|
|
1552
|
+
};
|
|
1553
|
+
return edgesIn.every((lines) => {
|
|
1554
|
+
if (lines.length < MIN_PROSE_LINES) return false;
|
|
1555
|
+
return agreeing(lines.map((l) => l.right)) <= agreeing(lines.map((l) => l.left));
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* The drawn glyphs that are STRAY marks in a line of ordinary text.
|
|
1560
|
+
*
|
|
1561
|
+
* §9.6.6 — a glyph the file states no character for is drawn, because nothing
|
|
1562
|
+
* downstream can write it. That is the whole content of some pages: a subset
|
|
1563
|
+
* that names its glyphs `g18`, a program with no `cmap`, a font of pictures.
|
|
1564
|
+
* On such a page the drawings ARE the words and they stay.
|
|
1565
|
+
*
|
|
1566
|
+
* A single mark among readable words is the other case, and Stripe's invoices
|
|
1567
|
+
* are full of them — a `/ToUnicode` stating U+0000 for every piece of
|
|
1568
|
+
* punctuation it sets. The flowing reading has nowhere to put a shape inside a
|
|
1569
|
+
* line, so the colon of "Kazakhstan VAT: 86-1696045" was placed as a floating
|
|
1570
|
+
* drawing and landed a word away, on a line of its own, pushing the text
|
|
1571
|
+
* around it aside. Dropped, the line keeps its words and the loss report still
|
|
1572
|
+
* says a character was unrecoverable.
|
|
1573
|
+
*
|
|
1574
|
+
* The line decides: where nearly everything on it is drawn, the drawings are
|
|
1575
|
+
* the text — bug1151216.pdf sets three lines of prices that way. Where one or
|
|
1576
|
+
* two marks stand among several readable runs, they are punctuation.
|
|
1577
|
+
*
|
|
1578
|
+
* @param vectors The page's painted paths.
|
|
1579
|
+
* @param runs The page's runs.
|
|
1580
|
+
* @returns The drawn glyphs to leave out of a flowing reading.
|
|
1581
|
+
*/
|
|
1582
|
+
function strayMarks(vectors, runs) {
|
|
1583
|
+
const glyphs = vectors.filter((v) => v.glyph === true);
|
|
1584
|
+
const out = /* @__PURE__ */ new Set();
|
|
1585
|
+
if (glyphs.length === 0) return out;
|
|
1586
|
+
const readable = runs.filter((r) => r.text.trim() !== "" && !r.text.includes("�"));
|
|
1587
|
+
for (const v of glyphs) {
|
|
1588
|
+
const mid = (v.minY + v.maxY) / 2;
|
|
1589
|
+
const size = v.maxY - v.minY || 10;
|
|
1590
|
+
const words = readable.filter((r) => Math.abs(r.y - mid) <= size);
|
|
1591
|
+
const before = words.some((r) => r.endX <= v.minX + size);
|
|
1592
|
+
const after = words.some((r) => r.x >= v.maxX - size);
|
|
1593
|
+
if (!before && !after) continue;
|
|
1594
|
+
if (glyphs.filter((g) => Math.abs((g.minY + g.maxY) / 2 - mid) <= size).length <= MOST_STRAY_MARKS && words.length >= LEAST_READABLE_RUNS) out.add(v);
|
|
1595
|
+
}
|
|
1596
|
+
return out;
|
|
1597
|
+
}
|
|
1598
|
+
/** How few drawn glyphs a line may hold before they are its text, not marks in it. */
|
|
1599
|
+
var MOST_STRAY_MARKS = 2;
|
|
1600
|
+
/** …and how many readable runs must stand on that line for them to be strays. */
|
|
1601
|
+
var LEAST_READABLE_RUNS = 4;
|
|
1602
|
+
/**
|
|
1603
|
+
* §17.4.38 — the consecutive lines a page set out on the SAME stops, as the
|
|
1604
|
+
* TABLE they are.
|
|
1605
|
+
*
|
|
1606
|
+
* A line broken at a gap no word space explains is a line the page laid out
|
|
1607
|
+
* (see {@link tabbed}); several of them one under another, broken in the same
|
|
1608
|
+
* places, is a table's head and its rows. An invoice's item table is exactly
|
|
1609
|
+
* that, and so is the payment history under it.
|
|
1610
|
+
*
|
|
1611
|
+
* Two stops at least, which is three columns: ONE stop is a contents entry with
|
|
1612
|
+
* its page number at the measure, and a list of those is a list.
|
|
1613
|
+
*
|
|
1614
|
+
* The stops do not have to agree to the point — a heading sits over its column
|
|
1615
|
+
* and a figure is set against the far side of it — so they are matched within
|
|
1616
|
+
* an em, and the column is put where the rows agree it is.
|
|
1617
|
+
*
|
|
1618
|
+
* @param paras The column's paragraphs, in order.
|
|
1619
|
+
* @param measure The measure they are set across.
|
|
1620
|
+
* @returns For each paragraph that belongs to a table: the table at its first
|
|
1621
|
+
* row, and `null` at the rows after it (their content is inside it).
|
|
1622
|
+
*/
|
|
1623
|
+
function tabbedRows(paras, measure) {
|
|
1624
|
+
const out = /* @__PURE__ */ new Map();
|
|
1625
|
+
if (!measure) return out;
|
|
1626
|
+
for (let i = 0; i < paras.length;) {
|
|
1627
|
+
const stops = paras[i]?.stops ?? [];
|
|
1628
|
+
if (stops.length < LEAST_TABLE_STOPS) {
|
|
1629
|
+
i++;
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
const near = (a, b) => a.length === b.length && a.every((x, k) => Math.abs(x - b[k]) <= Math.max(paras[i].fontSize, TABLE_STOP_SLACK_PT));
|
|
1633
|
+
let to = i + 1;
|
|
1634
|
+
while (to < paras.length && near(stops, paras[to]?.stops ?? [])) to++;
|
|
1635
|
+
if (to - i < 2) {
|
|
1636
|
+
i++;
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
const rows = paras.slice(i, to);
|
|
1640
|
+
const bounds = stops.map((_, k) => Math.min(...rows.map((r) => r.stops[k])));
|
|
1641
|
+
const width = measure.right - measure.left;
|
|
1642
|
+
const edges = [
|
|
1643
|
+
0,
|
|
1644
|
+
...bounds,
|
|
1645
|
+
width
|
|
1646
|
+
];
|
|
1647
|
+
out.set(i, {
|
|
1648
|
+
kind: "table",
|
|
1649
|
+
table: {
|
|
1650
|
+
properties: {
|
|
1651
|
+
defaultCellMargins: {
|
|
1652
|
+
left: pt(0),
|
|
1653
|
+
right: pt(0)
|
|
1654
|
+
},
|
|
1655
|
+
layout: "fixed"
|
|
1656
|
+
},
|
|
1657
|
+
grid: edges.slice(0, -1).map((from, k) => pt(Math.max(edges[k + 1] - from, 1))),
|
|
1658
|
+
rows: rows.map((row, r) => {
|
|
1659
|
+
const prev = rows[r - 1];
|
|
1660
|
+
const pitch = prev ? prev.top - row.top - row.fontSize * 1.2 : 0;
|
|
1661
|
+
const opening = r === 0 ? row.spacingBefore : pitch > row.fontSize * .3 ? pitch : void 0;
|
|
1662
|
+
return {
|
|
1663
|
+
properties: {},
|
|
1664
|
+
cells: splitAtTabs(row.spans).map((cell) => ({
|
|
1665
|
+
properties: {},
|
|
1666
|
+
content: [paragraphFromRuns(cell, void 0, opening !== void 0 ? { spacingBefore: pt(opening) } : {})]
|
|
1667
|
+
}))
|
|
1668
|
+
};
|
|
1669
|
+
})
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
for (let k = i + 1; k < to; k++) out.set(k, null);
|
|
1673
|
+
i = to;
|
|
1674
|
+
}
|
|
1675
|
+
return out;
|
|
1676
|
+
}
|
|
1677
|
+
/** How many stops a line must stand on before a run of them is a table. */
|
|
1678
|
+
var LEAST_TABLE_STOPS = 2;
|
|
1679
|
+
/**
|
|
1680
|
+
* How far two rows' stops may stand apart and still be one column.
|
|
1681
|
+
*
|
|
1682
|
+
* A heading is set over its column and a figure against the far side of it: an
|
|
1683
|
+
* invoice's "Qty" stands eight points left of the "1" under it, and the heading
|
|
1684
|
+
* is set two sizes smaller, so its own em is not a wide enough tolerance.
|
|
1685
|
+
*/
|
|
1686
|
+
var TABLE_STOP_SLACK_PT = 10;
|
|
1687
|
+
/** One line's spans cut into its cells, at the tabs the page set out. */
|
|
1688
|
+
function splitAtTabs(spans) {
|
|
1689
|
+
const cells = [[]];
|
|
1690
|
+
for (const span of spans) if (span.text === " ") cells.push([]);
|
|
1691
|
+
else cells[cells.length - 1].push(span);
|
|
1692
|
+
return cells;
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Give each separator rule to the paragraph it separates, as that paragraph's
|
|
1696
|
+
* own border (§17.3.1.24), and say which rules were given away.
|
|
1697
|
+
*
|
|
1698
|
+
* A rule sits in the white between two blocks: under a table's headings, over a
|
|
1699
|
+
* total. The block BELOW it takes it as a top border, because that is the block
|
|
1700
|
+
* the rule introduces; where nothing follows closely enough, the block above
|
|
1701
|
+
* takes it as a bottom one.
|
|
1702
|
+
*
|
|
1703
|
+
* @param vectors The page's painted paths.
|
|
1704
|
+
* @param blocks The blocks read off the page so far, which the rule joins.
|
|
1705
|
+
* @param width The page's width, which a rule is long relative to.
|
|
1706
|
+
* @returns The rules that became borders, and so must not be drawn again.
|
|
1707
|
+
*/
|
|
1708
|
+
function ruleBorders(vectors, blocks, width) {
|
|
1709
|
+
const given = /* @__PURE__ */ new Set();
|
|
1710
|
+
const paragraphs = blocks.filter((b) => b.el.kind === "paragraph");
|
|
1711
|
+
if (paragraphs.length === 0) return given;
|
|
1712
|
+
for (const v of vectors) {
|
|
1713
|
+
if (!separatorRule(v, width)) continue;
|
|
1714
|
+
const y = (v.minY + v.maxY) / 2;
|
|
1715
|
+
const below = paragraphs.filter((b) => b.top < y).sort((a, b) => b.top - a.top)[0];
|
|
1716
|
+
const above = paragraphs.filter((b) => b.top >= y).sort((a, b) => a.top - b.top)[0];
|
|
1717
|
+
const side = below !== void 0 && y - below.top <= RULE_REACH_PT ? {
|
|
1718
|
+
block: below,
|
|
1719
|
+
edge: "top"
|
|
1720
|
+
} : above !== void 0 && above.top - y <= RULE_REACH_PT ? {
|
|
1721
|
+
block: above,
|
|
1722
|
+
edge: "bottom"
|
|
1723
|
+
} : void 0;
|
|
1724
|
+
if (!side || side.block.el.kind !== "paragraph") continue;
|
|
1725
|
+
const border = {
|
|
1726
|
+
style: "single",
|
|
1727
|
+
width: pt(Math.max(v.lineWidth ?? v.maxY - v.minY, RULE_MIN_PT)),
|
|
1728
|
+
colorHex: v.strokeHex ?? v.fillHex ?? "000000"
|
|
1729
|
+
};
|
|
1730
|
+
const { paragraph } = side.block.el;
|
|
1731
|
+
side.block.el = {
|
|
1732
|
+
kind: "paragraph",
|
|
1733
|
+
paragraph: {
|
|
1734
|
+
...paragraph,
|
|
1735
|
+
properties: {
|
|
1736
|
+
...paragraph.properties,
|
|
1737
|
+
borders: {
|
|
1738
|
+
...paragraph.properties.borders,
|
|
1739
|
+
[side.edge]: border
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
given.add(v);
|
|
1745
|
+
}
|
|
1746
|
+
return given;
|
|
1747
|
+
}
|
|
1748
|
+
/** How far from a paragraph a rule may stand and still belong to it. */
|
|
1749
|
+
var RULE_REACH_PT = 14;
|
|
1750
|
+
/** The thinnest a border may be drawn and still be seen. */
|
|
1751
|
+
var RULE_MIN_PT = .5;
|
|
1752
|
+
/**
|
|
1753
|
+
* Whether a painted path is a RULE — a line drawn to separate one block from
|
|
1754
|
+
* the next, rather than a piece of the page's artwork.
|
|
1755
|
+
*
|
|
1756
|
+
* Flat and long: a hairline the width of a column under a table's headings, or
|
|
1757
|
+
* the line a total is written over. Everything else — boxes, panels, drawings —
|
|
1758
|
+
* keeps the place on the page it was drawn at.
|
|
1759
|
+
*
|
|
1760
|
+
* @param v The painted path.
|
|
1761
|
+
* @param width The page's width, which the rule is long RELATIVE to.
|
|
1762
|
+
* @returns Whether it separates blocks rather than drawing something.
|
|
1763
|
+
*/
|
|
1764
|
+
function separatorRule(v, width) {
|
|
1765
|
+
const w = v.maxX - v.minX;
|
|
1766
|
+
const h = v.maxY - v.minY;
|
|
1767
|
+
return h <= RULE_THICK_PT && w >= width * RULE_SHARE && w > h * RULE_RATIO;
|
|
1768
|
+
}
|
|
1769
|
+
/** How thick a mark may be and still be a rule. */
|
|
1770
|
+
var RULE_THICK_PT = 2;
|
|
1771
|
+
/** How much of the sheet it must run across. */
|
|
1772
|
+
var RULE_SHARE = .2;
|
|
1773
|
+
/** …and how much longer than it is thick. */
|
|
1774
|
+
var RULE_RATIO = 20;
|
|
1775
|
+
/** How close two edges stand before they count as the same one. */
|
|
1776
|
+
var FLUSH_PT = 1;
|
|
1777
|
+
/** And how many lines it takes before a region is a column at all. */
|
|
1778
|
+
var MIN_PROSE_LINES = 6;
|
|
1779
|
+
/**
|
|
1407
1780
|
* §17.4.38 — the page's rows as the TABLE they are.
|
|
1408
1781
|
*
|
|
1409
1782
|
* The regions between the gutters are the columns and the rows are the rows;
|
|
@@ -323,7 +323,7 @@ function reconstructTaggedPdf(file) {
|
|
|
323
323
|
const reached = [...claimed].reduce((n, r) => n + r.text.length, 0);
|
|
324
324
|
if (onPage > 0 && reached * 2 < onPage) return void 0;
|
|
325
325
|
return {
|
|
326
|
-
doc: buildFlowDoc(body, resources, withMeasuredMargins(sectionFromPdfPages(pages), shown, placedRuns), collectEmbeddedFonts(file, pages, imageLosses)),
|
|
326
|
+
doc: buildFlowDoc(body, resources, withMeasuredMargins(sectionFromPdfPages(pages), shown, placedRuns, pageImages.map((p) => p.images)), collectEmbeddedFonts(file, pages, imageLosses)),
|
|
327
327
|
losses: imageLosses
|
|
328
328
|
};
|
|
329
329
|
}
|
|
@@ -37,6 +37,8 @@ export interface PdfVector {
|
|
|
37
37
|
readonly minY: number;
|
|
38
38
|
readonly maxX: number;
|
|
39
39
|
readonly maxY: number;
|
|
40
|
+
/** §9.6.6 — a traced glyph rather than artwork: type the file states no character for. */
|
|
41
|
+
readonly glyph?: boolean;
|
|
40
42
|
readonly mcid?: number;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
@@ -482,7 +482,7 @@ function chartBlockXml(chart, state, scope, losses) {
|
|
|
482
482
|
const cx = Math.round(chart.width * EMU_PER_PT);
|
|
483
483
|
const cy = Math.round(chart.height * EMU_PER_PT);
|
|
484
484
|
const id = ++state.drawingSeq;
|
|
485
|
-
return `<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${id}" name="Chart ${id}"${chart.altText ? ` descr="${escapeAttr(chart.altText)}"` : ""}/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="${relId}"/></a:graphicData></a:graphic></wp:inline></w:drawing>`;
|
|
485
|
+
return `<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${id}" name="Chart ${id}"${chart.altText ? ` descr="${escapeAttr(chart.altText)}"` : ""}/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="${relId}"/></a:graphicData></a:graphic></wp:inline></w:drawing>`;
|
|
486
486
|
}
|
|
487
487
|
/**
|
|
488
488
|
* §20.4.2.3 — the frame a drawing is placed by: `wp:anchor` where the block
|
|
@@ -510,7 +510,7 @@ function chartBlockXml(chart, state, scope, losses) {
|
|
|
510
510
|
*/
|
|
511
511
|
function drawingFrame(float, cx, cy, head, graphic, state, rotation60k) {
|
|
512
512
|
const WP_NS = " xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\"";
|
|
513
|
-
if (!float) return `<w:drawing><wp:inline${WP_NS}><wp:extent cx="${cx}" cy="${cy}"/>${head}${graphic}</wp:inline></w:drawing>`;
|
|
513
|
+
if (!float) return `<w:drawing><wp:inline${WP_NS} distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${cx}" cy="${cy}"/>${head}${graphic}</wp:inline></w:drawing>`;
|
|
514
514
|
const emu = (pt) => Number.isFinite(pt) ? Math.round((pt ?? 0) * EMU_PER_PT) : 0;
|
|
515
515
|
const d = float.wrapDist;
|
|
516
516
|
const dist = ` distT="${String(emu(d?.topPt))}" distB="${String(emu(d?.bottomPt))}" distL="${String(emu(d?.leftPt))}" distR="${String(emu(d?.rightPt))}"`;
|
|
@@ -821,22 +821,27 @@ function runXml(run, state, scope) {
|
|
|
821
821
|
if (run.noteNumber) return `<w:r>${rPr}<${scope.noteKind === "endnote" ? "w:endnoteRef" : "w:footnoteRef"}/></w:r>`;
|
|
822
822
|
if (run.commentRef !== void 0) return `<w:r>${rPr}<w:commentReference w:id="${escapeAttr(run.commentRef)}"/></w:r>`;
|
|
823
823
|
const brk = run.pageBreak ? "<w:br w:type=\"page\"/>" : "";
|
|
824
|
+
if (run.field !== void 0 && run.text !== "") {
|
|
825
|
+
const inner = `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(run.text)}</w:t></w:r>`;
|
|
826
|
+
return `<w:fldSimple w:instr=" ${run.field} ">${inner}</w:fldSimple>${brk}`;
|
|
827
|
+
}
|
|
824
828
|
if (run.text === "") return brk ? `<w:r>${rPr}${brk}</w:r>` : "";
|
|
825
|
-
return `<w:r>${rPr}
|
|
829
|
+
return `<w:r>${rPr}${run.text.split(" ").map((piece) => piece === "" ? "" : `<w:t xml:space="preserve">${escapeXml(piece)}</w:t>`).join("<w:tab/>")}${brk}</w:r>`;
|
|
826
830
|
}
|
|
827
831
|
function rPrXml(r) {
|
|
828
832
|
const out = [];
|
|
833
|
+
const states = (key) => r[key] !== void 0 && r[key] !== DEFAULT_RUN[key];
|
|
829
834
|
const fonts = rFontsXml(r.fontFamily);
|
|
830
835
|
if (fonts) out.push(fonts);
|
|
831
|
-
if (
|
|
832
|
-
if (
|
|
833
|
-
if (
|
|
834
|
-
if (
|
|
835
|
-
if (
|
|
836
|
-
if (
|
|
836
|
+
if (states("bold")) out.push(toggle("w:b", r.bold));
|
|
837
|
+
if (states("italic")) out.push(toggle("w:i", r.italic));
|
|
838
|
+
if (states("strike")) out.push(toggle("w:strike", r.strike));
|
|
839
|
+
if (states("colorHex")) out.push(`<w:color w:val="${r.colorHex}"/>`);
|
|
840
|
+
if (states("fontSizePt")) out.push(`<w:sz w:val="${Math.round(r.fontSizePt * 2)}"/>`);
|
|
841
|
+
if (states("underline")) out.push(underlineXml(r));
|
|
837
842
|
if (r.shadingColorHex !== void 0) out.push(runShdXml(r.shadingColorHex));
|
|
838
|
-
if (
|
|
839
|
-
if (
|
|
843
|
+
if (states("verticalAlign")) out.push(`<w:vertAlign w:val="${r.verticalAlign}"/>`);
|
|
844
|
+
if (states("rtl")) out.push(toggle("w:rtl", r.rtl));
|
|
840
845
|
if (r.lang !== void 0) out.push(`<w:lang w:val="${escapeAttr(r.lang)}"/>`);
|
|
841
846
|
return out.length > 0 ? `<w:rPr>${out.join("")}</w:rPr>` : "";
|
|
842
847
|
}
|
|
@@ -858,6 +863,17 @@ function pPrBody(p) {
|
|
|
858
863
|
const spacing = spacingXml(p);
|
|
859
864
|
if (spacing) out.push(spacing);
|
|
860
865
|
if (JC.has(p.alignment) && p.alignment !== DEFAULT_PARA.alignment) out.push(`<w:jc w:val="${p.alignment}"/>`);
|
|
866
|
+
const pBdr = bordersXml("w:pBdr", p.borders);
|
|
867
|
+
if (pBdr) out.push(pBdr);
|
|
868
|
+
const tabs = p.tabs ?? [];
|
|
869
|
+
if (tabs.length > 0) {
|
|
870
|
+
const stops = tabs.map((t) => {
|
|
871
|
+
const pos = twips(t.positionPt);
|
|
872
|
+
const leader = t.leader !== void 0 ? ` w:leader="${escapeAttr(t.leader)}"` : "";
|
|
873
|
+
return `<w:tab w:val="${escapeAttr(t.alignment ?? "left")}" w:pos="${String(pos)}"${leader}/>`;
|
|
874
|
+
}).join("");
|
|
875
|
+
out.push(`<w:tabs>${stops}</w:tabs>`);
|
|
876
|
+
}
|
|
861
877
|
return out.join("");
|
|
862
878
|
}
|
|
863
879
|
function pPrWithSect(p, closingSectPr) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reamkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "Ream — convert DOCX, XLSX, PPTX and PDF to PDF, SVG, HTML, DOCX and XLSX, built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alex Krassavin <info@reamkit.dev>",
|