reamkit 1.29.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/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 +5 -2
- package/dist/esm/pdf-reader/content.d.ts +17 -4
- package/dist/esm/pdf-reader/content.js +163 -11
- package/dist/esm/pdf-reader/display.js +16 -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 +96 -5
- package/dist/esm/pdf-reader/flow-build.js +210 -23
- 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 +638 -92
- 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 +8 -2
- package/dist/esm/pdf-reader/vector.js +34 -5
- package/dist/esm/word/docx-writer.js +137 -36
- 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,10 @@ 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;
|
|
39
45
|
const pageMarks = pages.map(() => []);
|
|
40
46
|
const medianFont = median(pageRuns.flat().map((r) => r.fontSizePt).filter((s) => s > 0)) || 12;
|
|
41
47
|
const resources = new ResourceStore();
|
|
@@ -62,6 +68,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
62
68
|
const sectionEnds = [];
|
|
63
69
|
let sectionFrom = 0;
|
|
64
70
|
let lastSize = "";
|
|
71
|
+
const leads = [];
|
|
65
72
|
let curColumns = 1;
|
|
66
73
|
let curSpace = 0;
|
|
67
74
|
let pendingContinuous = false;
|
|
@@ -80,11 +87,11 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
80
87
|
const addColumn = (allRuns, col) => {
|
|
81
88
|
const colRuns = allRuns.filter((r) => r.type3 !== true && (mode !== "positional" || r.invisible !== true));
|
|
82
89
|
if (mode === "positional") {
|
|
83
|
-
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)) {
|
|
84
91
|
if (line.text.length === 0) continue;
|
|
85
92
|
const box = turnedBox(line, angle, pageWidth);
|
|
86
93
|
placed.push({
|
|
87
|
-
key:
|
|
94
|
+
key: placedKey(runs, placed.length),
|
|
88
95
|
col,
|
|
89
96
|
top: box.y + box.height,
|
|
90
97
|
make: (z) => positionedText(line.spans, box, frame, z, rotation60kOf(angle))
|
|
@@ -95,7 +102,8 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
95
102
|
const columnSize = median(colRuns.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
|
|
96
103
|
const maths = matrixBlocks(colRuns, columnSize);
|
|
97
104
|
const inMath = new Set(maths.flatMap((m) => [...m.used]));
|
|
98
|
-
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);
|
|
99
107
|
const measure = measureOf(col, gutters, textEdges) ?? measureOfLines(lines);
|
|
100
108
|
for (const found of maths) {
|
|
101
109
|
const { alignment } = alignmentOf([{
|
|
@@ -123,38 +131,153 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
123
131
|
}
|
|
124
132
|
});
|
|
125
133
|
}
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 = [];
|
|
131
176
|
for (const [at, para] of paras.entries()) {
|
|
132
177
|
const table = asRows.get(at);
|
|
133
178
|
if (table !== void 0) {
|
|
134
|
-
if (table !== null)
|
|
135
|
-
band: bandAt(para.top),
|
|
136
|
-
col,
|
|
179
|
+
if (table !== null) out.push({
|
|
137
180
|
top: para.top,
|
|
138
181
|
el: table
|
|
139
182
|
});
|
|
140
183
|
continue;
|
|
141
184
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
})) } : {},
|
|
191
|
+
...para.alignment !== void 0 ? { alignment: para.alignment } : {},
|
|
192
|
+
...para.spacingBefore !== void 0 ? { spacingBefore: pt(para.spacingBefore) } : {},
|
|
193
|
+
spacingLine: pt(para.lineHeight),
|
|
194
|
+
spacingLineRule: "exact",
|
|
195
|
+
...para.indentLeft !== void 0 ? { indentLeft: pt(para.indentLeft) } : {},
|
|
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({
|
|
145
201
|
top: para.top,
|
|
146
|
-
el
|
|
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
|
-
})
|
|
202
|
+
el
|
|
156
203
|
});
|
|
157
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
|
+
};
|
|
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
|
+
};
|
|
158
281
|
};
|
|
159
282
|
const placed = [];
|
|
160
283
|
const colOf = (centerX) => gutters.filter((g) => centerX >= g.mid).length;
|
|
@@ -162,6 +285,16 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
162
285
|
left: 0,
|
|
163
286
|
top: display.height
|
|
164
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
|
+
}
|
|
165
298
|
const raw = collectPageImages(file, page);
|
|
166
299
|
const imgs = {
|
|
167
300
|
images: placeImages(raw.images, display),
|
|
@@ -177,9 +310,14 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
177
310
|
})));
|
|
178
311
|
losses.push(...lifted.losses);
|
|
179
312
|
const placedVectors = placeVectors(lifted.vectors, display);
|
|
180
|
-
const
|
|
313
|
+
const drawnRules = markDrawnRules(runs, placedVectors);
|
|
181
314
|
const strayGlyphs = strayMarks(placedVectors, runs);
|
|
315
|
+
const ruled = mode !== "positional" ? {
|
|
316
|
+
...drawnRules,
|
|
317
|
+
runs: readDrawnMarks(drawnRules.runs, strayGlyphs)
|
|
318
|
+
} : drawnRules;
|
|
182
319
|
const vectors = placedVectors.filter((v) => !ruled.consumed.has(v)).filter((v) => !(mode !== "positional" && strayGlyphs.has(v)));
|
|
320
|
+
const drawn = drawnWords(vectors);
|
|
183
321
|
const ruledIntoColumns = mode !== "positional" && looksRuled(ruled.runs, gutters, textEdges);
|
|
184
322
|
const asTable = ruledIntoColumns && textEdges ? tableFrom(ruled.runs, gutters, textEdges, stepped) : void 0;
|
|
185
323
|
if (asTable) for (const block of asTable) blocks.push({
|
|
@@ -194,6 +332,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
194
332
|
for (const col of [SPANNING_COLUMN, ...columns]) addColumn(ruled.runs.filter((r) => columnFor(r) === col), col);
|
|
195
333
|
} else addColumn(ruled.runs, 0);
|
|
196
334
|
const under = mode !== "positional";
|
|
335
|
+
setOff(blocks, far, (line, k) => positionedText(line.spans, turnedBox(line, 0, pageWidth), frame, FAR_Z + k));
|
|
197
336
|
const givenAway = mode !== "positional" ? ruleBorders(vectors, blocks, display.width) : void 0;
|
|
198
337
|
[
|
|
199
338
|
...imgs.images.map((img) => ({
|
|
@@ -202,7 +341,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
202
341
|
top: img.y + img.heightPt,
|
|
203
342
|
make: (z) => imageBlock(img, resources, void 0, frame, z, under)
|
|
204
343
|
})),
|
|
205
|
-
...
|
|
344
|
+
...drawn.filter((v) => givenAway?.has(v) !== true).map((v) => ({
|
|
206
345
|
key: v.orderKey,
|
|
207
346
|
col: colOf((v.minX + v.maxX) / 2),
|
|
208
347
|
top: v.maxY,
|
|
@@ -233,12 +372,22 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
233
372
|
pendingContinuous = false;
|
|
234
373
|
}
|
|
235
374
|
lastSize = size;
|
|
236
|
-
if (i > 0 && !opensSection
|
|
375
|
+
if (i > 0 && !opensSection) body.push({
|
|
237
376
|
kind: "paragraph",
|
|
238
377
|
paragraph: {
|
|
239
378
|
properties: {
|
|
240
379
|
pageBreakBefore: true,
|
|
241
|
-
spacingLine:
|
|
380
|
+
spacingLine: CARRIER_LINE_PT,
|
|
381
|
+
spacingLineRule: "exact"
|
|
382
|
+
},
|
|
383
|
+
runs: []
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
else if (i === 0 && blocks.length === 0) body.push({
|
|
387
|
+
kind: "paragraph",
|
|
388
|
+
paragraph: {
|
|
389
|
+
properties: {
|
|
390
|
+
spacingLine: CARRIER_LINE_PT,
|
|
242
391
|
spacingLineRule: "exact"
|
|
243
392
|
},
|
|
244
393
|
runs: []
|
|
@@ -246,7 +395,18 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
246
395
|
});
|
|
247
396
|
const columnsHere = ruledIntoColumns || !proseColumns(runs, gutters, textEdges) ? 1 : gutters.length + 1;
|
|
248
397
|
const spacePt = columnsHere > 1 ? median(gutters.map((g) => g.to - g.from)) : 0;
|
|
398
|
+
let led = false;
|
|
249
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
|
+
}
|
|
250
410
|
const count = block.col === SPANNING_COLUMN ? 1 : columnsHere;
|
|
251
411
|
if (count !== curColumns) {
|
|
252
412
|
sectionEnds.push({
|
|
@@ -267,7 +427,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
267
427
|
});
|
|
268
428
|
const setUp = (from, to) => {
|
|
269
429
|
const own = sectionFromPdfPages(pages.slice(from, to));
|
|
270
|
-
return mode === "positional" ? own : withMeasuredMargins(own, shown.slice(from, to), pageRuns.slice(from, to), pageMarks.slice(from, to));
|
|
430
|
+
return mode === "positional" ? own : withMeasuredMargins(own, shown.slice(from, to), pageRuns.slice(from, to), pageMarks.slice(from, to), foot?.band);
|
|
271
431
|
};
|
|
272
432
|
sectionEnds.push({
|
|
273
433
|
at: body.length,
|
|
@@ -292,6 +452,25 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
292
452
|
endIndex: end.at
|
|
293
453
|
}];
|
|
294
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
|
+
}
|
|
295
474
|
const stepped0 = stepsBetweenWords(allRuns[0] ?? []);
|
|
296
475
|
const edges0 = pageTextEdges(allRuns[0] ?? []);
|
|
297
476
|
const band = foot ? footerBand(foot.band, stepped0, edges0, foot.numbered) : [];
|
|
@@ -311,7 +490,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
311
490
|
doc: buildFlowDoc(body, resources, withFooter(setUp(0, pages.length)), collectEmbeddedFonts(file, pages, losses), sections.map((s) => ({
|
|
312
491
|
...s,
|
|
313
492
|
properties: withFooter(s.properties) ?? s.properties
|
|
314
|
-
})), 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)),
|
|
315
494
|
losses: dedupeLosses(losses)
|
|
316
495
|
};
|
|
317
496
|
}
|
|
@@ -691,6 +870,32 @@ function bandOf(breaks, top, epsilon) {
|
|
|
691
870
|
* does this — a flowing paragraph is meant to be read across.
|
|
692
871
|
*/
|
|
693
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
|
+
}
|
|
694
899
|
/** Runs by the direction of their baseline, upright first, each angle rounded. */
|
|
695
900
|
function byAngle(runs) {
|
|
696
901
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -777,10 +982,12 @@ function groupIntoLines(runs, split = false, stepped = false) {
|
|
|
777
982
|
runs: [run]
|
|
778
983
|
});
|
|
779
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));
|
|
780
987
|
return clusters.flatMap((c) => {
|
|
781
|
-
const ordered = c.runs
|
|
988
|
+
const ordered = c.runs;
|
|
782
989
|
const fontSize = c.fontSize || 10;
|
|
783
|
-
if (!split) return [lineOf(ordered, c.y, fontSize, stepped)];
|
|
990
|
+
if (!split) return [lineOf(ordered, c.y, fontSize, stepped, stops)];
|
|
784
991
|
const pieces = [[]];
|
|
785
992
|
for (const run of ordered) {
|
|
786
993
|
const prev = pieces[pieces.length - 1];
|
|
@@ -789,10 +996,50 @@ function groupIntoLines(runs, split = false, stepped = false) {
|
|
|
789
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([]);
|
|
790
997
|
pieces[pieces.length - 1].push(run);
|
|
791
998
|
}
|
|
792
|
-
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));
|
|
793
1000
|
});
|
|
794
1001
|
}
|
|
795
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
|
+
}
|
|
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;
|
|
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;
|
|
1042
|
+
/**
|
|
796
1043
|
* Where a line's INK is, which is not how far its pen travelled.
|
|
797
1044
|
*
|
|
798
1045
|
* A run of spaces advances the pen and marks nothing. basicapi.pdf sets its
|
|
@@ -824,8 +1071,8 @@ function inkSpan(runs) {
|
|
|
824
1071
|
};
|
|
825
1072
|
}
|
|
826
1073
|
/** One run of runs, left to right on a shared baseline, as a {@link Line}. */
|
|
827
|
-
function lineOf(runs, y, fontSize, stepped) {
|
|
828
|
-
const { spans: ordered, stops } = 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);
|
|
829
1076
|
const spans = ordered.every((s) => s.text.trim() === "" || isRightToLeft(s.text)) ? [...ordered].reverse() : ordered;
|
|
830
1077
|
const ink = inkSpan(runs);
|
|
831
1078
|
return {
|
|
@@ -833,8 +1080,11 @@ function lineOf(runs, y, fontSize, stepped) {
|
|
|
833
1080
|
width: ink.width,
|
|
834
1081
|
y,
|
|
835
1082
|
fontSize,
|
|
836
|
-
...tabbed(runs, fontSize) ? { tabbed: true } : {},
|
|
837
|
-
...stops.length > 0 ? {
|
|
1083
|
+
...tabbed(runs, fontSize) || stops.length > 0 ? { tabbed: true } : {},
|
|
1084
|
+
...stops.length > 0 ? {
|
|
1085
|
+
stops,
|
|
1086
|
+
pieces
|
|
1087
|
+
} : {},
|
|
838
1088
|
text: spans.map((s) => s.text).join("").replace(/\s+/g, " ").trim(),
|
|
839
1089
|
spans
|
|
840
1090
|
};
|
|
@@ -930,17 +1180,35 @@ function stepsBetweenWords(runs) {
|
|
|
930
1180
|
if (runs.length < 8) return false;
|
|
931
1181
|
return runs.filter((r) => /\s/u.test(r.text)).length / runs.length < .05;
|
|
932
1182
|
}
|
|
933
|
-
function lineSpans(runs, fontSize, stepped) {
|
|
1183
|
+
function lineSpans(runs, fontSize, stepped, shared = []) {
|
|
934
1184
|
const spans = [];
|
|
935
1185
|
const stops = [];
|
|
1186
|
+
const pieces = [{
|
|
1187
|
+
from: Infinity,
|
|
1188
|
+
to: -Infinity
|
|
1189
|
+
}];
|
|
936
1190
|
const body = runs.filter((r) => (r.fontSizePt || fontSize) > fontSize * SCRIPT_SIZE);
|
|
937
1191
|
const baseline = median((body.length > 0 ? body : runs).map((r) => r.y));
|
|
938
1192
|
let prev;
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
+
}
|
|
944
1212
|
spans.push({
|
|
945
1213
|
text: run.text.replaceAll("�", ""),
|
|
946
1214
|
sizePt: run.fontSizePt,
|
|
@@ -957,10 +1225,17 @@ function lineSpans(runs, fontSize, stepped) {
|
|
|
957
1225
|
...run.href !== void 0 ? { href: run.href } : {}
|
|
958
1226
|
});
|
|
959
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
|
+
}
|
|
960
1234
|
}
|
|
961
1235
|
return {
|
|
962
1236
|
spans,
|
|
963
|
-
stops
|
|
1237
|
+
stops,
|
|
1238
|
+
pieces
|
|
964
1239
|
};
|
|
965
1240
|
}
|
|
966
1241
|
/**
|
|
@@ -1030,41 +1305,92 @@ function ruleOfCharacters(line) {
|
|
|
1030
1305
|
}
|
|
1031
1306
|
/** How many of them it takes before a row of marks is a rule. */
|
|
1032
1307
|
var RULE_CHARS = 3;
|
|
1033
|
-
function groupIntoParagraphs(lines, column, pageHeight = 0) {
|
|
1308
|
+
function groupIntoParagraphs(lines, column, pageHeight = 0, before, eachLine = false, reach) {
|
|
1034
1309
|
const groups = [];
|
|
1035
1310
|
const gaps = [];
|
|
1036
1311
|
let prev;
|
|
1037
1312
|
for (const line of lines) {
|
|
1038
1313
|
const gap = prev !== void 0 ? prev.y - line.y : 0;
|
|
1039
1314
|
const opened = prev !== void 0 && gap > line.fontSize * 1.5;
|
|
1040
|
-
if (groups.length === 0 || opened || ruleOfCharacters(line) || prev !== void 0 && (ruleOfCharacters(prev) || 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)) {
|
|
1041
1316
|
groups.push([]);
|
|
1042
|
-
gaps.push(prev === void 0 ? 0 : gap);
|
|
1317
|
+
gaps.push(prev === void 0 ? before !== void 0 ? before.y - line.y : 0 : gap);
|
|
1043
1318
|
}
|
|
1044
1319
|
groups[groups.length - 1].push(line);
|
|
1045
1320
|
prev = line;
|
|
1046
1321
|
}
|
|
1047
1322
|
const starts = lines.map((l) => l.x).sort((a, b) => a - b);
|
|
1048
1323
|
const columnLeft = starts[Math.floor(starts.length * COLUMN_LEFT_QUANTILE)] ?? 0;
|
|
1324
|
+
const heights = [];
|
|
1049
1325
|
return groups.map((g, i) => {
|
|
1050
1326
|
const first = g[0];
|
|
1051
1327
|
const fontSize = Math.max(...g.map((l) => l.fontSize));
|
|
1052
|
-
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;
|
|
1053
1338
|
const most = pageHeight > 0 ? pageHeight / 3 : fontSize * 3;
|
|
1054
|
-
const spacingBefore = opened >
|
|
1339
|
+
const spacingBefore = opened > SPACING_NOISE_PT ? Math.min(opened, most) : void 0;
|
|
1055
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);
|
|
1056
1342
|
return {
|
|
1057
1343
|
spans: joinLines(g),
|
|
1058
1344
|
fontSize,
|
|
1059
1345
|
top: first.y,
|
|
1060
|
-
|
|
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
|
+
} : {},
|
|
1061
1354
|
...spacingBefore !== void 0 ? { spacingBefore } : {},
|
|
1062
|
-
|
|
1063
|
-
...
|
|
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 } : {}
|
|
1064
1361
|
};
|
|
1065
1362
|
});
|
|
1066
1363
|
}
|
|
1067
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
|
+
/**
|
|
1068
1394
|
* A paragraph's lines as one run of spans, joined the way the page broke them.
|
|
1069
1395
|
*
|
|
1070
1396
|
* A line that ends in a hyphen was broken THERE, and the break is not part of
|
|
@@ -1091,7 +1417,7 @@ function joinLines(lines) {
|
|
|
1091
1417
|
...prev,
|
|
1092
1418
|
text: ends.slice(0, -1)
|
|
1093
1419
|
};
|
|
1094
|
-
else if (!hard)
|
|
1420
|
+
else if (!hard && !/\s$/u.test(ends) && !/^\s/u.test(line.spans[0]?.text ?? "")) out.push(spaceAfter(prev));
|
|
1095
1421
|
}
|
|
1096
1422
|
out.push(...line.spans);
|
|
1097
1423
|
});
|
|
@@ -1145,6 +1471,69 @@ function indentOf(lines, columnLeft, alignment) {
|
|
|
1145
1471
|
};
|
|
1146
1472
|
}
|
|
1147
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
|
+
/**
|
|
1148
1537
|
* How far, in ems, a paragraph has to be set in before it is indented rather
|
|
1149
1538
|
* than merely started. Half an em clears the rounding a producer leaves at the
|
|
1150
1539
|
* head of a line and is well under the smallest indent anybody sets.
|
|
@@ -1165,8 +1554,14 @@ var INDENT_EM = .5;
|
|
|
1165
1554
|
* placed rather than set — a centred title's every line is short of the measure
|
|
1166
1555
|
* and none of them ends anything.
|
|
1167
1556
|
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
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.
|
|
1170
1565
|
* @param column The measure both were set in, when it is known.
|
|
1171
1566
|
* @returns Whether the first line ended a paragraph.
|
|
1172
1567
|
*/
|
|
@@ -1175,6 +1570,11 @@ function endedParagraph(prev, next, column) {
|
|
|
1175
1570
|
const width = column.right - column.left;
|
|
1176
1571
|
if (!(width > 0)) return false;
|
|
1177
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
|
+
}
|
|
1178
1578
|
const shift = next.x - prev.x;
|
|
1179
1579
|
if (shift < -step || shift > step * INDENT_LINES) return false;
|
|
1180
1580
|
return column.right - (prev.x + prev.width) > width * .25;
|
|
@@ -1242,7 +1642,8 @@ function compareOrder(a, b) {
|
|
|
1242
1642
|
* `undefined` where the document repeats nothing.
|
|
1243
1643
|
*/
|
|
1244
1644
|
function runningFoot(pageRuns, shown, where) {
|
|
1245
|
-
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));
|
|
1246
1647
|
const found = feet.filter((f) => f !== void 0);
|
|
1247
1648
|
if (found.length < 2 || found.length < pageRuns.length * FOOT_SHARE) return void 0;
|
|
1248
1649
|
const ys = found.map((f) => f.y);
|
|
@@ -1273,7 +1674,8 @@ var FOOT_DRIFT = 4;
|
|
|
1273
1674
|
* its own, and a two-page document came out as four.
|
|
1274
1675
|
*/
|
|
1275
1676
|
function edgeLine(runs, pageHeight, where) {
|
|
1276
|
-
|
|
1677
|
+
const head = where === "head";
|
|
1678
|
+
if (runs.length < (head ? HEAD_RUNS : 1) || pageHeight <= 0) return void 0;
|
|
1277
1679
|
const fontSize = median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
|
|
1278
1680
|
const rows = rowsOf(runs, fontSize);
|
|
1279
1681
|
if (rows.length === 0) return void 0;
|
|
@@ -1281,9 +1683,9 @@ function edgeLine(runs, pageHeight, where) {
|
|
|
1281
1683
|
const edgeY = (row) => where === "foot" ? Math.max(...row.map((r) => r.y)) : Math.min(...row.map((r) => r.y));
|
|
1282
1684
|
const inside = (row) => where === "foot" ? Math.min(...row.map((r) => r.y)) : Math.max(...row.map((r) => r.y));
|
|
1283
1685
|
const y = edgeY(at(0));
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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)) {
|
|
1287
1689
|
const all = inked.flat();
|
|
1288
1690
|
const text = all.map((r) => r.text).join("").trim();
|
|
1289
1691
|
return inBand && text.length > 0 && text.length <= FOOT_CHARS * inked.length ? {
|
|
@@ -1293,8 +1695,9 @@ function edgeLine(runs, pageHeight, where) {
|
|
|
1293
1695
|
}
|
|
1294
1696
|
let found;
|
|
1295
1697
|
let group = [];
|
|
1296
|
-
const most =
|
|
1297
|
-
|
|
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++) {
|
|
1298
1701
|
const row = at(i);
|
|
1299
1702
|
const edge = edgeY(row);
|
|
1300
1703
|
if (where === "foot" ? edge > pageHeight * FOOT_BAND : edge < pageHeight * (1 - FOOT_BAND)) break;
|
|
@@ -1313,6 +1716,8 @@ function edgeLine(runs, pageHeight, where) {
|
|
|
1313
1716
|
var FOOT_GAP_EM = 2;
|
|
1314
1717
|
/** How many lines a running foot may hold before it is a paragraph. */
|
|
1315
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;
|
|
1316
1721
|
/** How far up the sheet a running foot may sit. */
|
|
1317
1722
|
var FOOT_BAND = .12;
|
|
1318
1723
|
/** A running foot is a line, not a paragraph. */
|
|
@@ -1426,9 +1831,11 @@ function bandLine(row, fontSize, stepped, measure) {
|
|
|
1426
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);
|
|
1427
1832
|
if (lines.length === 0) return void 0;
|
|
1428
1833
|
if (lines.length === 1 || lines.length > BAND_REGIONS || !measure) {
|
|
1429
|
-
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;
|
|
1430
1837
|
return {
|
|
1431
|
-
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]),
|
|
1432
1839
|
properties: alignment ? { alignment } : {}
|
|
1433
1840
|
};
|
|
1434
1841
|
}
|
|
@@ -1586,8 +1993,8 @@ function strayMarks(vectors, runs) {
|
|
|
1586
1993
|
const readable = runs.filter((r) => r.text.trim() !== "" && !r.text.includes("�"));
|
|
1587
1994
|
for (const v of glyphs) {
|
|
1588
1995
|
const mid = (v.minY + v.maxY) / 2;
|
|
1589
|
-
const
|
|
1590
|
-
const
|
|
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;
|
|
1591
1998
|
const before = words.some((r) => r.endX <= v.minX + size);
|
|
1592
1999
|
const after = words.some((r) => r.x >= v.maxX - size);
|
|
1593
2000
|
if (!before && !after) continue;
|
|
@@ -1595,6 +2002,68 @@ function strayMarks(vectors, runs) {
|
|
|
1595
2002
|
}
|
|
1596
2003
|
return out;
|
|
1597
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;
|
|
1598
2067
|
/** How few drawn glyphs a line may hold before they are its text, not marks in it. */
|
|
1599
2068
|
var MOST_STRAY_MARKS = 2;
|
|
1600
2069
|
/** …and how many readable runs must stand on that line for them to be strays. */
|
|
@@ -1639,11 +2108,35 @@ function tabbedRows(paras, measure) {
|
|
|
1639
2108
|
const rows = paras.slice(i, to);
|
|
1640
2109
|
const bounds = stops.map((_, k) => Math.min(...rows.map((r) => r.stops[k])));
|
|
1641
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
|
+
});
|
|
1642
2126
|
const edges = [
|
|
1643
2127
|
0,
|
|
1644
2128
|
...bounds,
|
|
1645
2129
|
width
|
|
1646
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)));
|
|
1647
2140
|
out.set(i, {
|
|
1648
2141
|
kind: "table",
|
|
1649
2142
|
table: {
|
|
@@ -1652,19 +2145,33 @@ function tabbedRows(paras, measure) {
|
|
|
1652
2145
|
left: pt(0),
|
|
1653
2146
|
right: pt(0)
|
|
1654
2147
|
},
|
|
1655
|
-
layout: "fixed"
|
|
2148
|
+
layout: "fixed",
|
|
2149
|
+
widthType: "dxa",
|
|
2150
|
+
widthPt: pt(grid.reduce((sum, w) => sum + w, 0))
|
|
1656
2151
|
},
|
|
1657
|
-
grid
|
|
2152
|
+
grid,
|
|
1658
2153
|
rows: rows.map((row, r) => {
|
|
1659
2154
|
const prev = rows[r - 1];
|
|
1660
|
-
const pitch = prev ? prev.top - row.top -
|
|
1661
|
-
const opening = r === 0 ? row.spacingBefore : pitch >
|
|
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;
|
|
1662
2157
|
return {
|
|
1663
2158
|
properties: {},
|
|
1664
|
-
cells: splitAtTabs(row.spans).map((cell) =>
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
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
|
+
})
|
|
1668
2175
|
};
|
|
1669
2176
|
})
|
|
1670
2177
|
}
|
|
@@ -1674,6 +2181,8 @@ function tabbedRows(paras, measure) {
|
|
|
1674
2181
|
}
|
|
1675
2182
|
return out;
|
|
1676
2183
|
}
|
|
2184
|
+
/** How far apart the ends of a column's cells may stand and still be flush. */
|
|
2185
|
+
var FLUSH_SLACK_PT = 2;
|
|
1677
2186
|
/** How many stops a line must stand on before a run of them is a table. */
|
|
1678
2187
|
var LEAST_TABLE_STOPS = 2;
|
|
1679
2188
|
/**
|
|
@@ -1709,8 +2218,9 @@ function ruleBorders(vectors, blocks, width) {
|
|
|
1709
2218
|
const given = /* @__PURE__ */ new Set();
|
|
1710
2219
|
const paragraphs = blocks.filter((b) => b.el.kind === "paragraph");
|
|
1711
2220
|
if (paragraphs.length === 0) return given;
|
|
1712
|
-
for (const
|
|
1713
|
-
|
|
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;
|
|
1714
2224
|
const y = (v.minY + v.maxY) / 2;
|
|
1715
2225
|
const below = paragraphs.filter((b) => b.top < y).sort((a, b) => b.top - a.top)[0];
|
|
1716
2226
|
const above = paragraphs.filter((b) => b.top >= y).sort((a, b) => a.top - b.top)[0];
|
|
@@ -1728,20 +2238,24 @@ function ruleBorders(vectors, blocks, width) {
|
|
|
1728
2238
|
colorHex: v.strokeHex ?? v.fillHex ?? "000000"
|
|
1729
2239
|
};
|
|
1730
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)) } : {};
|
|
1731
2243
|
side.block.el = {
|
|
1732
2244
|
kind: "paragraph",
|
|
1733
2245
|
paragraph: {
|
|
1734
2246
|
...paragraph,
|
|
1735
2247
|
properties: {
|
|
1736
2248
|
...paragraph.properties,
|
|
2249
|
+
...room,
|
|
1737
2250
|
borders: {
|
|
1738
2251
|
...paragraph.properties.borders,
|
|
1739
|
-
[side.edge]: border
|
|
2252
|
+
[side.edge]: border,
|
|
2253
|
+
...side.edge === "top" ? { insideH: border } : {}
|
|
1740
2254
|
}
|
|
1741
2255
|
}
|
|
1742
2256
|
}
|
|
1743
2257
|
};
|
|
1744
|
-
given.add(
|
|
2258
|
+
for (const piece of band.pieces) given.add(piece);
|
|
1745
2259
|
}
|
|
1746
2260
|
return given;
|
|
1747
2261
|
}
|
|
@@ -1750,22 +2264,54 @@ var RULE_REACH_PT = 14;
|
|
|
1750
2264
|
/** The thinnest a border may be drawn and still be seen. */
|
|
1751
2265
|
var RULE_MIN_PT = .5;
|
|
1752
2266
|
/**
|
|
1753
|
-
* Whether a painted path is
|
|
1754
|
-
* the next, rather than a piece of the
|
|
1755
|
-
*
|
|
1756
|
-
*
|
|
1757
|
-
* the line a total is written over. Everything else — boxes,
|
|
1758
|
-
* keeps the place on the page it was drawn at.
|
|
1759
|
-
*
|
|
1760
|
-
* @param v
|
|
1761
|
-
* @
|
|
1762
|
-
* @returns Whether it separates blocks rather than drawing something.
|
|
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.
|
|
1763
2276
|
*/
|
|
1764
|
-
function
|
|
2277
|
+
function flatBar(v) {
|
|
1765
2278
|
const w = v.maxX - v.minX;
|
|
1766
2279
|
const h = v.maxY - v.minY;
|
|
1767
|
-
return h <= RULE_THICK_PT && w
|
|
2280
|
+
return h <= RULE_THICK_PT && w > h * RULE_RATIO;
|
|
1768
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;
|
|
1769
2315
|
/** How thick a mark may be and still be a rule. */
|
|
1770
2316
|
var RULE_THICK_PT = 2;
|
|
1771
2317
|
/** How much of the sheet it must run across. */
|