reamkit 1.28.0 → 1.30.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/core/document-model/types.d.ts +2 -0
- package/dist/esm/core/drawingml/shape-render.js +13 -1
- package/dist/esm/core/fonts/index.d.ts +1 -1
- package/dist/esm/core/fonts/remote-fonts.d.ts +8 -0
- package/dist/esm/core/fonts/remote-fonts.js +100 -17
- package/dist/esm/core/ir/flow.d.ts +17 -0
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/pdf-reader/annot-draw.js +93 -1
- package/dist/esm/pdf-reader/annots.d.ts +18 -0
- package/dist/esm/pdf-reader/annots.js +86 -9
- package/dist/esm/pdf-reader/cmap.js +13 -3
- package/dist/esm/pdf-reader/content.d.ts +32 -4
- package/dist/esm/pdf-reader/content.js +177 -12
- package/dist/esm/pdf-reader/display.js +16 -0
- package/dist/esm/pdf-reader/document.d.ts +8 -0
- package/dist/esm/pdf-reader/embedded-fonts.d.ts +24 -0
- package/dist/esm/pdf-reader/embedded-fonts.js +48 -9
- package/dist/esm/pdf-reader/flow-build.d.ts +101 -5
- package/dist/esm/pdf-reader/flow-build.js +219 -25
- package/dist/esm/pdf-reader/font.d.ts +27 -1
- package/dist/esm/pdf-reader/font.js +313 -29
- package/dist/esm/pdf-reader/glyf-outline.js +13 -2
- package/dist/esm/pdf-reader/glyph-shapes.d.ts +18 -0
- package/dist/esm/pdf-reader/glyph-shapes.js +57 -0
- package/dist/esm/pdf-reader/image-decode.js +70 -4
- package/dist/esm/pdf-reader/images.d.ts +5 -0
- package/dist/esm/pdf-reader/images.js +4 -2
- package/dist/esm/pdf-reader/jbig2.d.ts +40 -1
- package/dist/esm/pdf-reader/jbig2.js +78 -16
- package/dist/esm/pdf-reader/jpeg.d.ts +6 -3
- package/dist/esm/pdf-reader/jpeg.js +21 -1
- package/dist/esm/pdf-reader/layout.d.ts +26 -2
- package/dist/esm/pdf-reader/layout.js +974 -55
- package/dist/esm/pdf-reader/lexer.d.ts +10 -0
- package/dist/esm/pdf-reader/lexer.js +17 -0
- package/dist/esm/pdf-reader/pattern-tint.d.ts +11 -1
- package/dist/esm/pdf-reader/pattern-tint.js +21 -3
- package/dist/esm/pdf-reader/regions.d.ts +25 -0
- package/dist/esm/pdf-reader/regions.js +167 -0
- package/dist/esm/pdf-reader/shading.d.ts +58 -2
- package/dist/esm/pdf-reader/shading.js +181 -11
- package/dist/esm/pdf-reader/struct-tree.js +112 -8
- package/dist/esm/pdf-reader/tagged.js +27 -7
- package/dist/esm/pdf-reader/text-rules.js +1 -1
- package/dist/esm/pdf-reader/text.js +9 -14
- package/dist/esm/pdf-reader/vector.d.ts +10 -2
- package/dist/esm/pdf-reader/vector.js +35 -5
- package/dist/esm/word/docx-writer.js +156 -39
- package/dist/esm/word/drawing-parser.js +7 -2
- package/dist/esm/word/paragraph-properties.js +2 -0
- package/package.json +5 -3
|
@@ -3,12 +3,15 @@ import { ResourceStore } from "../core/ir/resources.js";
|
|
|
3
3
|
import { FEATURES } from "../core/ir/features.js";
|
|
4
4
|
import { isRightToLeft } from "./content.js";
|
|
5
5
|
import { collectEmbeddedFonts } from "./embedded-fonts.js";
|
|
6
|
+
import { collectFaceFamilies } from "./font.js";
|
|
6
7
|
import { extractPageText } from "./text.js";
|
|
7
8
|
import { collectPageVectors } from "./vector.js";
|
|
8
9
|
import { displayOf, placeImages, placeRuns, placeVectors } from "./display.js";
|
|
9
|
-
import { buildFlowDoc, dedupeLosses, imageBlock, paragraphFromRuns, positionedText, sectionFromPdfPages, shapeBlock, withMeasuredMargins } from "./flow-build.js";
|
|
10
|
+
import { BASELINE_AT, CARRIER_LINE_PT, GUESSED_MARGIN, NATURAL_LINE_EM, buildFlowDoc, dedupeLosses, imageBlock, paragraphFromRuns, positionedText, sectionFromPdfPages, shapeBlock, spaceAfter, textSizeOf, tooSmallToRead, withMeasuredMargins } from "./flow-build.js";
|
|
10
11
|
import { collectPageImages } from "./images.js";
|
|
11
12
|
import { markDrawnRules } from "./text-rules.js";
|
|
13
|
+
import { regionsOf } from "./regions.js";
|
|
14
|
+
import { punctuationOf } from "./glyph-shapes.js";
|
|
12
15
|
import { matrixBlocks } from "./math-rows.js";
|
|
13
16
|
//#region src/pdf-reader/layout.ts
|
|
14
17
|
/** The relationships the reconstruction files its running head and foot under. */
|
|
@@ -35,7 +38,11 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
35
38
|
const allRuns = pages.map((page, i) => placeRuns(extractPageText(file, page), shown[i]));
|
|
36
39
|
const foot = mode === "positional" ? void 0 : runningFoot(allRuns, shown, "foot");
|
|
37
40
|
const head = mode === "positional" ? void 0 : runningFoot(allRuns, shown, "head");
|
|
38
|
-
const
|
|
41
|
+
const textSize = textSizeOf(allRuns);
|
|
42
|
+
const stamp = (r) => mode !== "positional" && (r.annotation === true || tooSmallToRead(r, textSize));
|
|
43
|
+
const stamps = allRuns.map((runs) => runs.filter(stamp));
|
|
44
|
+
const pageRuns = foot || head || stamps.some((s) => s.length > 0) ? allRuns.map((runs, i) => runs.filter((r) => foot?.lift[i]?.has(r) !== true && head?.lift[i]?.has(r) !== true && !stamp(r))) : allRuns;
|
|
45
|
+
const pageMarks = pages.map(() => []);
|
|
39
46
|
const medianFont = median(pageRuns.flat().map((r) => r.fontSizePt).filter((s) => s > 0)) || 12;
|
|
40
47
|
const resources = new ResourceStore();
|
|
41
48
|
const losses = [];
|
|
@@ -61,6 +68,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
61
68
|
const sectionEnds = [];
|
|
62
69
|
let sectionFrom = 0;
|
|
63
70
|
let lastSize = "";
|
|
71
|
+
const leads = [];
|
|
64
72
|
let curColumns = 1;
|
|
65
73
|
let curSpace = 0;
|
|
66
74
|
let pendingContinuous = false;
|
|
@@ -71,18 +79,19 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
71
79
|
const pageWidth = display.width;
|
|
72
80
|
const stepped = stepsBetweenWords(runs);
|
|
73
81
|
const blocks = [];
|
|
74
|
-
const split = gutters.length > 0 ? assignColumns(runs, gutters) : void 0;
|
|
75
82
|
const textEdges = pageTextEdges(runs);
|
|
83
|
+
const inColumns = proseColumns(runs, gutters, textEdges);
|
|
84
|
+
const split = gutters.length > 0 && inColumns ? assignColumns(runs, gutters) : void 0;
|
|
76
85
|
const bandEpsilon = (median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10) / 2;
|
|
77
86
|
const bandAt = (top) => bandOf(split?.breaks ?? [], top, bandEpsilon);
|
|
78
87
|
const addColumn = (allRuns, col) => {
|
|
79
|
-
const colRuns =
|
|
88
|
+
const colRuns = allRuns.filter((r) => r.type3 !== true && (mode !== "positional" || r.invisible !== true));
|
|
80
89
|
if (mode === "positional") {
|
|
81
|
-
for (const [angle,
|
|
90
|
+
for (const [angle, turned] of byAngle(colRuns)) for (const runs of byPainter(turned)) for (const line of groupIntoLines(rotate(runs, -angle), true, stepped)) {
|
|
82
91
|
if (line.text.length === 0) continue;
|
|
83
92
|
const box = turnedBox(line, angle, pageWidth);
|
|
84
93
|
placed.push({
|
|
85
|
-
key:
|
|
94
|
+
key: placedKey(runs, placed.length),
|
|
86
95
|
col,
|
|
87
96
|
top: box.y + box.height,
|
|
88
97
|
make: (z) => positionedText(line.spans, box, frame, z, rotation60kOf(angle))
|
|
@@ -93,7 +102,8 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
93
102
|
const columnSize = median(colRuns.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
|
|
94
103
|
const maths = matrixBlocks(colRuns, columnSize);
|
|
95
104
|
const inMath = new Set(maths.flatMap((m) => [...m.used]));
|
|
96
|
-
const
|
|
105
|
+
const textRuns = inMath.size > 0 ? colRuns.filter((r) => !inMath.has(r)) : colRuns;
|
|
106
|
+
const lines = groupIntoLines(textRuns, false, stepped).filter((l) => l.text.length > 0);
|
|
97
107
|
const measure = measureOf(col, gutters, textEdges) ?? measureOfLines(lines);
|
|
98
108
|
for (const found of maths) {
|
|
99
109
|
const { alignment } = alignmentOf([{
|
|
@@ -121,17 +131,153 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
121
131
|
}
|
|
122
132
|
});
|
|
123
133
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
let above;
|
|
135
|
+
for (const region of regionsOf(textRuns)) {
|
|
136
|
+
if (region.kind === "side") {
|
|
137
|
+
const made = sideBySide(region, measure, above);
|
|
138
|
+
if (made === void 0) continue;
|
|
139
|
+
blocks.push({
|
|
140
|
+
band: bandAt(made.top),
|
|
141
|
+
col,
|
|
142
|
+
top: made.top,
|
|
143
|
+
el: made.el
|
|
144
|
+
});
|
|
145
|
+
above = made.below;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const regionLines = groupIntoLines(region.runs, false, stepped).filter((l) => l.text.length > 0);
|
|
149
|
+
const reach = lines.length < 3 && gutters.length === 0 ? pageWidth * (1 - GUESSED_MARGIN) : void 0;
|
|
150
|
+
const paras = groupIntoParagraphs(regionLines, measure, display.height, above, false, reach);
|
|
151
|
+
for (const set of setParagraphs(paras, measure && {
|
|
152
|
+
left: measure.left,
|
|
153
|
+
right: Math.max(measure.right, textEdges?.right ?? measure.right)
|
|
154
|
+
}, pageWidth)) blocks.push({
|
|
155
|
+
band: bandAt(set.top),
|
|
156
|
+
col,
|
|
157
|
+
top: set.top,
|
|
158
|
+
el: set.el
|
|
159
|
+
});
|
|
160
|
+
const last = paras[paras.length - 1];
|
|
161
|
+
if (last !== void 0) above = {
|
|
162
|
+
y: last.bottom,
|
|
163
|
+
lineHeight: last.lineHeight
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* A region's paragraphs as the elements they are set in: the lines that
|
|
169
|
+
* stand on the same stops as a table (§17.4.38), the rest as paragraphs
|
|
170
|
+
* placed and spaced the way the page placed and spaced them.
|
|
171
|
+
*/
|
|
172
|
+
const far = /* @__PURE__ */ new WeakMap();
|
|
173
|
+
const setParagraphs = (paras, tableMeasure, sheetWidth = 0) => {
|
|
174
|
+
const asRows = tabbedRows(paras, tableMeasure);
|
|
175
|
+
const out = [];
|
|
176
|
+
for (const [at, para] of paras.entries()) {
|
|
177
|
+
const table = asRows.get(at);
|
|
178
|
+
if (table !== void 0) {
|
|
179
|
+
if (table !== null) out.push({
|
|
180
|
+
top: para.top,
|
|
181
|
+
el: table
|
|
182
|
+
});
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const overflow = sheetWidth > 0 ? runOn(para, tableMeasure, sheetWidth) : 0;
|
|
186
|
+
const el = paragraphFromRuns(para.spans, headingLevel(para.fontSize, medianFont), {
|
|
187
|
+
...para.stops !== void 0 && para.stops.length > 0 ? { tabs: para.stops.filter((x) => x > 0).map((x) => ({
|
|
188
|
+
positionPt: pt(x),
|
|
189
|
+
alignment: "left"
|
|
190
|
+
})) } : {},
|
|
129
191
|
...para.alignment !== void 0 ? { alignment: para.alignment } : {},
|
|
130
192
|
...para.spacingBefore !== void 0 ? { spacingBefore: pt(para.spacingBefore) } : {},
|
|
193
|
+
spacingLine: pt(para.lineHeight),
|
|
194
|
+
spacingLineRule: "exact",
|
|
131
195
|
...para.indentLeft !== void 0 ? { indentLeft: pt(para.indentLeft) } : {},
|
|
132
|
-
...para.indentFirstLine !== void 0 ? { indentFirstLine: pt(para.indentFirstLine) } : {}
|
|
133
|
-
|
|
196
|
+
...para.indentFirstLine !== void 0 ? { indentFirstLine: pt(para.indentFirstLine) } : {},
|
|
197
|
+
...overflow > 0 ? { indentRight: pt(-overflow) } : {}
|
|
198
|
+
});
|
|
199
|
+
if (sheetWidth > 0 && para.far !== void 0) far.set(el, para.far);
|
|
200
|
+
out.push({
|
|
201
|
+
top: para.top,
|
|
202
|
+
el
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* §17.4.38 — a band of blocks side by side, as the borderless table a
|
|
209
|
+
* writer sets such a band in: one row, a cell a block, each cell's lines
|
|
210
|
+
* read down on their own and spaced from what stands above the band.
|
|
211
|
+
*/
|
|
212
|
+
const sideBySide = (region, measure, above) => {
|
|
213
|
+
const left = measure?.left ?? Math.min(...region.cells.map((c) => c.from));
|
|
214
|
+
const right = Math.max(measure?.right ?? 0, textEdges?.right ?? 0, ...region.cells.map((c) => c.to));
|
|
215
|
+
const cells = region.cells.map((cell) => {
|
|
216
|
+
return {
|
|
217
|
+
cell,
|
|
218
|
+
lines: groupIntoLines(cell.runs, false, stepped).filter((l) => l.text.length > 0)
|
|
219
|
+
};
|
|
134
220
|
});
|
|
221
|
+
if (cells.some((c) => c.lines.length === 0)) return void 0;
|
|
222
|
+
const top = Math.max(...cells.map((c) => c.lines[0].y));
|
|
223
|
+
const edge = above ?? {
|
|
224
|
+
y: Math.max(...cells.map((c) => c.lines[0].y + .8 * 1.2 * c.lines[0].fontSize)),
|
|
225
|
+
lineHeight: 0
|
|
226
|
+
};
|
|
227
|
+
const starts = cells.map((c, k) => k === 0 ? left : c.cell.from);
|
|
228
|
+
let foot = Infinity;
|
|
229
|
+
const row = cells.map(({ cell, lines }, k) => {
|
|
230
|
+
const own = {
|
|
231
|
+
left: cell.from,
|
|
232
|
+
right: cell.to
|
|
233
|
+
};
|
|
234
|
+
const paras = groupIntoParagraphs(lines, own, display.height, edge, true);
|
|
235
|
+
const last = paras[paras.length - 1];
|
|
236
|
+
foot = Math.min(foot, last.bottom - (1 - BASELINE_AT) * last.lineHeight);
|
|
237
|
+
const inset = cell.from - starts[k];
|
|
238
|
+
const content = setParagraphs(paras, own).map(({ el }) => inset > 0 && el.kind === "paragraph" ? {
|
|
239
|
+
...el,
|
|
240
|
+
paragraph: {
|
|
241
|
+
...el.paragraph,
|
|
242
|
+
properties: {
|
|
243
|
+
...el.paragraph.properties,
|
|
244
|
+
indentLeft: pt((el.paragraph.properties.indentLeft ?? 0) + inset)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} : el);
|
|
248
|
+
const width = (starts[k + 1] ?? right) - starts[k];
|
|
249
|
+
return {
|
|
250
|
+
properties: { width: pt(Math.max(width, 1)) },
|
|
251
|
+
content
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
const grid = row.map((c) => c.properties.width);
|
|
255
|
+
return {
|
|
256
|
+
top,
|
|
257
|
+
below: {
|
|
258
|
+
y: foot,
|
|
259
|
+
lineHeight: 0
|
|
260
|
+
},
|
|
261
|
+
el: {
|
|
262
|
+
kind: "table",
|
|
263
|
+
table: {
|
|
264
|
+
properties: {
|
|
265
|
+
defaultCellMargins: {
|
|
266
|
+
left: pt(0),
|
|
267
|
+
right: pt(0)
|
|
268
|
+
},
|
|
269
|
+
layout: "fixed",
|
|
270
|
+
widthType: "dxa",
|
|
271
|
+
widthPt: pt(grid.reduce((sum, w) => sum + w, 0))
|
|
272
|
+
},
|
|
273
|
+
grid,
|
|
274
|
+
rows: [{
|
|
275
|
+
properties: {},
|
|
276
|
+
cells: row
|
|
277
|
+
}]
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
135
281
|
};
|
|
136
282
|
const placed = [];
|
|
137
283
|
const colOf = (centerX) => gutters.filter((g) => centerX >= g.mid).length;
|
|
@@ -139,12 +285,23 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
139
285
|
left: 0,
|
|
140
286
|
top: display.height
|
|
141
287
|
};
|
|
288
|
+
for (const runs of byPainter(stamps[i] ?? [])) for (const line of groupIntoLines(runs, true, stepped)) {
|
|
289
|
+
if (line.text.length === 0) continue;
|
|
290
|
+
const box = turnedBox(line, 0, pageWidth);
|
|
291
|
+
placed.push({
|
|
292
|
+
key: placedKey(runs, placed.length),
|
|
293
|
+
col: colOf(box.x + box.width / 2),
|
|
294
|
+
top: box.y + box.height,
|
|
295
|
+
make: (z) => positionedText(line.spans, box, frame, z)
|
|
296
|
+
});
|
|
297
|
+
}
|
|
142
298
|
const raw = collectPageImages(file, page);
|
|
143
299
|
const imgs = {
|
|
144
300
|
images: placeImages(raw.images, display),
|
|
145
301
|
losses: raw.losses
|
|
146
302
|
};
|
|
147
303
|
losses.push(...imgs.losses);
|
|
304
|
+
pageMarks[i] = imgs.images;
|
|
148
305
|
const lifted = collectPageVectors(file, page, imgs.images.map((img) => ({
|
|
149
306
|
minX: img.x,
|
|
150
307
|
minY: img.y,
|
|
@@ -153,8 +310,14 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
153
310
|
})));
|
|
154
311
|
losses.push(...lifted.losses);
|
|
155
312
|
const placedVectors = placeVectors(lifted.vectors, display);
|
|
156
|
-
const
|
|
157
|
-
const
|
|
313
|
+
const drawnRules = markDrawnRules(runs, placedVectors);
|
|
314
|
+
const strayGlyphs = strayMarks(placedVectors, runs);
|
|
315
|
+
const ruled = mode !== "positional" ? {
|
|
316
|
+
...drawnRules,
|
|
317
|
+
runs: readDrawnMarks(drawnRules.runs, strayGlyphs)
|
|
318
|
+
} : drawnRules;
|
|
319
|
+
const vectors = placedVectors.filter((v) => !ruled.consumed.has(v)).filter((v) => !(mode !== "positional" && strayGlyphs.has(v)));
|
|
320
|
+
const drawn = drawnWords(vectors);
|
|
158
321
|
const ruledIntoColumns = mode !== "positional" && looksRuled(ruled.runs, gutters, textEdges);
|
|
159
322
|
const asTable = ruledIntoColumns && textEdges ? tableFrom(ruled.runs, gutters, textEdges, stepped) : void 0;
|
|
160
323
|
if (asTable) for (const block of asTable) blocks.push({
|
|
@@ -169,6 +332,8 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
169
332
|
for (const col of [SPANNING_COLUMN, ...columns]) addColumn(ruled.runs.filter((r) => columnFor(r) === col), col);
|
|
170
333
|
} else addColumn(ruled.runs, 0);
|
|
171
334
|
const under = mode !== "positional";
|
|
335
|
+
setOff(blocks, far, (line, k) => positionedText(line.spans, turnedBox(line, 0, pageWidth), frame, FAR_Z + k));
|
|
336
|
+
const givenAway = mode !== "positional" ? ruleBorders(vectors, blocks, display.width) : void 0;
|
|
172
337
|
[
|
|
173
338
|
...imgs.images.map((img) => ({
|
|
174
339
|
key: img.orderKey,
|
|
@@ -176,7 +341,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
176
341
|
top: img.y + img.heightPt,
|
|
177
342
|
make: (z) => imageBlock(img, resources, void 0, frame, z, under)
|
|
178
343
|
})),
|
|
179
|
-
...
|
|
344
|
+
...drawn.filter((v) => givenAway?.has(v) !== true).map((v) => ({
|
|
180
345
|
key: v.orderKey,
|
|
181
346
|
col: colOf((v.minX + v.maxX) / 2),
|
|
182
347
|
top: v.maxY,
|
|
@@ -207,20 +372,41 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
207
372
|
pendingContinuous = false;
|
|
208
373
|
}
|
|
209
374
|
lastSize = size;
|
|
210
|
-
if (i > 0 && !opensSection
|
|
375
|
+
if (i > 0 && !opensSection) body.push({
|
|
211
376
|
kind: "paragraph",
|
|
212
377
|
paragraph: {
|
|
213
378
|
properties: {
|
|
214
379
|
pageBreakBefore: true,
|
|
215
|
-
spacingLine:
|
|
380
|
+
spacingLine: CARRIER_LINE_PT,
|
|
216
381
|
spacingLineRule: "exact"
|
|
217
382
|
},
|
|
218
383
|
runs: []
|
|
219
384
|
}
|
|
220
385
|
});
|
|
221
|
-
|
|
386
|
+
else if (i === 0 && blocks.length === 0) body.push({
|
|
387
|
+
kind: "paragraph",
|
|
388
|
+
paragraph: {
|
|
389
|
+
properties: {
|
|
390
|
+
spacingLine: CARRIER_LINE_PT,
|
|
391
|
+
spacingLineRule: "exact"
|
|
392
|
+
},
|
|
393
|
+
runs: []
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
const columnsHere = ruledIntoColumns || !proseColumns(runs, gutters, textEdges) ? 1 : gutters.length + 1;
|
|
222
397
|
const spacePt = columnsHere > 1 ? median(gutters.map((g) => g.to - g.from)) : 0;
|
|
398
|
+
let led = false;
|
|
223
399
|
for (const block of blocks) {
|
|
400
|
+
if (!led && mode !== "positional") {
|
|
401
|
+
const lead = leadingLine(block.el);
|
|
402
|
+
if (lead !== void 0) leads.push({
|
|
403
|
+
page: i,
|
|
404
|
+
at: body.length,
|
|
405
|
+
baseline: block.top,
|
|
406
|
+
lineHeight: lead
|
|
407
|
+
});
|
|
408
|
+
led = lead !== void 0 || block.el.kind === "table" || block.el.kind === "paragraph";
|
|
409
|
+
}
|
|
224
410
|
const count = block.col === SPANNING_COLUMN ? 1 : columnsHere;
|
|
225
411
|
if (count !== curColumns) {
|
|
226
412
|
sectionEnds.push({
|
|
@@ -241,7 +427,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
241
427
|
});
|
|
242
428
|
const setUp = (from, to) => {
|
|
243
429
|
const own = sectionFromPdfPages(pages.slice(from, to));
|
|
244
|
-
return mode === "positional" ? own : withMeasuredMargins(own, shown.slice(from, to), pageRuns.slice(from, to));
|
|
430
|
+
return mode === "positional" ? own : withMeasuredMargins(own, shown.slice(from, to), pageRuns.slice(from, to), pageMarks.slice(from, to), foot?.band);
|
|
245
431
|
};
|
|
246
432
|
sectionEnds.push({
|
|
247
433
|
at: body.length,
|
|
@@ -266,6 +452,25 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
266
452
|
endIndex: end.at
|
|
267
453
|
}];
|
|
268
454
|
}) : [];
|
|
455
|
+
for (const lead of leads) {
|
|
456
|
+
const end = sectionEnds.find((e) => e.at > lead.at);
|
|
457
|
+
const top = (end ? setUp(end.from, end.to) : setUp(0, pages.length))?.margins?.top;
|
|
458
|
+
const page = shown[lead.page];
|
|
459
|
+
const el = body[lead.at];
|
|
460
|
+
if (top === void 0 || page === void 0 || el?.kind !== "paragraph") continue;
|
|
461
|
+
const before = page.height - top - lead.baseline - BASELINE_AT * lead.lineHeight;
|
|
462
|
+
if (before <= SPACING_NOISE_PT) continue;
|
|
463
|
+
body[lead.at] = {
|
|
464
|
+
...el,
|
|
465
|
+
paragraph: {
|
|
466
|
+
...el.paragraph,
|
|
467
|
+
properties: {
|
|
468
|
+
...el.paragraph.properties,
|
|
469
|
+
spacingBefore: pt(before)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
269
474
|
const stepped0 = stepsBetweenWords(allRuns[0] ?? []);
|
|
270
475
|
const edges0 = pageTextEdges(allRuns[0] ?? []);
|
|
271
476
|
const band = foot ? footerBand(foot.band, stepped0, edges0, foot.numbered) : [];
|
|
@@ -285,7 +490,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
285
490
|
doc: buildFlowDoc(body, resources, withFooter(setUp(0, pages.length)), collectEmbeddedFonts(file, pages, losses), sections.map((s) => ({
|
|
286
491
|
...s,
|
|
287
492
|
properties: withFooter(s.properties) ?? s.properties
|
|
288
|
-
})), band.length > 0 || headBand.length > 0 ? new Map([...band.length > 0 ? [[FOOTER_PART, band]] : [], ...headBand.length > 0 ? [[HEADER_PART, headBand]] : []]) : void 0),
|
|
493
|
+
})), band.length > 0 || headBand.length > 0 ? new Map([...band.length > 0 ? [[FOOTER_PART, band]] : [], ...headBand.length > 0 ? [[HEADER_PART, headBand]] : []]) : void 0, collectFaceFamilies(file, pages)),
|
|
289
494
|
losses: dedupeLosses(losses)
|
|
290
495
|
};
|
|
291
496
|
}
|
|
@@ -302,9 +507,9 @@ function detectGutters(runs, pageWidth) {
|
|
|
302
507
|
if (span < pageWidth * .5) return [];
|
|
303
508
|
const sorted = [...spans].sort((a, b) => a[0] - b[0]);
|
|
304
509
|
let curEnd = sorted[0][1];
|
|
305
|
-
const
|
|
510
|
+
const bands = [];
|
|
306
511
|
for (const [l, r] of sorted) {
|
|
307
|
-
if (l - curEnd >= fontSize * 3)
|
|
512
|
+
if (l - curEnd >= fontSize * 3) bands.push({
|
|
308
513
|
mid: (curEnd + l) / 2,
|
|
309
514
|
from: curEnd,
|
|
310
515
|
to: l
|
|
@@ -315,6 +520,17 @@ function detectGutters(runs, pageWidth) {
|
|
|
315
520
|
const ink = runInk(r);
|
|
316
521
|
return ink ? [ink] : [];
|
|
317
522
|
}).sort((a, b) => a[0] - b[0]));
|
|
523
|
+
const straddling = (band) => {
|
|
524
|
+
let n = 0;
|
|
525
|
+
for (const row of rows) {
|
|
526
|
+
const before = row.filter(([, r]) => r <= band.from);
|
|
527
|
+
const after = row.filter(([l]) => l >= band.to);
|
|
528
|
+
if (before.length === 0 || after.length === 0) continue;
|
|
529
|
+
if (Math.min(...after.map(([l]) => l)) - Math.max(...before.map(([, r]) => r)) >= fontSize * MIN_GUTTER_EM) n++;
|
|
530
|
+
}
|
|
531
|
+
return n;
|
|
532
|
+
};
|
|
533
|
+
const empty = bands.filter((b) => straddling(b) >= MIN_EMPTY_BAND_ROWS);
|
|
318
534
|
const voted = [];
|
|
319
535
|
for (const x of sample(minX + span * .1, minX + span * .9, 1)) {
|
|
320
536
|
let columned = 0;
|
|
@@ -442,6 +658,13 @@ var MIN_SHARED_ROWS = 3;
|
|
|
442
658
|
/** How many lines have to be split at the same place before the page is in columns. */
|
|
443
659
|
var MIN_COLUMNED_ROWS = 12;
|
|
444
660
|
/**
|
|
661
|
+
* How many lines a band NO run crosses must separate before it is a gutter
|
|
662
|
+
* rather than an empty half of a page. Lower than the vote's bar, because a
|
|
663
|
+
* band nothing crosses is stronger evidence than a band most lines avoid — but
|
|
664
|
+
* not zero, which is what an invoice's blank left half offers.
|
|
665
|
+
*/
|
|
666
|
+
var MIN_EMPTY_BAND_ROWS = 8;
|
|
667
|
+
/**
|
|
445
668
|
* The page's runs grouped by baseline.
|
|
446
669
|
*
|
|
447
670
|
* Swept in order rather than bucketed: a superscript sits a few points above
|
|
@@ -647,6 +870,32 @@ function bandOf(breaks, top, epsilon) {
|
|
|
647
870
|
* does this — a flowing paragraph is meant to be read across.
|
|
648
871
|
*/
|
|
649
872
|
var SPACE_GAP_EM = .25;
|
|
873
|
+
/**
|
|
874
|
+
* §12.5.5 — the runs the page's own content paints, and then the ones an
|
|
875
|
+
* annotation's appearance does: two layers, grouped into lines apart.
|
|
876
|
+
*/
|
|
877
|
+
function byPainter(runs) {
|
|
878
|
+
return [runs.filter((r) => r.annotation !== true), runs.filter((r) => r.annotation === true)].filter((group) => group.length > 0);
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* §8.5.3 / §12.5.5 — where a placed line stands among the page's marks, which
|
|
882
|
+
* are ordered as they were painted (see `compareOrder`): the page's own words
|
|
883
|
+
* over everything its content drew, and under every annotation, whose
|
|
884
|
+
* appearances paint after it (`[MAX_SAFE_INTEGER, index, …]`); an
|
|
885
|
+
* annotation's words over all of it. Keyed as one layer, the placed lines
|
|
886
|
+
* fell among the appearances by their count: evaljs.pdf's "Execute" went
|
|
887
|
+
* under the grey of the button it names.
|
|
888
|
+
*
|
|
889
|
+
* @param runs The layer the line was grouped from (see `byPainter`).
|
|
890
|
+
* @param count How many lines are placed already.
|
|
891
|
+
*/
|
|
892
|
+
function placedKey(runs, count) {
|
|
893
|
+
return runs[0]?.annotation === true ? [
|
|
894
|
+
Number.MAX_SAFE_INTEGER,
|
|
895
|
+
Number.MAX_SAFE_INTEGER,
|
|
896
|
+
count
|
|
897
|
+
] : [Number.MAX_SAFE_INTEGER - 1, count];
|
|
898
|
+
}
|
|
650
899
|
/** Runs by the direction of their baseline, upright first, each angle rounded. */
|
|
651
900
|
function byAngle(runs) {
|
|
652
901
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -733,10 +982,12 @@ function groupIntoLines(runs, split = false, stepped = false) {
|
|
|
733
982
|
runs: [run]
|
|
734
983
|
});
|
|
735
984
|
}
|
|
985
|
+
for (const c of clusters) c.runs.sort((a, b) => a.x - b.x);
|
|
986
|
+
const stops = sharedStops(clusters.map((c) => c.runs));
|
|
736
987
|
return clusters.flatMap((c) => {
|
|
737
|
-
const ordered = c.runs
|
|
988
|
+
const ordered = c.runs;
|
|
738
989
|
const fontSize = c.fontSize || 10;
|
|
739
|
-
if (!split) return [lineOf(ordered, c.y, fontSize, stepped)];
|
|
990
|
+
if (!split) return [lineOf(ordered, c.y, fontSize, stepped, stops)];
|
|
740
991
|
const pieces = [[]];
|
|
741
992
|
for (const run of ordered) {
|
|
742
993
|
const prev = pieces[pieces.length - 1];
|
|
@@ -745,9 +996,49 @@ function groupIntoLines(runs, split = false, stepped = false) {
|
|
|
745
996
|
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([]);
|
|
746
997
|
pieces[pieces.length - 1].push(run);
|
|
747
998
|
}
|
|
748
|
-
return pieces.map((piece) => lineOf(piece, piece[0].y, Math.max(...piece.map((r) => r.fontSizePt || 0)) || fontSize, stepped));
|
|
999
|
+
return pieces.map((piece) => lineOf(piece, piece[0].y, Math.max(...piece.map((r) => r.fontSizePt || 0)) || fontSize, stepped, stops));
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* §17.3.1.38 — the stops a block of lines is set out on: where text resumes,
|
|
1004
|
+
* after a gap wider than a word space, at the same place on more than one line
|
|
1005
|
+
* — and on at least one of them after a gap only a tab makes.
|
|
1006
|
+
*
|
|
1007
|
+
* A tab's gap is as wide as what stands before it leaves it. An invoice sets
|
|
1008
|
+
* "Date due", "Date of issue" and "Invoice number" over one column of values
|
|
1009
|
+
* at 108pt, and only the shortest label leaves a gap no space could make: the
|
|
1010
|
+
* other two came back with their values a word's width after the label
|
|
1011
|
+
* instead of in the column.
|
|
1012
|
+
*
|
|
1013
|
+
* @param lines Each line's runs, left to right.
|
|
1014
|
+
* @returns The x of every stop two lines share.
|
|
1015
|
+
*/
|
|
1016
|
+
function sharedStops(lines) {
|
|
1017
|
+
const seen = [];
|
|
1018
|
+
lines.forEach((runs, line) => {
|
|
1019
|
+
let end;
|
|
1020
|
+
for (const run of runs) {
|
|
1021
|
+
if (run.text.replaceAll("�", "").trim() === "") continue;
|
|
1022
|
+
const size = run.fontSizePt || 10;
|
|
1023
|
+
if (end !== void 0 && run.x - end >= size * STOP_GAP_EM) seen.push({
|
|
1024
|
+
x: run.x,
|
|
1025
|
+
line,
|
|
1026
|
+
tab: run.x - end >= size * TAB_GAP_EM
|
|
1027
|
+
});
|
|
1028
|
+
end = Math.max(end ?? run.endX, run.endX);
|
|
1029
|
+
}
|
|
749
1030
|
});
|
|
1031
|
+
const stops = [];
|
|
1032
|
+
for (const tab of seen) {
|
|
1033
|
+
if (!tab.tab || stops.some((x) => Math.abs(x - tab.x) <= STOP_SLACK_PT)) continue;
|
|
1034
|
+
if (new Set(seen.filter((o) => Math.abs(o.x - tab.x) <= STOP_SLACK_PT).map((o) => o.line)).size >= 2) stops.push(tab.x);
|
|
1035
|
+
}
|
|
1036
|
+
return stops;
|
|
750
1037
|
}
|
|
1038
|
+
/** The least gap, in ems, that lands a run on a stop the lines around it share. */
|
|
1039
|
+
var STOP_GAP_EM = .5;
|
|
1040
|
+
/** How far from a shared stop a run may start and still stand on it. */
|
|
1041
|
+
var STOP_SLACK_PT = .75;
|
|
751
1042
|
/**
|
|
752
1043
|
* Where a line's INK is, which is not how far its pen travelled.
|
|
753
1044
|
*
|
|
@@ -780,8 +1071,8 @@ function inkSpan(runs) {
|
|
|
780
1071
|
};
|
|
781
1072
|
}
|
|
782
1073
|
/** One run of runs, left to right on a shared baseline, as a {@link Line}. */
|
|
783
|
-
function lineOf(runs, y, fontSize, stepped) {
|
|
784
|
-
const ordered = lineSpans(runs, fontSize, stepped || runs.length > 1 && !runs.some((r) => SPACE.test(r.text)));
|
|
1074
|
+
function lineOf(runs, y, fontSize, stepped, shared = []) {
|
|
1075
|
+
const { spans: ordered, stops, pieces } = lineSpans(runs, fontSize, stepped || runs.length > 1 && !runs.some((r) => SPACE.test(r.text)), shared);
|
|
785
1076
|
const spans = ordered.every((s) => s.text.trim() === "" || isRightToLeft(s.text)) ? [...ordered].reverse() : ordered;
|
|
786
1077
|
const ink = inkSpan(runs);
|
|
787
1078
|
return {
|
|
@@ -789,7 +1080,11 @@ function lineOf(runs, y, fontSize, stepped) {
|
|
|
789
1080
|
width: ink.width,
|
|
790
1081
|
y,
|
|
791
1082
|
fontSize,
|
|
792
|
-
...tabbed(runs, fontSize) ? { tabbed: true } : {},
|
|
1083
|
+
...tabbed(runs, fontSize) || stops.length > 0 ? { tabbed: true } : {},
|
|
1084
|
+
...stops.length > 0 ? {
|
|
1085
|
+
stops,
|
|
1086
|
+
pieces
|
|
1087
|
+
} : {},
|
|
793
1088
|
text: spans.map((s) => s.text).join("").replace(/\s+/g, " ").trim(),
|
|
794
1089
|
spans
|
|
795
1090
|
};
|
|
@@ -885,13 +1180,35 @@ function stepsBetweenWords(runs) {
|
|
|
885
1180
|
if (runs.length < 8) return false;
|
|
886
1181
|
return runs.filter((r) => /\s/u.test(r.text)).length / runs.length < .05;
|
|
887
1182
|
}
|
|
888
|
-
function lineSpans(runs, fontSize, stepped) {
|
|
1183
|
+
function lineSpans(runs, fontSize, stepped, shared = []) {
|
|
889
1184
|
const spans = [];
|
|
1185
|
+
const stops = [];
|
|
1186
|
+
const pieces = [{
|
|
1187
|
+
from: Infinity,
|
|
1188
|
+
to: -Infinity
|
|
1189
|
+
}];
|
|
890
1190
|
const body = runs.filter((r) => (r.fontSizePt || fontSize) > fontSize * SCRIPT_SIZE);
|
|
891
1191
|
const baseline = median((body.length > 0 ? body : runs).map((r) => r.y));
|
|
892
1192
|
let prev;
|
|
893
|
-
|
|
894
|
-
|
|
1193
|
+
let inked;
|
|
1194
|
+
for (const [i, run] of runs.entries()) {
|
|
1195
|
+
if (run.text.trim() === "" && run.endX - run.x <= 0 && inked !== void 0) {
|
|
1196
|
+
const next = runs.slice(i + 1).find((r) => r.text.trim() !== "");
|
|
1197
|
+
if (next !== void 0 && next.x - inked < spaceGap(next, fontSize, true)) continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (run.text.replaceAll("�", "") === "" && run.text !== "") continue;
|
|
1200
|
+
if (prev !== void 0 && run.x - prev.endX > spaceGap(prev, fontSize, stepped) && !(isLeader(prev.text) && prev.text === run.text)) {
|
|
1201
|
+
const size = run.fontSizePt || fontSize;
|
|
1202
|
+
const onStop = inked !== void 0 && run.x - inked >= size * STOP_GAP_EM && shared.some((x) => Math.abs(x - run.x) <= STOP_SLACK_PT);
|
|
1203
|
+
if (run.x - prev.endX >= size * TAB_GAP_EM || onStop) {
|
|
1204
|
+
spans.push({ text: " " });
|
|
1205
|
+
stops.push(run.x);
|
|
1206
|
+
pieces.push({
|
|
1207
|
+
from: Infinity,
|
|
1208
|
+
to: -Infinity
|
|
1209
|
+
});
|
|
1210
|
+
} else spans.push(spaceAfter(spans[spans.length - 1]));
|
|
1211
|
+
}
|
|
895
1212
|
spans.push({
|
|
896
1213
|
text: run.text.replaceAll("�", ""),
|
|
897
1214
|
sizePt: run.fontSizePt,
|
|
@@ -908,8 +1225,18 @@ function lineSpans(runs, fontSize, stepped) {
|
|
|
908
1225
|
...run.href !== void 0 ? { href: run.href } : {}
|
|
909
1226
|
});
|
|
910
1227
|
prev = run;
|
|
1228
|
+
if (run.text.trim() !== "") {
|
|
1229
|
+
inked = Math.max(inked ?? run.endX, run.endX);
|
|
1230
|
+
const piece = pieces[pieces.length - 1];
|
|
1231
|
+
piece.from = Math.min(piece.from, run.x);
|
|
1232
|
+
piece.to = Math.max(piece.to, run.endX);
|
|
1233
|
+
}
|
|
911
1234
|
}
|
|
912
|
-
return
|
|
1235
|
+
return {
|
|
1236
|
+
spans,
|
|
1237
|
+
stops,
|
|
1238
|
+
pieces
|
|
1239
|
+
};
|
|
913
1240
|
}
|
|
914
1241
|
/**
|
|
915
1242
|
* §17.3.2.42 — a run set off the line's baseline, and smaller, as the script it
|
|
@@ -960,39 +1287,110 @@ function carriesLeader(line) {
|
|
|
960
1287
|
}
|
|
961
1288
|
/** Three of the same leader character in a row is a leader and not punctuation. */
|
|
962
1289
|
var LEADER_RUN = /([.\u00b7_])\1{2,}/u;
|
|
963
|
-
|
|
1290
|
+
/**
|
|
1291
|
+
* Whether the line is a RULE drawn out of characters — a row of hyphens, dots
|
|
1292
|
+
* or underscores and nothing else.
|
|
1293
|
+
*
|
|
1294
|
+
* A page with no rule to draw types one. It is not a sentence, it does not run
|
|
1295
|
+
* on into the line under it and the line over it does not run on into it.
|
|
1296
|
+
*
|
|
1297
|
+
* @param line The line.
|
|
1298
|
+
* @returns Whether it is a rule rather than words.
|
|
1299
|
+
*/
|
|
1300
|
+
function ruleOfCharacters(line) {
|
|
1301
|
+
const text = line.text.trim();
|
|
1302
|
+
if (text.length < RULE_CHARS) return false;
|
|
1303
|
+
for (const ch of text) if (!LEADER_CHARS.has(ch)) return false;
|
|
1304
|
+
return true;
|
|
1305
|
+
}
|
|
1306
|
+
/** How many of them it takes before a row of marks is a rule. */
|
|
1307
|
+
var RULE_CHARS = 3;
|
|
1308
|
+
function groupIntoParagraphs(lines, column, pageHeight = 0, before, eachLine = false, reach) {
|
|
964
1309
|
const groups = [];
|
|
965
1310
|
const gaps = [];
|
|
966
1311
|
let prev;
|
|
967
1312
|
for (const line of lines) {
|
|
968
1313
|
const gap = prev !== void 0 ? prev.y - line.y : 0;
|
|
969
1314
|
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)) {
|
|
1315
|
+
if (groups.length === 0 || eachLine || opened || ruleOfCharacters(line) || prev !== void 0 && (ruleOfCharacters(prev) || endedParagraph(prev, line, column) || reach !== void 0 && roomForWord(prev, line, reach) || carriesLeader(prev) || prev.tabbed === true)) {
|
|
971
1316
|
groups.push([]);
|
|
972
|
-
gaps.push(prev === void 0 ? 0 : gap);
|
|
1317
|
+
gaps.push(prev === void 0 ? before !== void 0 ? before.y - line.y : 0 : gap);
|
|
973
1318
|
}
|
|
974
1319
|
groups[groups.length - 1].push(line);
|
|
975
1320
|
prev = line;
|
|
976
1321
|
}
|
|
977
1322
|
const starts = lines.map((l) => l.x).sort((a, b) => a - b);
|
|
978
1323
|
const columnLeft = starts[Math.floor(starts.length * COLUMN_LEFT_QUANTILE)] ?? 0;
|
|
1324
|
+
const heights = [];
|
|
979
1325
|
return groups.map((g, i) => {
|
|
980
1326
|
const first = g[0];
|
|
981
1327
|
const fontSize = Math.max(...g.map((l) => l.fontSize));
|
|
982
|
-
const
|
|
1328
|
+
const inner = g.slice(1).map((l, k) => g[k].y - l.y);
|
|
1329
|
+
let lineHeight = Math.min(Math.max(inner.length > 0 ? median(inner) : fontSize * NATURAL_LINE_EM, fontSize), fontSize * TALLEST_LINE_EM);
|
|
1330
|
+
const above = i > 0 ? heights[i - 1] : before?.lineHeight;
|
|
1331
|
+
const gap = gaps[i] ?? 0;
|
|
1332
|
+
if (inner.length === 0 && above !== void 0) {
|
|
1333
|
+
const room = (gap - (1 - BASELINE_AT) * above) / BASELINE_AT;
|
|
1334
|
+
if (room < lineHeight) lineHeight = Math.max(room, fontSize);
|
|
1335
|
+
}
|
|
1336
|
+
heights.push(lineHeight);
|
|
1337
|
+
const opened = above !== void 0 ? gap - (1 - BASELINE_AT) * above - BASELINE_AT * lineHeight : 0;
|
|
983
1338
|
const most = pageHeight > 0 ? pageHeight / 3 : fontSize * 3;
|
|
984
|
-
const spacingBefore = opened >
|
|
1339
|
+
const spacingBefore = opened > SPACING_NOISE_PT ? Math.min(opened, most) : void 0;
|
|
1340
|
+
const stops = g.length === 1 ? first.stops ?? [] : [];
|
|
1341
|
+
const aligned = g.every((l) => lines.some((o) => !g.includes(o) && (Math.abs(o.x - l.x) <= STOP_SLACK_PT || (o.stops ?? []).some((x) => Math.abs(x - l.x) <= STOP_SLACK_PT)))) ? {} : alignmentOf(g, column);
|
|
985
1342
|
return {
|
|
986
1343
|
spans: joinLines(g),
|
|
987
1344
|
fontSize,
|
|
988
1345
|
top: first.y,
|
|
1346
|
+
bottom: g[g.length - 1].y,
|
|
1347
|
+
...stops.length > 0 ? {
|
|
1348
|
+
stops: stops.map((x) => x - (column?.left ?? columnLeft)),
|
|
1349
|
+
pieces: (first.pieces ?? []).map((p) => ({
|
|
1350
|
+
from: p.from - (column?.left ?? columnLeft),
|
|
1351
|
+
to: p.to - (column?.left ?? columnLeft)
|
|
1352
|
+
}))
|
|
1353
|
+
} : {},
|
|
989
1354
|
...spacingBefore !== void 0 ? { spacingBefore } : {},
|
|
990
|
-
|
|
991
|
-
...
|
|
1355
|
+
lineHeight,
|
|
1356
|
+
...aligned,
|
|
1357
|
+
...indentOf(g, columnLeft, aligned.alignment),
|
|
1358
|
+
lineCount: g.length,
|
|
1359
|
+
right: Math.max(...g.map((l) => l.x + l.width)),
|
|
1360
|
+
...opened > most ? { far: g } : {}
|
|
992
1361
|
};
|
|
993
1362
|
});
|
|
994
1363
|
}
|
|
995
1364
|
/**
|
|
1365
|
+
* Whether the first word of `next` would have fit on `prev` short of `reach` —
|
|
1366
|
+
* so the page did not break `prev` for want of room, and ended it.
|
|
1367
|
+
*
|
|
1368
|
+
* The width of the word is its share of its line's: a guess good to a letter,
|
|
1369
|
+
* and the margin it is compared with is the rest of the sheet.
|
|
1370
|
+
*/
|
|
1371
|
+
function roomForWord(prev, next, reach) {
|
|
1372
|
+
const text = next.text.trimStart();
|
|
1373
|
+
const word = text.split(/\s/u)[0] ?? "";
|
|
1374
|
+
if (word.length === 0) return false;
|
|
1375
|
+
const width = next.width * word.length / text.length;
|
|
1376
|
+
return reach - (prev.x + prev.width) > width + prev.fontSize * WORD_SPACE_EM;
|
|
1377
|
+
}
|
|
1378
|
+
/** A word space, in ems: what stands between a line's end and the word put after it. */
|
|
1379
|
+
var WORD_SPACE_EM = .3;
|
|
1380
|
+
/**
|
|
1381
|
+
* The exact box a paragraph's first line stands in, where the paragraph is a
|
|
1382
|
+
* line of text set to one (see {@link groupIntoParagraphs}).
|
|
1383
|
+
*/
|
|
1384
|
+
function leadingLine(el) {
|
|
1385
|
+
if (el.kind !== "paragraph" || el.paragraph.runs.length === 0) return void 0;
|
|
1386
|
+
const { spacingLine, spacingLineRule } = el.paragraph.properties;
|
|
1387
|
+
return spacingLineRule === "exact" && spacingLine !== void 0 && spacingLine > 0 ? spacingLine : void 0;
|
|
1388
|
+
}
|
|
1389
|
+
/** The tallest box a line is given, in ems — past it the gap is paragraph spacing. */
|
|
1390
|
+
var TALLEST_LINE_EM = 3;
|
|
1391
|
+
/** Spacing under this is the rounding of the page's own numbers, not white put there. */
|
|
1392
|
+
var SPACING_NOISE_PT = .25;
|
|
1393
|
+
/**
|
|
996
1394
|
* A paragraph's lines as one run of spans, joined the way the page broke them.
|
|
997
1395
|
*
|
|
998
1396
|
* A line that ends in a hyphen was broken THERE, and the break is not part of
|
|
@@ -1019,7 +1417,7 @@ function joinLines(lines) {
|
|
|
1019
1417
|
...prev,
|
|
1020
1418
|
text: ends.slice(0, -1)
|
|
1021
1419
|
};
|
|
1022
|
-
else if (!hard)
|
|
1420
|
+
else if (!hard && !/\s$/u.test(ends) && !/^\s/u.test(line.spans[0]?.text ?? "")) out.push(spaceAfter(prev));
|
|
1023
1421
|
}
|
|
1024
1422
|
out.push(...line.spans);
|
|
1025
1423
|
});
|
|
@@ -1073,6 +1471,69 @@ function indentOf(lines, columnLeft, alignment) {
|
|
|
1073
1471
|
};
|
|
1074
1472
|
}
|
|
1075
1473
|
/**
|
|
1474
|
+
* §17.3.1.12 — how far a line the page set in ONE piece may run on into the
|
|
1475
|
+
* right margin, rather than wrap.
|
|
1476
|
+
*
|
|
1477
|
+
* The line is re-set in a face this reader has, and a face wider than the
|
|
1478
|
+
* page's pushes the last word of a line that ran nearly to the measure onto a
|
|
1479
|
+
* line of its own: bug1252420.pdf's one italic line came back with
|
|
1480
|
+
* "Centuries" under it, and on bug1108301.pdf's sheet, fifty points tall, the
|
|
1481
|
+
* word it pushed down fell off the paper altogether. A line the page did not
|
|
1482
|
+
* break is not broken: it may run on — so far as a sixth of its measure, and
|
|
1483
|
+
* never past the edge of the sheet. Only a line set flush, with nothing on
|
|
1484
|
+
* stops: a centred one would move its centre, and a tabbed one its stops.
|
|
1485
|
+
*
|
|
1486
|
+
* @param para The paragraph.
|
|
1487
|
+
* @param measure The column it was set in.
|
|
1488
|
+
* @param pageWidth The sheet's width.
|
|
1489
|
+
* @returns How far it may run past the measure, in points; 0 where it keeps to it.
|
|
1490
|
+
*/
|
|
1491
|
+
function runOn(para, measure, pageWidth) {
|
|
1492
|
+
if (measure === void 0 || para.lineCount !== 1 || para.alignment !== void 0) return 0;
|
|
1493
|
+
if ((para.stops?.length ?? 0) > 0) return 0;
|
|
1494
|
+
const width = measure.right - measure.left;
|
|
1495
|
+
if (!(width > 0) || para.right - measure.left < width * FULL_LINE_SHARE) return 0;
|
|
1496
|
+
return Math.max(0, Math.min(width * RUN_ON_SHARE, pageWidth - measure.right));
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* §17.3.1.33 — the last text of a sheet, set farther below everything above it
|
|
1500
|
+
* than a paragraph's spacing may say, stands where the page set it.
|
|
1501
|
+
*
|
|
1502
|
+
* The spacing before a paragraph is held to a third of the sheet, so that a
|
|
1503
|
+
* gap misread cannot throw everything after it off the page. The last text on
|
|
1504
|
+
* the sheet has nothing after it to throw, and held to a third it came back
|
|
1505
|
+
* half way up the paper: bug1989304.pdf signs its sheet "World" at the foot
|
|
1506
|
+
* and we set it in the middle. Its lines are placed instead, each where the
|
|
1507
|
+
* page drew it.
|
|
1508
|
+
*
|
|
1509
|
+
* @param blocks The sheet's blocks, in the order they are read.
|
|
1510
|
+
* @param far The paragraphs whose spacing was held, with their lines.
|
|
1511
|
+
* @param place A line as the box that places it, `k` counting them.
|
|
1512
|
+
*/
|
|
1513
|
+
function setOff(blocks, far, place) {
|
|
1514
|
+
const after = (a, b) => (a.band - b.band || columnOrder(a.col) - columnOrder(b.col) || b.top - a.top) > 0;
|
|
1515
|
+
let at = -1;
|
|
1516
|
+
blocks.forEach((b, k) => {
|
|
1517
|
+
if (b.el.kind !== "paragraph" && b.el.kind !== "table") return;
|
|
1518
|
+
if (at < 0 || after(b, blocks[at])) at = k;
|
|
1519
|
+
});
|
|
1520
|
+
const block = at >= 0 ? blocks[at] : void 0;
|
|
1521
|
+
const lines = block ? far.get(block.el) : void 0;
|
|
1522
|
+
if (!block || !lines) return;
|
|
1523
|
+
blocks.splice(at, 1, ...lines.map((line, k) => ({
|
|
1524
|
+
band: block.band,
|
|
1525
|
+
col: block.col,
|
|
1526
|
+
top: line.y,
|
|
1527
|
+
el: place(line, k)
|
|
1528
|
+
})));
|
|
1529
|
+
}
|
|
1530
|
+
/** Where the placed last text stands in the page's painting order: over it all. */
|
|
1531
|
+
var FAR_Z = 1e6;
|
|
1532
|
+
/** How far across its measure a line runs before it is at risk of wrapping when re-set. */
|
|
1533
|
+
var FULL_LINE_SHARE = .85;
|
|
1534
|
+
/** How far past its measure, as a share of it, a one-piece line may run on. */
|
|
1535
|
+
var RUN_ON_SHARE = 1 / 6;
|
|
1536
|
+
/**
|
|
1076
1537
|
* How far, in ems, a paragraph has to be set in before it is indented rather
|
|
1077
1538
|
* than merely started. Half an em clears the rounding a producer leaves at the
|
|
1078
1539
|
* head of a line and is well under the smallest indent anybody sets.
|
|
@@ -1093,8 +1554,14 @@ var INDENT_EM = .5;
|
|
|
1093
1554
|
* placed rather than set — a centred title's every line is short of the measure
|
|
1094
1555
|
* and none of them ends anything.
|
|
1095
1556
|
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1557
|
+
* A line of a right-to-left script starts at the RIGHT and ends at the left,
|
|
1558
|
+
* and is read the other way round: ArabicCIDTrueType.pdf sets four lines, each
|
|
1559
|
+
* flush right and shorter than the one before, and read as a left-to-right
|
|
1560
|
+
* setting every pair of them ran together as one paragraph.
|
|
1561
|
+
*
|
|
1562
|
+
* @param prev The line before: where it starts, how wide it is, its face,
|
|
1563
|
+
* and its text when it has any.
|
|
1564
|
+
* @param next The line after — where it starts, and how wide it is.
|
|
1098
1565
|
* @param column The measure both were set in, when it is known.
|
|
1099
1566
|
* @returns Whether the first line ended a paragraph.
|
|
1100
1567
|
*/
|
|
@@ -1103,6 +1570,11 @@ function endedParagraph(prev, next, column) {
|
|
|
1103
1570
|
const width = column.right - column.left;
|
|
1104
1571
|
if (!(width > 0)) return false;
|
|
1105
1572
|
const step = Math.max(prev.fontSize, 4);
|
|
1573
|
+
if (prev.text !== void 0 && next.width !== void 0 && isRightToLeft(prev.text)) {
|
|
1574
|
+
const indent = prev.x + prev.width - (next.x + next.width);
|
|
1575
|
+
if (indent < -step || indent > step * INDENT_LINES) return false;
|
|
1576
|
+
return prev.x - column.left > width * .25;
|
|
1577
|
+
}
|
|
1106
1578
|
const shift = next.x - prev.x;
|
|
1107
1579
|
if (shift < -step || shift > step * INDENT_LINES) return false;
|
|
1108
1580
|
return column.right - (prev.x + prev.width) > width * .25;
|
|
@@ -1170,7 +1642,8 @@ function compareOrder(a, b) {
|
|
|
1170
1642
|
* `undefined` where the document repeats nothing.
|
|
1171
1643
|
*/
|
|
1172
1644
|
function runningFoot(pageRuns, shown, where) {
|
|
1173
|
-
const
|
|
1645
|
+
const textSize = textSizeOf(pageRuns);
|
|
1646
|
+
const feet = pageRuns.map((runs, i) => edgeLine(runs.filter((r) => !tooSmallToRead(r, textSize)), shown[i]?.height ?? 0, where));
|
|
1174
1647
|
const found = feet.filter((f) => f !== void 0);
|
|
1175
1648
|
if (found.length < 2 || found.length < pageRuns.length * FOOT_SHARE) return void 0;
|
|
1176
1649
|
const ys = found.map((f) => f.y);
|
|
@@ -1201,18 +1674,30 @@ var FOOT_DRIFT = 4;
|
|
|
1201
1674
|
* its own, and a two-page document came out as four.
|
|
1202
1675
|
*/
|
|
1203
1676
|
function edgeLine(runs, pageHeight, where) {
|
|
1204
|
-
|
|
1677
|
+
const head = where === "head";
|
|
1678
|
+
if (runs.length < (head ? HEAD_RUNS : 1) || pageHeight <= 0) return void 0;
|
|
1205
1679
|
const fontSize = median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
|
|
1206
1680
|
const rows = rowsOf(runs, fontSize);
|
|
1207
|
-
if (rows.length
|
|
1681
|
+
if (rows.length === 0) return void 0;
|
|
1208
1682
|
const at = (i) => where === "foot" ? rows[rows.length - 1 - i] : rows[i];
|
|
1209
1683
|
const edgeY = (row) => where === "foot" ? Math.max(...row.map((r) => r.y)) : Math.min(...row.map((r) => r.y));
|
|
1210
1684
|
const inside = (row) => where === "foot" ? Math.min(...row.map((r) => r.y)) : Math.max(...row.map((r) => r.y));
|
|
1211
1685
|
const y = edgeY(at(0));
|
|
1686
|
+
const inked = rows.filter((row) => row.some((r) => r.text.trim() !== ""));
|
|
1687
|
+
const inBand = inked.length > 0 && inked.every((row) => where === "foot" ? edgeY(row) <= pageHeight * FOOT_BAND : edgeY(row) >= pageHeight * (1 - FOOT_BAND));
|
|
1688
|
+
if (rows.length < 4 && (inBand || head)) {
|
|
1689
|
+
const all = inked.flat();
|
|
1690
|
+
const text = all.map((r) => r.text).join("").trim();
|
|
1691
|
+
return inBand && text.length > 0 && text.length <= FOOT_CHARS * inked.length ? {
|
|
1692
|
+
y: edgeY(inked[inked.length - 1] ?? []),
|
|
1693
|
+
runs: all
|
|
1694
|
+
} : void 0;
|
|
1695
|
+
}
|
|
1212
1696
|
let found;
|
|
1213
1697
|
let group = [];
|
|
1214
|
-
const most =
|
|
1215
|
-
|
|
1698
|
+
const most = head ? 1 : FOOT_ROWS;
|
|
1699
|
+
const above = head ? 2 : 1;
|
|
1700
|
+
for (let i = 0; i < Math.min(most, rows.length - above); i++) {
|
|
1216
1701
|
const row = at(i);
|
|
1217
1702
|
const edge = edgeY(row);
|
|
1218
1703
|
if (where === "foot" ? edge > pageHeight * FOOT_BAND : edge < pageHeight * (1 - FOOT_BAND)) break;
|
|
@@ -1231,6 +1716,8 @@ function edgeLine(runs, pageHeight, where) {
|
|
|
1231
1716
|
var FOOT_GAP_EM = 2;
|
|
1232
1717
|
/** How many lines a running foot may hold before it is a paragraph. */
|
|
1233
1718
|
var FOOT_ROWS = 3;
|
|
1719
|
+
/** How many runs a page must hold before its first line is asked whether it is a running head. */
|
|
1720
|
+
var HEAD_RUNS = 4;
|
|
1234
1721
|
/** How far up the sheet a running foot may sit. */
|
|
1235
1722
|
var FOOT_BAND = .12;
|
|
1236
1723
|
/** A running foot is a line, not a paragraph. */
|
|
@@ -1252,6 +1739,25 @@ function pageNumbered(run) {
|
|
|
1252
1739
|
const number = found[2];
|
|
1253
1740
|
const before = run.text.slice(0, at);
|
|
1254
1741
|
const after = run.text.slice(at + number.length);
|
|
1742
|
+
const total = /(^|\s)(\d{1,4})(\s|$)/u.exec(after);
|
|
1743
|
+
const tail = total === null ? after === "" ? [] : [{
|
|
1744
|
+
...run,
|
|
1745
|
+
text: after
|
|
1746
|
+
}] : [
|
|
1747
|
+
...after.slice(0, total.index + total[1].length) === "" ? [] : [{
|
|
1748
|
+
...run,
|
|
1749
|
+
text: after.slice(0, total.index + total[1].length)
|
|
1750
|
+
}],
|
|
1751
|
+
{
|
|
1752
|
+
...run,
|
|
1753
|
+
text: total[2],
|
|
1754
|
+
field: "NUMPAGES"
|
|
1755
|
+
},
|
|
1756
|
+
...after.slice(total.index + total[1].length + total[2].length) === "" ? [] : [{
|
|
1757
|
+
...run,
|
|
1758
|
+
text: after.slice(total.index + total[1].length + total[2].length)
|
|
1759
|
+
}]
|
|
1760
|
+
];
|
|
1255
1761
|
return [
|
|
1256
1762
|
...before === "" ? [] : [{
|
|
1257
1763
|
...run,
|
|
@@ -1262,10 +1768,7 @@ function pageNumbered(run) {
|
|
|
1262
1768
|
text: number,
|
|
1263
1769
|
field: "PAGE"
|
|
1264
1770
|
},
|
|
1265
|
-
...
|
|
1266
|
-
...run,
|
|
1267
|
-
text: after
|
|
1268
|
-
}]
|
|
1771
|
+
...tail
|
|
1269
1772
|
];
|
|
1270
1773
|
}
|
|
1271
1774
|
/**
|
|
@@ -1328,9 +1831,11 @@ function bandLine(row, fontSize, stepped, measure) {
|
|
|
1328
1831
|
const lines = pieces.filter((piece) => piece.length > 0).map((piece) => lineOf(piece, y, Math.max(...piece.map((r) => r.fontSizePt || fontSize)), stepped)).filter((line) => line.text.length > 0);
|
|
1329
1832
|
if (lines.length === 0) return void 0;
|
|
1330
1833
|
if (lines.length === 1 || lines.length > BAND_REGIONS || !measure) {
|
|
1331
|
-
const
|
|
1834
|
+
const only = lines.length === 1 ? lines[0] : void 0;
|
|
1835
|
+
const width = measure ? measure.right - measure.left : 0;
|
|
1836
|
+
const alignment = only !== void 0 && measure !== void 0 && measure.right - (only.x + only.width) <= width * BAND_RIGHT_SHARE && only.x - measure.left >= width * BAND_RIGHT_SHARE ? "right" : alignmentOf(lines.slice(0, 1), measure).alignment;
|
|
1332
1837
|
return {
|
|
1333
|
-
spans: lines.flatMap((line, i) => i === 0 ? line.spans : [
|
|
1838
|
+
spans: lines.flatMap((line, i) => i === 0 ? line.spans : [spaceAfter(lines[i - 1].spans.at(-1)), ...line.spans]),
|
|
1334
1839
|
properties: alignment ? { alignment } : {}
|
|
1335
1840
|
};
|
|
1336
1841
|
}
|
|
@@ -1404,6 +1909,420 @@ function looksRuled(runs, gutters, edges) {
|
|
|
1404
1909
|
return (fills[Math.floor(fills.length / 2)] ?? 1) <= TABLE_CELL_FILL;
|
|
1405
1910
|
}
|
|
1406
1911
|
/**
|
|
1912
|
+
* Whether the regions the gutters cut the page into are COLUMNS OF PROSE — the
|
|
1913
|
+
* only thing worth re-setting a document in columns for.
|
|
1914
|
+
*
|
|
1915
|
+
* A gutter is evidence about the page's white space, and white space is not
|
|
1916
|
+
* enough: an invoice sets its labels along the left and its amounts against the
|
|
1917
|
+
* right margin, and a dozen lines then break at the same x. Read as two columns
|
|
1918
|
+
* the amounts became a column of their own and every label was indented past
|
|
1919
|
+
* the strip it was given — "Total excluding tax" came back one letter per line.
|
|
1920
|
+
*
|
|
1921
|
+
* What tells them apart is the same thing that tells a cell from a line of
|
|
1922
|
+
* prose: a column of prose FILLS its measure, over and over, because that is
|
|
1923
|
+
* what wrapping does. A column of values never fills anything — its lines are
|
|
1924
|
+
* short and it is their right edges that line up, not their left. So every
|
|
1925
|
+
* region must hold lines of its own and they must reach across it.
|
|
1926
|
+
*
|
|
1927
|
+
* @param runs The page's runs.
|
|
1928
|
+
* @param gutters The page's gutters.
|
|
1929
|
+
* @param edges Where the page's text starts and ends.
|
|
1930
|
+
* @returns Whether the page is set in columns of prose.
|
|
1931
|
+
*/
|
|
1932
|
+
function proseColumns(runs, gutters, edges) {
|
|
1933
|
+
if (gutters.length === 0) return false;
|
|
1934
|
+
if (!edges) return true;
|
|
1935
|
+
const rows = rowsOf(runs, median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10).filter((row) => row.length > 0);
|
|
1936
|
+
if (rows.length === 0) return true;
|
|
1937
|
+
const bounds = columnBounds(runs, gutters, edges);
|
|
1938
|
+
const regions = bounds.slice(0, -1).map((lo, i) => [lo, bounds[i + 1]]);
|
|
1939
|
+
const regionOf = (x) => {
|
|
1940
|
+
for (let i = regions.length - 1; i >= 0; i--) if (x >= regions[i][0]) return i;
|
|
1941
|
+
return 0;
|
|
1942
|
+
};
|
|
1943
|
+
const edgesIn = regions.map(() => []);
|
|
1944
|
+
for (const row of rows) {
|
|
1945
|
+
const byRegion = regions.map(() => []);
|
|
1946
|
+
for (const run of row) byRegion[regionOf(run.x)].push(run);
|
|
1947
|
+
byRegion.forEach((inRegion, i) => {
|
|
1948
|
+
if (inRegion.length === 0) return;
|
|
1949
|
+
edgesIn[i].push({
|
|
1950
|
+
left: Math.min(...inRegion.map((r) => r.x)),
|
|
1951
|
+
right: Math.max(...inRegion.map((r) => r.endX))
|
|
1952
|
+
});
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
const agreeing = (xs) => {
|
|
1956
|
+
let most = 0;
|
|
1957
|
+
for (const x of xs) most = Math.max(most, xs.filter((y) => Math.abs(y - x) <= FLUSH_PT).length);
|
|
1958
|
+
return most;
|
|
1959
|
+
};
|
|
1960
|
+
return edgesIn.every((lines) => {
|
|
1961
|
+
if (lines.length < MIN_PROSE_LINES) return false;
|
|
1962
|
+
return agreeing(lines.map((l) => l.right)) <= agreeing(lines.map((l) => l.left));
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* The drawn glyphs that are STRAY marks in a line of ordinary text.
|
|
1967
|
+
*
|
|
1968
|
+
* §9.6.6 — a glyph the file states no character for is drawn, because nothing
|
|
1969
|
+
* downstream can write it. That is the whole content of some pages: a subset
|
|
1970
|
+
* that names its glyphs `g18`, a program with no `cmap`, a font of pictures.
|
|
1971
|
+
* On such a page the drawings ARE the words and they stay.
|
|
1972
|
+
*
|
|
1973
|
+
* A single mark among readable words is the other case, and Stripe's invoices
|
|
1974
|
+
* are full of them — a `/ToUnicode` stating U+0000 for every piece of
|
|
1975
|
+
* punctuation it sets. The flowing reading has nowhere to put a shape inside a
|
|
1976
|
+
* line, so the colon of "Kazakhstan VAT: 86-1696045" was placed as a floating
|
|
1977
|
+
* drawing and landed a word away, on a line of its own, pushing the text
|
|
1978
|
+
* around it aside. Dropped, the line keeps its words and the loss report still
|
|
1979
|
+
* says a character was unrecoverable.
|
|
1980
|
+
*
|
|
1981
|
+
* The line decides: where nearly everything on it is drawn, the drawings are
|
|
1982
|
+
* the text — bug1151216.pdf sets three lines of prices that way. Where one or
|
|
1983
|
+
* two marks stand among several readable runs, they are punctuation.
|
|
1984
|
+
*
|
|
1985
|
+
* @param vectors The page's painted paths.
|
|
1986
|
+
* @param runs The page's runs.
|
|
1987
|
+
* @returns The drawn glyphs to leave out of a flowing reading.
|
|
1988
|
+
*/
|
|
1989
|
+
function strayMarks(vectors, runs) {
|
|
1990
|
+
const glyphs = vectors.filter((v) => v.glyph === true);
|
|
1991
|
+
const out = /* @__PURE__ */ new Set();
|
|
1992
|
+
if (glyphs.length === 0) return out;
|
|
1993
|
+
const readable = runs.filter((r) => r.text.trim() !== "" && !r.text.includes("�"));
|
|
1994
|
+
for (const v of glyphs) {
|
|
1995
|
+
const mid = (v.minY + v.maxY) / 2;
|
|
1996
|
+
const words = readable.filter((r) => Math.abs(r.y - mid) <= (r.fontSizePt || 10));
|
|
1997
|
+
const size = words.length > 0 ? median(words.map((r) => r.fontSizePt || 10)) : v.maxY - v.minY || 10;
|
|
1998
|
+
const before = words.some((r) => r.endX <= v.minX + size);
|
|
1999
|
+
const after = words.some((r) => r.x >= v.maxX - size);
|
|
2000
|
+
if (!before && !after) continue;
|
|
2001
|
+
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);
|
|
2002
|
+
}
|
|
2003
|
+
return out;
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* §9.10.2 — the runs of a line with each glyph the file names no character for
|
|
2007
|
+
* read off the shape it draws (see {@link punctuationOf}), where it is one of
|
|
2008
|
+
* the stray marks inside a readable line.
|
|
2009
|
+
*
|
|
2010
|
+
* @param runs The page's runs.
|
|
2011
|
+
* @param strays The drawn glyphs that stand inside lines of readable text.
|
|
2012
|
+
* @returns The runs, with every mark that could be read written as text.
|
|
2013
|
+
*/
|
|
2014
|
+
function readDrawnMarks(runs, strays) {
|
|
2015
|
+
if (strays.size === 0) return [...runs];
|
|
2016
|
+
const marks = [...strays];
|
|
2017
|
+
return runs.map((run) => {
|
|
2018
|
+
if (!run.text.includes("�") || run.text.replaceAll("�", "").trim() !== "") return run;
|
|
2019
|
+
const size = run.fontSizePt || 10;
|
|
2020
|
+
const mark = marks.find((v) => {
|
|
2021
|
+
const x = (v.minX + v.maxX) / 2;
|
|
2022
|
+
return x >= run.x - .5 && x <= run.endX + .5 && Math.abs((v.minY + v.maxY) / 2 - run.y) <= size;
|
|
2023
|
+
});
|
|
2024
|
+
const read = mark ? punctuationOf(mark, run.y, size) : void 0;
|
|
2025
|
+
return read === void 0 ? run : {
|
|
2026
|
+
...run,
|
|
2027
|
+
text: run.text.replaceAll("�", read)
|
|
2028
|
+
};
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* §9.6.6 — the glyphs a page draws for want of characters, gathered into the
|
|
2033
|
+
* WORDS they stand in: a traced glyph that stands against the one before it,
|
|
2034
|
+
* on its line and in its colour, is drawn as one shape with it.
|
|
2035
|
+
*
|
|
2036
|
+
* Every mark kept apart is a shape of its own in the document, anchored to
|
|
2037
|
+
* the page — and a paper set in a subset whose glyphs name nothing is nothing
|
|
2038
|
+
* but such marks: TAMReview.pdf came back as forty-two thousand shapes, a
|
|
2039
|
+
* package no reader would open in under three minutes. The picture is the
|
|
2040
|
+
* same either way.
|
|
2041
|
+
*
|
|
2042
|
+
* @param vectors The page's painted paths, in painting order.
|
|
2043
|
+
* @returns The same paths, each run of traced glyphs made one.
|
|
2044
|
+
*/
|
|
2045
|
+
function drawnWords(vectors) {
|
|
2046
|
+
const out = [];
|
|
2047
|
+
for (const v of vectors) {
|
|
2048
|
+
const last = out[out.length - 1];
|
|
2049
|
+
const size = v.maxY - v.minY;
|
|
2050
|
+
if (v.glyph === true && last?.glyph === true && last.fillHex === v.fillHex && last.strokeHex === v.strokeHex && last.alpha === v.alpha && Math.min(last.maxY, v.maxY) - Math.max(last.minY, v.minY) > Math.min(size, last.maxY - last.minY) * .3 && v.minX >= last.minX && v.minX - last.maxX <= Math.max(size, last.maxY - last.minY) * WORD_GAP_EM) {
|
|
2051
|
+
out[out.length - 1] = {
|
|
2052
|
+
...last,
|
|
2053
|
+
segs: [...last.segs, ...v.segs],
|
|
2054
|
+
minX: Math.min(last.minX, v.minX),
|
|
2055
|
+
minY: Math.min(last.minY, v.minY),
|
|
2056
|
+
maxX: Math.max(last.maxX, v.maxX),
|
|
2057
|
+
maxY: Math.max(last.maxY, v.maxY)
|
|
2058
|
+
};
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
out.push(v);
|
|
2062
|
+
}
|
|
2063
|
+
return out;
|
|
2064
|
+
}
|
|
2065
|
+
/** How far apart, in heights of the taller glyph, two drawn glyphs may stand and be one word. */
|
|
2066
|
+
var WORD_GAP_EM = .6;
|
|
2067
|
+
/** How few drawn glyphs a line may hold before they are its text, not marks in it. */
|
|
2068
|
+
var MOST_STRAY_MARKS = 2;
|
|
2069
|
+
/** …and how many readable runs must stand on that line for them to be strays. */
|
|
2070
|
+
var LEAST_READABLE_RUNS = 4;
|
|
2071
|
+
/**
|
|
2072
|
+
* §17.4.38 — the consecutive lines a page set out on the SAME stops, as the
|
|
2073
|
+
* TABLE they are.
|
|
2074
|
+
*
|
|
2075
|
+
* A line broken at a gap no word space explains is a line the page laid out
|
|
2076
|
+
* (see {@link tabbed}); several of them one under another, broken in the same
|
|
2077
|
+
* places, is a table's head and its rows. An invoice's item table is exactly
|
|
2078
|
+
* that, and so is the payment history under it.
|
|
2079
|
+
*
|
|
2080
|
+
* Two stops at least, which is three columns: ONE stop is a contents entry with
|
|
2081
|
+
* its page number at the measure, and a list of those is a list.
|
|
2082
|
+
*
|
|
2083
|
+
* The stops do not have to agree to the point — a heading sits over its column
|
|
2084
|
+
* and a figure is set against the far side of it — so they are matched within
|
|
2085
|
+
* an em, and the column is put where the rows agree it is.
|
|
2086
|
+
*
|
|
2087
|
+
* @param paras The column's paragraphs, in order.
|
|
2088
|
+
* @param measure The measure they are set across.
|
|
2089
|
+
* @returns For each paragraph that belongs to a table: the table at its first
|
|
2090
|
+
* row, and `null` at the rows after it (their content is inside it).
|
|
2091
|
+
*/
|
|
2092
|
+
function tabbedRows(paras, measure) {
|
|
2093
|
+
const out = /* @__PURE__ */ new Map();
|
|
2094
|
+
if (!measure) return out;
|
|
2095
|
+
for (let i = 0; i < paras.length;) {
|
|
2096
|
+
const stops = paras[i]?.stops ?? [];
|
|
2097
|
+
if (stops.length < LEAST_TABLE_STOPS) {
|
|
2098
|
+
i++;
|
|
2099
|
+
continue;
|
|
2100
|
+
}
|
|
2101
|
+
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));
|
|
2102
|
+
let to = i + 1;
|
|
2103
|
+
while (to < paras.length && near(stops, paras[to]?.stops ?? [])) to++;
|
|
2104
|
+
if (to - i < 2) {
|
|
2105
|
+
i++;
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
const rows = paras.slice(i, to);
|
|
2109
|
+
const bounds = stops.map((_, k) => Math.min(...rows.map((r) => r.stops[k])));
|
|
2110
|
+
const width = measure.right - measure.left;
|
|
2111
|
+
const inks = [0, ...bounds].map((_, k) => {
|
|
2112
|
+
const cells = rows.map((r) => r.pieces?.[k]);
|
|
2113
|
+
if (cells.some((c) => c === void 0 || !Number.isFinite(c.from))) return void 0;
|
|
2114
|
+
return {
|
|
2115
|
+
from: Math.min(...cells.map((c) => c.from)),
|
|
2116
|
+
to: Math.max(...cells.map((c) => c.to))
|
|
2117
|
+
};
|
|
2118
|
+
});
|
|
2119
|
+
const flush = inks.map((ink, k) => {
|
|
2120
|
+
if (k === 0 || ink === void 0) return false;
|
|
2121
|
+
const tos = rows.map((r) => r.pieces[k].to);
|
|
2122
|
+
const froms = rows.map((r) => r.pieces[k].from);
|
|
2123
|
+
const even = Math.max(...tos) - Math.min(...tos);
|
|
2124
|
+
return even <= FLUSH_SLACK_PT && Math.max(...froms) - Math.min(...froms) > even + FLUSH_SLACK_PT;
|
|
2125
|
+
});
|
|
2126
|
+
const edges = [
|
|
2127
|
+
0,
|
|
2128
|
+
...bounds,
|
|
2129
|
+
width
|
|
2130
|
+
];
|
|
2131
|
+
for (let k = 1; k < bounds.length + 1; k++) {
|
|
2132
|
+
const before = inks[k - 1];
|
|
2133
|
+
const own = inks[k];
|
|
2134
|
+
if (!flush[k] || before === void 0 || own === void 0) continue;
|
|
2135
|
+
const mid = (before.to + own.from) / 2;
|
|
2136
|
+
if (mid > edges[k - 1] && mid < own.from) edges[k] = mid;
|
|
2137
|
+
}
|
|
2138
|
+
const flushRight = flush.map((right, k) => right ? Math.max(0, edges[k + 1] - inks[k].to) : void 0);
|
|
2139
|
+
const grid = edges.slice(0, -1).map((from, k) => pt(Math.max(edges[k + 1] - from, 1)));
|
|
2140
|
+
out.set(i, {
|
|
2141
|
+
kind: "table",
|
|
2142
|
+
table: {
|
|
2143
|
+
properties: {
|
|
2144
|
+
defaultCellMargins: {
|
|
2145
|
+
left: pt(0),
|
|
2146
|
+
right: pt(0)
|
|
2147
|
+
},
|
|
2148
|
+
layout: "fixed",
|
|
2149
|
+
widthType: "dxa",
|
|
2150
|
+
widthPt: pt(grid.reduce((sum, w) => sum + w, 0))
|
|
2151
|
+
},
|
|
2152
|
+
grid,
|
|
2153
|
+
rows: rows.map((row, r) => {
|
|
2154
|
+
const prev = rows[r - 1];
|
|
2155
|
+
const pitch = prev ? prev.top - row.top - (1 - BASELINE_AT) * prev.lineHeight - BASELINE_AT * row.lineHeight : 0;
|
|
2156
|
+
const opening = r === 0 ? row.spacingBefore : pitch > 0 ? pitch : void 0;
|
|
2157
|
+
return {
|
|
2158
|
+
properties: {},
|
|
2159
|
+
cells: splitAtTabs(row.spans).map((cell, k) => {
|
|
2160
|
+
const inset = flushRight[k];
|
|
2161
|
+
const own = grid[k];
|
|
2162
|
+
return {
|
|
2163
|
+
properties: own !== void 0 ? { width: own } : {},
|
|
2164
|
+
content: [paragraphFromRuns(cell, void 0, {
|
|
2165
|
+
...opening !== void 0 ? { spacingBefore: pt(opening) } : {},
|
|
2166
|
+
spacingLine: pt(row.lineHeight),
|
|
2167
|
+
spacingLineRule: "exact",
|
|
2168
|
+
...inset !== void 0 ? {
|
|
2169
|
+
alignment: "right",
|
|
2170
|
+
indentRight: pt(inset)
|
|
2171
|
+
} : {}
|
|
2172
|
+
})]
|
|
2173
|
+
};
|
|
2174
|
+
})
|
|
2175
|
+
};
|
|
2176
|
+
})
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2179
|
+
for (let k = i + 1; k < to; k++) out.set(k, null);
|
|
2180
|
+
i = to;
|
|
2181
|
+
}
|
|
2182
|
+
return out;
|
|
2183
|
+
}
|
|
2184
|
+
/** How far apart the ends of a column's cells may stand and still be flush. */
|
|
2185
|
+
var FLUSH_SLACK_PT = 2;
|
|
2186
|
+
/** How many stops a line must stand on before a run of them is a table. */
|
|
2187
|
+
var LEAST_TABLE_STOPS = 2;
|
|
2188
|
+
/**
|
|
2189
|
+
* How far two rows' stops may stand apart and still be one column.
|
|
2190
|
+
*
|
|
2191
|
+
* A heading is set over its column and a figure against the far side of it: an
|
|
2192
|
+
* invoice's "Qty" stands eight points left of the "1" under it, and the heading
|
|
2193
|
+
* is set two sizes smaller, so its own em is not a wide enough tolerance.
|
|
2194
|
+
*/
|
|
2195
|
+
var TABLE_STOP_SLACK_PT = 10;
|
|
2196
|
+
/** One line's spans cut into its cells, at the tabs the page set out. */
|
|
2197
|
+
function splitAtTabs(spans) {
|
|
2198
|
+
const cells = [[]];
|
|
2199
|
+
for (const span of spans) if (span.text === " ") cells.push([]);
|
|
2200
|
+
else cells[cells.length - 1].push(span);
|
|
2201
|
+
return cells;
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* Give each separator rule to the paragraph it separates, as that paragraph's
|
|
2205
|
+
* own border (§17.3.1.24), and say which rules were given away.
|
|
2206
|
+
*
|
|
2207
|
+
* A rule sits in the white between two blocks: under a table's headings, over a
|
|
2208
|
+
* total. The block BELOW it takes it as a top border, because that is the block
|
|
2209
|
+
* the rule introduces; where nothing follows closely enough, the block above
|
|
2210
|
+
* takes it as a bottom one.
|
|
2211
|
+
*
|
|
2212
|
+
* @param vectors The page's painted paths.
|
|
2213
|
+
* @param blocks The blocks read off the page so far, which the rule joins.
|
|
2214
|
+
* @param width The page's width, which a rule is long relative to.
|
|
2215
|
+
* @returns The rules that became borders, and so must not be drawn again.
|
|
2216
|
+
*/
|
|
2217
|
+
function ruleBorders(vectors, blocks, width) {
|
|
2218
|
+
const given = /* @__PURE__ */ new Set();
|
|
2219
|
+
const paragraphs = blocks.filter((b) => b.el.kind === "paragraph");
|
|
2220
|
+
if (paragraphs.length === 0) return given;
|
|
2221
|
+
for (const band of collinear(vectors.filter((v) => flatBar(v)))) {
|
|
2222
|
+
const v = band.pieces[0];
|
|
2223
|
+
if (band.to - band.from < width * RULE_SHARE) continue;
|
|
2224
|
+
const y = (v.minY + v.maxY) / 2;
|
|
2225
|
+
const below = paragraphs.filter((b) => b.top < y).sort((a, b) => b.top - a.top)[0];
|
|
2226
|
+
const above = paragraphs.filter((b) => b.top >= y).sort((a, b) => a.top - b.top)[0];
|
|
2227
|
+
const side = below !== void 0 && y - below.top <= RULE_REACH_PT ? {
|
|
2228
|
+
block: below,
|
|
2229
|
+
edge: "top"
|
|
2230
|
+
} : above !== void 0 && above.top - y <= RULE_REACH_PT ? {
|
|
2231
|
+
block: above,
|
|
2232
|
+
edge: "bottom"
|
|
2233
|
+
} : void 0;
|
|
2234
|
+
if (!side || side.block.el.kind !== "paragraph") continue;
|
|
2235
|
+
const border = {
|
|
2236
|
+
style: "single",
|
|
2237
|
+
width: pt(Math.max(v.lineWidth ?? v.maxY - v.minY, RULE_MIN_PT)),
|
|
2238
|
+
colorHex: v.strokeHex ?? v.fillHex ?? "000000"
|
|
2239
|
+
};
|
|
2240
|
+
const { paragraph } = side.block.el;
|
|
2241
|
+
const before = paragraph.properties.spacingBefore;
|
|
2242
|
+
const room = side.edge === "top" && before !== void 0 ? { spacingBefore: pt(Math.max(0, before - border.width)) } : {};
|
|
2243
|
+
side.block.el = {
|
|
2244
|
+
kind: "paragraph",
|
|
2245
|
+
paragraph: {
|
|
2246
|
+
...paragraph,
|
|
2247
|
+
properties: {
|
|
2248
|
+
...paragraph.properties,
|
|
2249
|
+
...room,
|
|
2250
|
+
borders: {
|
|
2251
|
+
...paragraph.properties.borders,
|
|
2252
|
+
[side.edge]: border,
|
|
2253
|
+
...side.edge === "top" ? { insideH: border } : {}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
for (const piece of band.pieces) given.add(piece);
|
|
2259
|
+
}
|
|
2260
|
+
return given;
|
|
2261
|
+
}
|
|
2262
|
+
/** How far from a paragraph a rule may stand and still belong to it. */
|
|
2263
|
+
var RULE_REACH_PT = 14;
|
|
2264
|
+
/** The thinnest a border may be drawn and still be seen. */
|
|
2265
|
+
var RULE_MIN_PT = .5;
|
|
2266
|
+
/**
|
|
2267
|
+
* Whether a painted path is flat and thin enough to be a piece of a RULE — a
|
|
2268
|
+
* line drawn to separate one block from the next, rather than a piece of the
|
|
2269
|
+
* page's artwork. How LONG the rule is is a question for the whole of it (see
|
|
2270
|
+
* {@link collinear}): a hairline the width of a column under a table's
|
|
2271
|
+
* headings, or the line a total is written over. Everything else — boxes,
|
|
2272
|
+
* panels, drawings — keeps the place on the page it was drawn at.
|
|
2273
|
+
*
|
|
2274
|
+
* @param v The painted path.
|
|
2275
|
+
* @returns Whether it could be a rule, or a piece of one.
|
|
2276
|
+
*/
|
|
2277
|
+
function flatBar(v) {
|
|
2278
|
+
const w = v.maxX - v.minX;
|
|
2279
|
+
const h = v.maxY - v.minY;
|
|
2280
|
+
return h <= RULE_THICK_PT && w > h * RULE_RATIO;
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* The flat bars of a page gathered into the rules they draw: pieces on one
|
|
2284
|
+
* baseline, end to end, are one rule from the first's left edge to the last's
|
|
2285
|
+
* right.
|
|
2286
|
+
*
|
|
2287
|
+
* @param bars The page's flat bars.
|
|
2288
|
+
* @returns One band per rule, with the pieces it was drawn in.
|
|
2289
|
+
*/
|
|
2290
|
+
function collinear(bars) {
|
|
2291
|
+
const out = [];
|
|
2292
|
+
for (const bar of [...bars].sort((a, b) => b.maxY - a.maxY || a.minX - b.minX)) {
|
|
2293
|
+
const y = (bar.minY + bar.maxY) / 2;
|
|
2294
|
+
const band = out.find((b) => Math.abs((b.pieces[0].minY + b.pieces[0].maxY) / 2 - y) <= RULE_SAME_LINE_PT && bar.minX <= b.to + RULE_JOIN_PT && (bar.strokeHex ?? bar.fillHex) === (b.pieces[0].strokeHex ?? b.pieces[0].fillHex));
|
|
2295
|
+
if (band) {
|
|
2296
|
+
band.from = Math.min(band.from, bar.minX);
|
|
2297
|
+
band.to = Math.max(band.to, bar.maxX);
|
|
2298
|
+
band.pieces.push(bar);
|
|
2299
|
+
} else out.push({
|
|
2300
|
+
from: bar.minX,
|
|
2301
|
+
to: bar.maxX,
|
|
2302
|
+
pieces: [bar]
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
return out;
|
|
2306
|
+
}
|
|
2307
|
+
/** How far apart two bars' baselines may be and still be one rule. */
|
|
2308
|
+
var RULE_SAME_LINE_PT = 1.5;
|
|
2309
|
+
/**
|
|
2310
|
+
* …and how wide a gap between two pieces of it the page may leave: an invoice
|
|
2311
|
+
* draws the rule under its item row cell by cell and skips the thirteen points
|
|
2312
|
+
* of gutter between two of them.
|
|
2313
|
+
*/
|
|
2314
|
+
var RULE_JOIN_PT = 18;
|
|
2315
|
+
/** How thick a mark may be and still be a rule. */
|
|
2316
|
+
var RULE_THICK_PT = 2;
|
|
2317
|
+
/** How much of the sheet it must run across. */
|
|
2318
|
+
var RULE_SHARE = .2;
|
|
2319
|
+
/** …and how much longer than it is thick. */
|
|
2320
|
+
var RULE_RATIO = 20;
|
|
2321
|
+
/** How close two edges stand before they count as the same one. */
|
|
2322
|
+
var FLUSH_PT = 1;
|
|
2323
|
+
/** And how many lines it takes before a region is a column at all. */
|
|
2324
|
+
var MIN_PROSE_LINES = 6;
|
|
2325
|
+
/**
|
|
1407
2326
|
* §17.4.38 — the page's rows as the TABLE they are.
|
|
1408
2327
|
*
|
|
1409
2328
|
* The regions between the gutters are the columns and the rows are the rows;
|