reamkit 1.29.0 → 1.31.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/README.md +60 -24
- package/dist/esm/core/converter/project.js +3 -1
- package/dist/esm/core/crypto/offcrypto.js +1 -1
- package/dist/esm/core/document-model/index.d.ts +1 -1
- package/dist/esm/core/document-model/types.d.ts +65 -0
- package/dist/esm/core/drawingml/shape-render.js +13 -1
- package/dist/esm/core/font/index.d.ts +2 -0
- package/dist/esm/core/font/ttf-build.d.ts +113 -0
- package/dist/esm/core/font/ttf-build.js +1224 -0
- package/dist/esm/core/font/ttf-subset.d.ts +19 -0
- package/dist/esm/core/font/ttf-subset.js +14 -1
- package/dist/esm/core/fonts/index.d.ts +1 -1
- package/dist/esm/core/fonts/provider.d.ts +17 -0
- package/dist/esm/core/fonts/provider.js +27 -2
- package/dist/esm/core/fonts/remote-fonts.d.ts +8 -0
- package/dist/esm/core/fonts/remote-fonts.js +108 -18
- package/dist/esm/core/ir/flow.d.ts +88 -1
- package/dist/esm/core/numbering/index.d.ts +1 -1
- package/dist/esm/core/numbering/state.d.ts +11 -1
- package/dist/esm/core/numbering/state.js +10 -1
- package/dist/esm/core/style-cascade/resolver.js +35 -4
- package/dist/esm/core/style-cascade/types.d.ts +19 -1
- package/dist/esm/core/style-cascade/types.js +3 -0
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/layout/page-doc.d.ts +10 -3
- package/dist/esm/layout/styled-layout.d.ts +48 -7
- package/dist/esm/layout/styled-layout.js +881 -119
- package/dist/esm/layout/turned-section.d.ts +38 -0
- package/dist/esm/layout/turned-section.js +193 -0
- package/dist/esm/pdf/styled-page-emitter.js +78 -2
- 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/cff-outline.d.ts +27 -0
- package/dist/esm/pdf-reader/cff-outline.js +169 -21
- package/dist/esm/pdf-reader/cmap.js +5 -2
- package/dist/esm/pdf-reader/content.d.ts +70 -4
- package/dist/esm/pdf-reader/content.js +172 -12
- package/dist/esm/pdf-reader/display.d.ts +36 -0
- package/dist/esm/pdf-reader/display.js +82 -1
- package/dist/esm/pdf-reader/document.js +5 -1
- package/dist/esm/pdf-reader/embedded-fonts.d.ts +25 -0
- package/dist/esm/pdf-reader/embedded-fonts.js +78 -9
- package/dist/esm/pdf-reader/encodings.d.ts +8 -0
- package/dist/esm/pdf-reader/encodings.js +25 -3
- package/dist/esm/pdf-reader/face-outlines.d.ts +78 -0
- package/dist/esm/pdf-reader/face-outlines.js +362 -0
- package/dist/esm/pdf-reader/figures.d.ts +52 -0
- package/dist/esm/pdf-reader/figures.js +433 -0
- package/dist/esm/pdf-reader/flow-build.d.ts +224 -7
- package/dist/esm/pdf-reader/flow-build.js +545 -39
- package/dist/esm/pdf-reader/font.d.ts +27 -1
- package/dist/esm/pdf-reader/font.js +632 -63
- package/dist/esm/pdf-reader/glyf-outline.d.ts +33 -0
- package/dist/esm/pdf-reader/glyf-outline.js +148 -3
- package/dist/esm/pdf-reader/glyph-names.js +154 -1
- 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 +119 -2
- package/dist/esm/pdf-reader/layout.js +2219 -184
- package/dist/esm/pdf-reader/lexer.d.ts +10 -0
- package/dist/esm/pdf-reader/lexer.js +17 -0
- package/dist/esm/pdf-reader/page-numbers.d.ts +53 -0
- package/dist/esm/pdf-reader/page-numbers.js +167 -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 +207 -28
- package/dist/esm/pdf-reader/text-rules.js +1 -1
- package/dist/esm/pdf-reader/text.d.ts +6 -3
- package/dist/esm/pdf-reader/text.js +106 -22
- package/dist/esm/pdf-reader/type1-outline.d.ts +11 -0
- package/dist/esm/pdf-reader/type1-outline.js +63 -8
- package/dist/esm/pdf-reader/vector.d.ts +8 -2
- package/dist/esm/pdf-reader/vector.js +105 -6
- package/dist/esm/word/doc/doc-reader.js +6 -2
- package/dist/esm/word/doc/doc-text.d.ts +6 -0
- package/dist/esm/word/doc/doc-text.js +19 -1
- package/dist/esm/word/document-parser.d.ts +2 -2
- package/dist/esm/word/document-parser.js +11 -1
- package/dist/esm/word/docx-reader.js +5 -3
- package/dist/esm/word/docx-writer.js +341 -54
- package/dist/esm/word/drawing-parser.d.ts +5 -3
- package/dist/esm/word/drawing-parser.js +56 -10
- package/dist/esm/word/font-embed.d.ts +30 -0
- package/dist/esm/word/font-embed.js +173 -0
- package/dist/esm/word/font-table.d.ts +10 -0
- package/dist/esm/word/font-table.js +13 -1
- package/dist/esm/word/index.js +1 -1
- package/dist/esm/word/numbering-parser.d.ts +3 -1
- package/dist/esm/word/numbering-parser.js +2 -1
- package/dist/esm/word/paragraph-properties.d.ts +7 -6
- package/dist/esm/word/paragraph-properties.js +16 -2
- package/dist/esm/word/run-properties.js +26 -0
- package/package.json +12 -5
|
@@ -3,16 +3,24 @@ 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";
|
|
7
|
+
import { displayOf, placeImages, placeRuns, placeVectors, textFrameOf, wordsTurnOf } from "./display.js";
|
|
8
|
+
import { BASELINE_AT, CARRIER_LINE_PT, GUESSED_MARGIN, NATURAL_LINE_EM, buildFlowDoc, dedupeLosses, figureBlock, floatOntoSheet, imageBlock, imageMember, labelMember, membersBox, paragraphFromRuns, positionedText, sectionFromPdfPages, sectionOnSheet, shapeBlock, spaceAfter, textSizeOf, tooSmallToRead, vectorMember, withMeasuredMargins } from "./flow-build.js";
|
|
9
|
+
import { faceOutlinesOf, kernedFaces, pageSpacing } from "./face-outlines.js";
|
|
6
10
|
import { extractPageText } from "./text.js";
|
|
7
11
|
import { collectPageVectors } from "./vector.js";
|
|
8
|
-
import { displayOf, placeImages, placeRuns, placeVectors } from "./display.js";
|
|
9
|
-
import { buildFlowDoc, dedupeLosses, imageBlock, paragraphFromRuns, positionedText, sectionFromPdfPages, shapeBlock, withMeasuredMargins } from "./flow-build.js";
|
|
10
12
|
import { collectPageImages } from "./images.js";
|
|
11
13
|
import { markDrawnRules } from "./text-rules.js";
|
|
14
|
+
import { joinLetters, pageFigures, tracedRun } from "./figures.js";
|
|
15
|
+
import { regionsOf } from "./regions.js";
|
|
16
|
+
import { punctuationOf } from "./glyph-shapes.js";
|
|
12
17
|
import { matrixBlocks } from "./math-rows.js";
|
|
18
|
+
import { pageNumberingOf, runOf } from "./page-numbers.js";
|
|
13
19
|
//#region src/pdf-reader/layout.ts
|
|
14
20
|
/** The relationships the reconstruction files its running head and foot under. */
|
|
15
21
|
var FOOTER_PART = "pdf-running-foot";
|
|
22
|
+
/** The part the running foot of a book's even pages is written to (see `sidesOf`). */
|
|
23
|
+
var EVEN_FOOTER_PART = "pdf-running-foot-even";
|
|
16
24
|
var HEADER_PART = "pdf-running-head";
|
|
17
25
|
/**
|
|
18
26
|
* Heuristically reconstruct an untagged PDF into a {@link Reconstruction}
|
|
@@ -31,16 +39,60 @@ var HEADER_PART = "pdf-running-head";
|
|
|
31
39
|
*/
|
|
32
40
|
function reconstructByLayout(file, mode = "flow") {
|
|
33
41
|
const pages = file.pages();
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
42
|
+
const sheets = pages.map((page) => displayOf(page));
|
|
43
|
+
const painted = /* @__PURE__ */ new Map();
|
|
44
|
+
const extracted = typewritten(pages.map((page) => extractPageText(file, page, painted)));
|
|
45
|
+
const spacing = pageSpacing(extracted);
|
|
46
|
+
const outlines = faceOutlinesOf(painted, spacing);
|
|
47
|
+
const faceSpaces = new Map([...outlines].flatMap(([name, face]) => {
|
|
48
|
+
const advance = face.glyphs.get(" ")?.advance;
|
|
49
|
+
return advance !== void 0 ? [[name, advance]] : [];
|
|
50
|
+
}));
|
|
51
|
+
const onSheets = extracted.map((runs, i) => placeRuns(runs, sheets[i]));
|
|
52
|
+
const turns = onSheets.map((runs) => wordsTurnOf(runs));
|
|
53
|
+
const shown = sheets.map((sheet, i) => mode !== "positional" && turns[i] === 270 ? textFrameOf(sheet) : sheet);
|
|
54
|
+
const allRuns = shown.map((d, i) => d.sheet ? placeRuns(extracted[i], d) : onSheets[i]);
|
|
55
|
+
const offSheet = (r, i) => r.y < 0 || r.y > shown[i].height;
|
|
56
|
+
const bandRuns = allRuns.map((runs, i) => shown[i].sheet ? [] : runs.filter((r) => !offSheet(r, i)));
|
|
57
|
+
const foot = mode === "positional" ? void 0 : runningFoot(bandRuns, shown, "foot");
|
|
58
|
+
const head = mode === "positional" ? void 0 : runningFoot(bandRuns, shown, "head");
|
|
59
|
+
const numberedBand = foot?.numbered === true ? foot : head?.numbered === true ? head : void 0;
|
|
60
|
+
const numbering = numberingOf(numberedBand);
|
|
61
|
+
const textSize = textSizeOf(allRuns);
|
|
62
|
+
const stamp = (r, i) => mode !== "positional" && (r.annotation === true || tooSmallToRead(r, textSize) || offSheet(r, i));
|
|
63
|
+
const stamps = allRuns.map((runs, i) => runs.filter((r) => stamp(r, i)));
|
|
64
|
+
const readRuns = 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, i))) : allRuns;
|
|
65
|
+
const art = pages.map((page, i) => {
|
|
66
|
+
const raw = collectPageImages(file, page);
|
|
67
|
+
const images = placeImages(raw.images, shown[i]);
|
|
68
|
+
const lifted = collectPageVectors(file, page, images.map((img) => ({
|
|
69
|
+
minX: img.x,
|
|
70
|
+
minY: img.y,
|
|
71
|
+
maxX: img.x + img.widthPt,
|
|
72
|
+
maxY: img.y + img.heightPt
|
|
73
|
+
})));
|
|
74
|
+
return {
|
|
75
|
+
images,
|
|
76
|
+
imageLosses: raw.losses,
|
|
77
|
+
vectors: placeVectors(lifted.vectors, shown[i]),
|
|
78
|
+
vectorLosses: lifted.losses
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
const figures = art.map((a, i) => mode === "positional" || shown[i].sheet !== void 0 ? [] : pageFigures(a.vectors, a.images, readRuns[i], shown[i]));
|
|
82
|
+
const pageRuns = figures.some((f) => f.length > 0) ? readRuns.map((runs, i) => {
|
|
83
|
+
const labels = new Set(figures[i].flatMap((f) => f.labels));
|
|
84
|
+
return labels.size > 0 ? runs.filter((r) => !labels.has(r)) : runs;
|
|
85
|
+
}) : readRuns;
|
|
39
86
|
const pageMarks = pages.map(() => []);
|
|
40
87
|
const medianFont = median(pageRuns.flat().map((r) => r.fontSizePt).filter((s) => s > 0)) || 12;
|
|
41
88
|
const resources = new ResourceStore();
|
|
42
89
|
const losses = [];
|
|
43
|
-
if (
|
|
90
|
+
if (mode !== "positional" && turns.some((t) => t === 90 || t === 180)) losses.push({
|
|
91
|
+
severity: "degraded",
|
|
92
|
+
feature: FEATURES.text,
|
|
93
|
+
detail: "a page shows its words running up the sheet or upside down; a document sets a section’s text across the sheet or down it, so they are set across it"
|
|
94
|
+
});
|
|
95
|
+
if (readRuns.some((page) => page.some((r) => r.text.includes("�")))) losses.push({
|
|
44
96
|
severity: "dropped",
|
|
45
97
|
feature: FEATURES.text,
|
|
46
98
|
detail: "some glyphs map to no character — the font states no /ToUnicode and its program says nothing either, so that text is unrecoverable"
|
|
@@ -55,36 +107,50 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
55
107
|
feature: FEATURES.text,
|
|
56
108
|
detail: "text filled with a shading pattern is drawn in the middle colour of the gradient, not as the gradient"
|
|
57
109
|
});
|
|
58
|
-
const
|
|
110
|
+
const gutterRuns = pageRuns.map((runs, i) => {
|
|
111
|
+
const listed = withoutListings(runs);
|
|
112
|
+
const prose = withoutTables(listed);
|
|
113
|
+
return prose !== listed && detectGutters(prose, shown[i].width).length > 0 ? prose : listed;
|
|
114
|
+
});
|
|
115
|
+
const perPage = pages.map((_, i) => detectGutters(gutterRuns[i], shown[i].width));
|
|
59
116
|
const shared = commonGutters(perPage);
|
|
60
|
-
const pageGutters = perPage.map((own, i) => own.length > 0 ? own : shared && fitsGutters(
|
|
117
|
+
const pageGutters = perPage.map((own, i) => own.length > 0 ? own : shared && fitsGutters(gutterRuns[i], shared) ? shared : own);
|
|
61
118
|
const body = [];
|
|
62
119
|
const sectionEnds = [];
|
|
63
120
|
let sectionFrom = 0;
|
|
64
121
|
let lastSize = "";
|
|
122
|
+
const sheetSizes = [];
|
|
123
|
+
const leads = [];
|
|
65
124
|
let curColumns = 1;
|
|
66
125
|
let curSpace = 0;
|
|
67
126
|
let pendingContinuous = false;
|
|
127
|
+
const columnFeet = [];
|
|
128
|
+
const columnTurns = [];
|
|
68
129
|
pages.forEach((page, i) => {
|
|
69
130
|
const runs = pageRuns[i];
|
|
70
131
|
const display = shown[i];
|
|
132
|
+
const turned = display.sheet !== void 0;
|
|
71
133
|
const gutters = pageGutters[i];
|
|
72
134
|
const pageWidth = display.width;
|
|
73
135
|
const stepped = stepsBetweenWords(runs);
|
|
74
136
|
const blocks = [];
|
|
137
|
+
const shaded = /* @__PURE__ */ new Set();
|
|
75
138
|
const textEdges = pageTextEdges(runs);
|
|
76
139
|
const inColumns = proseColumns(runs, gutters, textEdges);
|
|
77
140
|
const split = gutters.length > 0 && inColumns ? assignColumns(runs, gutters) : void 0;
|
|
141
|
+
const figs = figures[i];
|
|
142
|
+
const spansGutter = (f) => gutters.some((g) => f.minX < g.mid && f.maxX > g.mid);
|
|
143
|
+
const breaks = split ? [...split.breaks, ...figs.filter(spansGutter).map((f) => f.maxY)].sort((a, b) => b - a) : [];
|
|
78
144
|
const bandEpsilon = (median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10) / 2;
|
|
79
|
-
const bandAt = (top) => bandOf(
|
|
80
|
-
const addColumn = (allRuns, col) => {
|
|
145
|
+
const bandAt = (top) => bandOf(breaks, top, bandEpsilon);
|
|
146
|
+
const addColumn = (allRuns, col, columnFigures = []) => {
|
|
81
147
|
const colRuns = allRuns.filter((r) => r.type3 !== true && (mode !== "positional" || r.invisible !== true));
|
|
82
148
|
if (mode === "positional") {
|
|
83
|
-
for (const [angle,
|
|
149
|
+
for (const [angle, turned] of byAngle(colRuns)) for (const runs of byPainter(turned)) for (const line of groupIntoLines(rotate(runs, -angle), true, stepped)) {
|
|
84
150
|
if (line.text.length === 0) continue;
|
|
85
151
|
const box = turnedBox(line, angle, pageWidth);
|
|
86
152
|
placed.push({
|
|
87
|
-
key:
|
|
153
|
+
key: placedKey(runs, placed.length),
|
|
88
154
|
col,
|
|
89
155
|
top: box.y + box.height,
|
|
90
156
|
make: (z) => positionedText(line.spans, box, frame, z, rotation60kOf(angle))
|
|
@@ -95,7 +161,8 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
95
161
|
const columnSize = median(colRuns.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
|
|
96
162
|
const maths = matrixBlocks(colRuns, columnSize);
|
|
97
163
|
const inMath = new Set(maths.flatMap((m) => [...m.used]));
|
|
98
|
-
const
|
|
164
|
+
const textRuns = inMath.size > 0 ? colRuns.filter((r) => !inMath.has(r)) : colRuns;
|
|
165
|
+
const lines = groupIntoLines(textRuns, false, stepped).filter((l) => l.text.length > 0);
|
|
99
166
|
const measure = measureOf(col, gutters, textEdges) ?? measureOfLines(lines);
|
|
100
167
|
for (const found of maths) {
|
|
101
168
|
const { alignment } = alignmentOf([{
|
|
@@ -123,38 +190,239 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
123
190
|
}
|
|
124
191
|
});
|
|
125
192
|
}
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
193
|
+
let above;
|
|
194
|
+
const slabs = [];
|
|
195
|
+
let rest = textRuns;
|
|
196
|
+
for (const figure of columnFigures) {
|
|
197
|
+
slabs.push({
|
|
198
|
+
runs: rest.filter((r) => r.y > figure.maxY),
|
|
199
|
+
figure
|
|
200
|
+
});
|
|
201
|
+
rest = rest.filter((r) => r.y <= figure.maxY);
|
|
202
|
+
}
|
|
203
|
+
slabs.push({ runs: rest });
|
|
204
|
+
for (const slab of slabs) {
|
|
205
|
+
const regions = turned ? [{
|
|
206
|
+
kind: "flow",
|
|
207
|
+
runs: slab.runs
|
|
208
|
+
}] : regionsOf(slab.runs);
|
|
209
|
+
for (const region of regions) {
|
|
210
|
+
if (region.kind === "side") {
|
|
211
|
+
const made = sideBySide(region, measure, above);
|
|
212
|
+
if (made === void 0) continue;
|
|
213
|
+
blocks.push({
|
|
214
|
+
band: bandAt(made.top),
|
|
215
|
+
col,
|
|
216
|
+
top: made.top,
|
|
217
|
+
el: made.el
|
|
218
|
+
});
|
|
219
|
+
above = made.below;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const regionLines = groupIntoLines(region.runs, false, stepped, faceSpaces, columnRules(vectors)).filter((l) => l.text.length > 0);
|
|
223
|
+
const reach = lines.length < 3 && gutters.length === 0 ? pageWidth * (1 - GUESSED_MARGIN) : void 0;
|
|
224
|
+
const { paras, under } = onBoxes(groupIntoParagraphs(regionLines, measure, display.height, above, false, reach), vectors.filter(solidBox), measure);
|
|
225
|
+
for (const set of setParagraphs(paras, measure && {
|
|
226
|
+
left: measure.left,
|
|
227
|
+
right: Math.max(measure.right, textEdges?.right ?? measure.right)
|
|
228
|
+
}, pageWidth)) blocks.push({
|
|
229
|
+
band: bandAt(set.top),
|
|
230
|
+
col,
|
|
231
|
+
top: set.top,
|
|
232
|
+
el: set.el,
|
|
233
|
+
...set.foot !== void 0 ? { foot: set.foot } : {}
|
|
234
|
+
});
|
|
235
|
+
const last = paras[paras.length - 1];
|
|
236
|
+
if (last !== void 0) above = {
|
|
237
|
+
y: last.bottom - under,
|
|
238
|
+
lineHeight: last.lineHeight
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (slab.figure !== void 0) {
|
|
242
|
+
const made = figureIn(slab.figure, measure, above, col === SPANNING_COLUMN);
|
|
243
|
+
blocks.push({
|
|
244
|
+
band: bandAt(made.top),
|
|
245
|
+
col,
|
|
246
|
+
top: made.top,
|
|
247
|
+
el: made.el
|
|
248
|
+
});
|
|
249
|
+
above = made.below;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
/** A figure's parts in the order the page painted them, its words over its drawing. */
|
|
254
|
+
const figureMembers = (figure) => {
|
|
255
|
+
const drawn = [...joinLetters([...figure.vectors].sort((a, b) => compareOrder(a.orderKey, b.orderKey))).map((v) => ({
|
|
256
|
+
key: v.orderKey,
|
|
257
|
+
member: vectorMember(v)
|
|
258
|
+
})), ...figure.images.map((img) => ({
|
|
259
|
+
key: img.orderKey,
|
|
260
|
+
member: imageMember(img, resources)
|
|
261
|
+
}))].sort((a, b) => compareOrder(a.key, b.key));
|
|
262
|
+
const labels = [];
|
|
263
|
+
const written = figure.labels.filter((r) => !tracedRun(r));
|
|
264
|
+
for (const [angle, turnedRuns] of byAngle(written)) for (const layer of byPainter(turnedRuns)) for (const line of groupIntoLines(rotate(layer, -angle), true, stepped)) {
|
|
265
|
+
if (line.text.length === 0) continue;
|
|
266
|
+
const box = angle === 0 ? {
|
|
267
|
+
x: line.x,
|
|
268
|
+
y: line.y - line.fontSize * .25,
|
|
269
|
+
width: line.width + line.fontSize,
|
|
270
|
+
height: line.fontSize * 1.25
|
|
271
|
+
} : turnedBox(line, angle, pageWidth);
|
|
272
|
+
labels.push(labelMember(line.spans, box, rotation60kOf(angle)));
|
|
273
|
+
}
|
|
274
|
+
return [...drawn.map((d) => d.member), ...labels];
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* §20.5.2.17 — a figure as the paragraph it stands in (see `figureBlock`):
|
|
278
|
+
* spaced from the text over it as a paragraph is, and set in from its
|
|
279
|
+
* column's edge as far as the page set it.
|
|
280
|
+
*
|
|
281
|
+
* A figure across the columns is spaced from the text nearest over it in
|
|
282
|
+
* any column: what its own column holds over it is the line before the
|
|
283
|
+
* columns began.
|
|
284
|
+
*/
|
|
285
|
+
const figureIn = (figure, measure, over, across = false) => {
|
|
286
|
+
const members = figureMembers(figure);
|
|
287
|
+
const box = membersBox(members);
|
|
288
|
+
const nearest = across ? runs.map((r) => r.y - r.fontSizePt * .25).filter((edge) => edge > box.top).reduce((low, edge) => Math.min(low ?? edge, edge), void 0) : void 0;
|
|
289
|
+
const above = nearest !== void 0 ? {
|
|
290
|
+
y: nearest,
|
|
291
|
+
lineHeight: 0
|
|
292
|
+
} : over;
|
|
293
|
+
const opened = above !== void 0 ? above.y - (1 - BASELINE_AT) * above.lineHeight - box.top : 0;
|
|
294
|
+
const indent = box.left - (measure?.left ?? box.left);
|
|
295
|
+
const properties = {
|
|
296
|
+
...opened > SPACING_NOISE_PT ? { spacingBefore: pt(Math.min(opened, display.height / 3)) } : {},
|
|
297
|
+
...Math.abs(indent) > SPACING_NOISE_PT ? { indentLeft: pt(indent) } : {}
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
top: box.top,
|
|
301
|
+
below: {
|
|
302
|
+
y: box.bottom,
|
|
303
|
+
lineHeight: 0
|
|
304
|
+
},
|
|
305
|
+
el: figureBlock(members, properties)
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* A region's paragraphs as the elements they are set in: the lines that
|
|
310
|
+
* stand on the same stops as a table (§17.4.38), the rest as paragraphs
|
|
311
|
+
* placed and spaced the way the page placed and spaced them.
|
|
312
|
+
*/
|
|
313
|
+
const far = /* @__PURE__ */ new WeakMap();
|
|
314
|
+
const setParagraphs = (paras, tableMeasure, sheetWidth = 0) => {
|
|
315
|
+
const asRows = turned ? /* @__PURE__ */ new Map() : tabbedRows(paras, tableMeasure);
|
|
316
|
+
const out = [];
|
|
131
317
|
for (const [at, para] of paras.entries()) {
|
|
132
318
|
const table = asRows.get(at);
|
|
133
319
|
if (table !== void 0) {
|
|
134
|
-
if (table !== null)
|
|
135
|
-
band: bandAt(para.top),
|
|
136
|
-
col,
|
|
320
|
+
if (table !== null) out.push({
|
|
137
321
|
top: para.top,
|
|
138
322
|
el: table
|
|
139
323
|
});
|
|
140
324
|
continue;
|
|
141
325
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
326
|
+
const overflow = sheetWidth > 0 ? runOn(para, tableMeasure, sheetWidth) : 0;
|
|
327
|
+
const { shade } = para;
|
|
328
|
+
if (shade) shaded.add(shade.box);
|
|
329
|
+
const el = paragraphFromRuns(para.spans, headingLevel(para.fontSize, medianFont), {
|
|
330
|
+
...para.stops !== void 0 && para.stops.length > 0 ? { tabs: para.stops.filter((x) => x > 0).map((x) => ({
|
|
331
|
+
positionPt: pt(x),
|
|
332
|
+
alignment: "left"
|
|
333
|
+
})) } : {},
|
|
334
|
+
...para.alignment !== void 0 ? { alignment: para.alignment } : {},
|
|
335
|
+
...para.spacingBefore !== void 0 ? { spacingBefore: pt(para.spacingBefore) } : {},
|
|
336
|
+
spacingLine: pt(para.lineHeight),
|
|
337
|
+
spacingLineRule: "exact",
|
|
338
|
+
...para.indentLeft !== void 0 ? { indentLeft: pt(para.indentLeft) } : {},
|
|
339
|
+
...para.indentFirstLine !== void 0 ? { indentFirstLine: pt(para.indentFirstLine) } : {},
|
|
340
|
+
...overflow > 0 ? { indentRight: pt(-overflow) } : {},
|
|
341
|
+
...shade ? shadeProperties(shade) : {}
|
|
342
|
+
});
|
|
343
|
+
if (sheetWidth > 0 && para.far !== void 0) far.set(el, para.far);
|
|
344
|
+
out.push({
|
|
145
345
|
top: para.top,
|
|
146
|
-
el
|
|
147
|
-
|
|
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
|
-
})
|
|
346
|
+
el,
|
|
347
|
+
foot: para.bottom - (1 - BASELINE_AT) * para.lineHeight - (shade?.under ?? 0)
|
|
156
348
|
});
|
|
157
349
|
}
|
|
350
|
+
return out;
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* §17.4.38 — a band of blocks side by side, as the borderless table a
|
|
354
|
+
* writer sets such a band in: one row, a cell a block, each cell's lines
|
|
355
|
+
* read down on their own and spaced from what stands above the band.
|
|
356
|
+
*/
|
|
357
|
+
const sideBySide = (region, measure, above) => {
|
|
358
|
+
const left = measure?.left ?? Math.min(...region.cells.map((c) => c.from));
|
|
359
|
+
const right = Math.max(measure?.right ?? 0, textEdges?.right ?? 0, ...region.cells.map((c) => c.to));
|
|
360
|
+
const cells = region.cells.map((cell) => {
|
|
361
|
+
return {
|
|
362
|
+
cell,
|
|
363
|
+
lines: groupIntoLines(cell.runs, false, stepped, faceSpaces).filter((l) => l.text.length > 0)
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
if (cells.some((c) => c.lines.length === 0)) return void 0;
|
|
367
|
+
const top = Math.max(...cells.map((c) => c.lines[0].y));
|
|
368
|
+
const edge = above ?? {
|
|
369
|
+
y: Math.max(...cells.map((c) => c.lines[0].y + .8 * 1.2 * c.lines[0].fontSize)),
|
|
370
|
+
lineHeight: 0
|
|
371
|
+
};
|
|
372
|
+
const starts = cells.map((c, k) => k === 0 ? left : c.cell.from);
|
|
373
|
+
let foot = Infinity;
|
|
374
|
+
const row = cells.map(({ cell, lines }, k) => {
|
|
375
|
+
const own = {
|
|
376
|
+
left: cell.from,
|
|
377
|
+
right: cell.to
|
|
378
|
+
};
|
|
379
|
+
const paras = groupIntoParagraphs(lines, own, display.height, edge, !isProse(lines, own));
|
|
380
|
+
const last = paras[paras.length - 1];
|
|
381
|
+
foot = Math.min(foot, last.bottom - (1 - BASELINE_AT) * last.lineHeight);
|
|
382
|
+
const inset = cell.from - starts[k];
|
|
383
|
+
const content = setParagraphs(paras, own).map(({ el }) => inset > 0 && el.kind === "paragraph" ? {
|
|
384
|
+
...el,
|
|
385
|
+
paragraph: {
|
|
386
|
+
...el.paragraph,
|
|
387
|
+
properties: {
|
|
388
|
+
...el.paragraph.properties,
|
|
389
|
+
indentLeft: pt((el.paragraph.properties.indentLeft ?? 0) + inset)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} : el);
|
|
393
|
+
const width = (starts[k + 1] ?? right) - starts[k];
|
|
394
|
+
return {
|
|
395
|
+
properties: { width: pt(Math.max(width, 1)) },
|
|
396
|
+
content
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
const grid = row.map((c) => c.properties.width);
|
|
400
|
+
return {
|
|
401
|
+
top,
|
|
402
|
+
below: {
|
|
403
|
+
y: foot,
|
|
404
|
+
lineHeight: 0
|
|
405
|
+
},
|
|
406
|
+
el: {
|
|
407
|
+
kind: "table",
|
|
408
|
+
table: {
|
|
409
|
+
properties: {
|
|
410
|
+
defaultCellMargins: {
|
|
411
|
+
left: pt(0),
|
|
412
|
+
right: pt(0)
|
|
413
|
+
},
|
|
414
|
+
layout: "fixed",
|
|
415
|
+
widthType: "dxa",
|
|
416
|
+
widthPt: pt(grid.reduce((sum, w) => sum + w, 0))
|
|
417
|
+
},
|
|
418
|
+
grid,
|
|
419
|
+
rows: [{
|
|
420
|
+
properties: {},
|
|
421
|
+
cells: row
|
|
422
|
+
}]
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
158
426
|
};
|
|
159
427
|
const placed = [];
|
|
160
428
|
const colOf = (centerX) => gutters.filter((g) => centerX >= g.mid).length;
|
|
@@ -162,39 +430,59 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
162
430
|
left: 0,
|
|
163
431
|
top: display.height
|
|
164
432
|
};
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
losses.push(...lifted.
|
|
179
|
-
const
|
|
180
|
-
const
|
|
433
|
+
for (const runs of byPainter(stamps[i] ?? [])) for (const line of groupIntoLines(runs, true, stepped)) {
|
|
434
|
+
if (line.text.length === 0) continue;
|
|
435
|
+
const box = turnedBox(line, 0, pageWidth);
|
|
436
|
+
placed.push({
|
|
437
|
+
key: placedKey(runs, placed.length),
|
|
438
|
+
col: colOf(box.x + box.width / 2),
|
|
439
|
+
top: box.y + box.height,
|
|
440
|
+
make: (z) => positionedText(line.spans, box, frame, z)
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
const lifted = art[i];
|
|
444
|
+
losses.push(...lifted.imageLosses);
|
|
445
|
+
pageMarks[i] = lifted.images;
|
|
446
|
+
losses.push(...lifted.vectorLosses);
|
|
447
|
+
const figured = new Set(figures[i].flatMap((f) => [...f.vectors, ...f.images]));
|
|
448
|
+
const imgs = { images: figured.size > 0 ? lifted.images.filter((img) => !figured.has(img)) : lifted.images };
|
|
449
|
+
const placedVectors = figured.size > 0 ? lifted.vectors.filter((v) => !figured.has(v)) : lifted.vectors;
|
|
450
|
+
const drawnRules = markDrawnRules(runs, placedVectors);
|
|
181
451
|
const strayGlyphs = strayMarks(placedVectors, runs);
|
|
452
|
+
const ruled = mode !== "positional" ? {
|
|
453
|
+
...drawnRules,
|
|
454
|
+
runs: readDrawnMarks(drawnRules.runs, strayGlyphs)
|
|
455
|
+
} : drawnRules;
|
|
182
456
|
const vectors = placedVectors.filter((v) => !ruled.consumed.has(v)).filter((v) => !(mode !== "positional" && strayGlyphs.has(v)));
|
|
183
|
-
const
|
|
457
|
+
const drawn = drawnWords(vectors);
|
|
458
|
+
const ruledIntoColumns = mode !== "positional" && !turned && looksRuled(ruled.runs, gutters, textEdges);
|
|
184
459
|
const asTable = ruledIntoColumns && textEdges ? tableFrom(ruled.runs, gutters, textEdges, stepped) : void 0;
|
|
185
|
-
if (asTable)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
460
|
+
if (asTable) {
|
|
461
|
+
for (const block of asTable) blocks.push({
|
|
462
|
+
band: bandAt(block.top),
|
|
463
|
+
col: 0,
|
|
464
|
+
top: block.top,
|
|
465
|
+
el: block.el
|
|
466
|
+
});
|
|
467
|
+
for (const figure of figs) {
|
|
468
|
+
const made = figureIn(figure, void 0, void 0);
|
|
469
|
+
blocks.push({
|
|
470
|
+
band: bandAt(made.top),
|
|
471
|
+
col: 0,
|
|
472
|
+
top: made.top,
|
|
473
|
+
el: made.el
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
} else if (split && !ruledIntoColumns) {
|
|
192
477
|
const columnFor = (r) => split.columnOf.get(r) ?? colOf(r.x);
|
|
478
|
+
const figureColumn = (f) => spansGutter(f) ? SPANNING_COLUMN : colOf((f.minX + f.maxX) / 2);
|
|
193
479
|
const columns = Array.from({ length: gutters.length + 1 }, (_, n) => n);
|
|
194
|
-
for (const col of [SPANNING_COLUMN, ...columns]) addColumn(ruled.runs.filter((r) => columnFor(r) === col), col);
|
|
195
|
-
|
|
480
|
+
for (const col of [SPANNING_COLUMN, ...columns]) addColumn(ruled.runs.filter((r) => columnFor(r) === col), col, figs.filter((f) => figureColumn(f) === col));
|
|
481
|
+
spaceUnderSpans(blocks);
|
|
482
|
+
} else addColumn(ruled.runs, 0, figs);
|
|
196
483
|
const under = mode !== "positional";
|
|
197
|
-
|
|
484
|
+
setOff(blocks, far, (line, k) => positionedText(line.spans, turnedBox(line, 0, pageWidth), frame, FAR_Z + k));
|
|
485
|
+
const givenAway = mode !== "positional" ? ruleBorders(vectors, blocks, display.width, colOf) : void 0;
|
|
198
486
|
[
|
|
199
487
|
...imgs.images.map((img) => ({
|
|
200
488
|
key: img.orderKey,
|
|
@@ -202,7 +490,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
202
490
|
top: img.y + img.heightPt,
|
|
203
491
|
make: (z) => imageBlock(img, resources, void 0, frame, z, under)
|
|
204
492
|
})),
|
|
205
|
-
...
|
|
493
|
+
...drawn.filter((v) => givenAway?.has(v) !== true && !shaded.has(v)).map((v) => ({
|
|
206
494
|
key: v.orderKey,
|
|
207
495
|
col: colOf((v.minX + v.maxX) / 2),
|
|
208
496
|
top: v.maxY,
|
|
@@ -218,8 +506,8 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
218
506
|
});
|
|
219
507
|
});
|
|
220
508
|
blocks.sort((a, b) => a.band - b.band || columnOrder(a.col) - columnOrder(b.col) || b.top - a.top);
|
|
221
|
-
const size = `${shown[i].width.toFixed(2)}x${shown[i].height.toFixed(2)}`;
|
|
222
|
-
const opensSection = i > 0 && size !== lastSize;
|
|
509
|
+
const size = `${shown[i].width.toFixed(2)}x${shown[i].height.toFixed(2)}${turned ? " down" : ""}`;
|
|
510
|
+
const opensSection = i > 0 && (size !== lastSize || numbering?.runs.some((run) => run.from === i) === true);
|
|
223
511
|
if (opensSection) {
|
|
224
512
|
sectionEnds.push({
|
|
225
513
|
at: body.length,
|
|
@@ -233,12 +521,24 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
233
521
|
pendingContinuous = false;
|
|
234
522
|
}
|
|
235
523
|
lastSize = size;
|
|
236
|
-
|
|
524
|
+
sheetSizes[i] = size;
|
|
525
|
+
const breakAt = i > 0 && !opensSection ? body.length : void 0;
|
|
526
|
+
if (i > 0 && !opensSection) body.push({
|
|
237
527
|
kind: "paragraph",
|
|
238
528
|
paragraph: {
|
|
239
529
|
properties: {
|
|
240
530
|
pageBreakBefore: true,
|
|
241
|
-
spacingLine:
|
|
531
|
+
spacingLine: CARRIER_LINE_PT,
|
|
532
|
+
spacingLineRule: "exact"
|
|
533
|
+
},
|
|
534
|
+
runs: []
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
else if (i === 0 && blocks.length === 0) body.push({
|
|
538
|
+
kind: "paragraph",
|
|
539
|
+
paragraph: {
|
|
540
|
+
properties: {
|
|
541
|
+
spacingLine: CARRIER_LINE_PT,
|
|
242
542
|
spacingLineRule: "exact"
|
|
243
543
|
},
|
|
244
544
|
runs: []
|
|
@@ -246,29 +546,147 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
246
546
|
});
|
|
247
547
|
const columnsHere = ruledIntoColumns || !proseColumns(runs, gutters, textEdges) ? 1 : gutters.length + 1;
|
|
248
548
|
const spacePt = columnsHere > 1 ? median(gutters.map((g) => g.to - g.from)) : 0;
|
|
549
|
+
columnFeet[i] = columnsHere > 1 ? lastColumnFeet(blocks) : void 0;
|
|
550
|
+
let led = false;
|
|
551
|
+
let begun = false;
|
|
552
|
+
const footOf = (band, col) => {
|
|
553
|
+
const column = blocks.filter((b) => b.band === band && b.col === col);
|
|
554
|
+
if (column.length === 0) return void 0;
|
|
555
|
+
return column.reduce((low, b) => b.top < low.top ? b : low).foot;
|
|
556
|
+
};
|
|
557
|
+
let prev;
|
|
558
|
+
const placedAt = /* @__PURE__ */ new Map();
|
|
249
559
|
for (const block of blocks) {
|
|
560
|
+
if (columnsHere > 1 && prev !== void 0 && block.band === prev.band && prev.col !== SPANNING_COLUMN && block.col > prev.col) {
|
|
561
|
+
const ends = footOf(prev.band, prev.col);
|
|
562
|
+
const next = footOf(block.band, block.col);
|
|
563
|
+
const closed = blocks.some((b) => b.band === block.band && b.col === SPANNING_COLUMN);
|
|
564
|
+
if (ends !== void 0 && next !== void 0) {
|
|
565
|
+
const rest = blocks.slice(blocks.indexOf(block));
|
|
566
|
+
const skipped = rest.findIndex((b) => !floating(b.el));
|
|
567
|
+
const lead = skipped >= 0 ? leadingLine(rest[skipped].el) : void 0;
|
|
568
|
+
const opener = blocks.find((b) => b.band === block.band && b.col !== SPANNING_COLUMN && !floating(b.el));
|
|
569
|
+
const openerAt = opener !== void 0 ? placedAt.get(opener) : void 0;
|
|
570
|
+
const openerLine = opener !== void 0 ? leadingLine(opener.el) : void 0;
|
|
571
|
+
columnTurns.push({
|
|
572
|
+
at: body.length,
|
|
573
|
+
foot: ends,
|
|
574
|
+
next,
|
|
575
|
+
closed,
|
|
576
|
+
...lead !== void 0 ? { head: {
|
|
577
|
+
at: body.length + skipped,
|
|
578
|
+
baseline: rest[skipped].top,
|
|
579
|
+
lineHeight: lead
|
|
580
|
+
} } : {},
|
|
581
|
+
...opener !== void 0 && openerAt !== void 0 && openerLine !== void 0 ? { opens: {
|
|
582
|
+
at: openerAt,
|
|
583
|
+
baseline: opener.top,
|
|
584
|
+
lineHeight: openerLine
|
|
585
|
+
} } : {}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
prev = block;
|
|
250
590
|
const count = block.col === SPANNING_COLUMN ? 1 : columnsHere;
|
|
251
591
|
if (count !== curColumns) {
|
|
592
|
+
const opensPage = breakAt !== void 0 && !begun;
|
|
593
|
+
if (opensPage) {
|
|
594
|
+
const last = body[breakAt - 1];
|
|
595
|
+
const own = sectionEnds.every((e) => e.at < breakAt);
|
|
596
|
+
if (last?.kind === "paragraph" && own) body.pop();
|
|
597
|
+
else body[breakAt] = {
|
|
598
|
+
kind: "paragraph",
|
|
599
|
+
paragraph: {
|
|
600
|
+
properties: {
|
|
601
|
+
spacingLine: CARRIER_LINE_PT,
|
|
602
|
+
spacingLineRule: "exact"
|
|
603
|
+
},
|
|
604
|
+
runs: []
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
252
608
|
sectionEnds.push({
|
|
253
609
|
at: body.length,
|
|
254
610
|
from: sectionFrom,
|
|
255
|
-
to: i + 1,
|
|
611
|
+
to: opensPage ? i : i + 1,
|
|
256
612
|
columns: curColumns,
|
|
257
613
|
spacePt: curSpace,
|
|
258
614
|
continuous: pendingContinuous
|
|
259
615
|
});
|
|
260
616
|
sectionFrom = i;
|
|
261
|
-
pendingContinuous =
|
|
617
|
+
pendingContinuous = !opensPage;
|
|
262
618
|
curColumns = count;
|
|
263
619
|
curSpace = spacePt;
|
|
264
620
|
}
|
|
621
|
+
if (!led && mode !== "positional") {
|
|
622
|
+
const lead = leadingLine(block.el);
|
|
623
|
+
if (lead !== void 0) leads.push({
|
|
624
|
+
page: i,
|
|
625
|
+
at: body.length,
|
|
626
|
+
baseline: block.top,
|
|
627
|
+
lineHeight: lead
|
|
628
|
+
});
|
|
629
|
+
led = lead !== void 0 || block.el.kind === "table" || block.el.kind === "paragraph";
|
|
630
|
+
}
|
|
631
|
+
placedAt.set(block, body.length);
|
|
265
632
|
body.push(block.el);
|
|
633
|
+
begun = true;
|
|
266
634
|
}
|
|
267
635
|
});
|
|
636
|
+
const measured = (own, from, to) => withMeasuredMargins(own, shown.slice(from, to), pageRuns.slice(from, to), pageMarks.slice(from, to), shown[from]?.sheet ? void 0 : foot?.band);
|
|
637
|
+
const sheetRun = (from) => {
|
|
638
|
+
let start = from;
|
|
639
|
+
while (start > 0 && sheetSizes[start - 1] === sheetSizes[from]) start--;
|
|
640
|
+
let end = from + 1;
|
|
641
|
+
while (end < pages.length && sheetSizes[end] === sheetSizes[from]) end++;
|
|
642
|
+
return [start, end];
|
|
643
|
+
};
|
|
644
|
+
const setUps = /* @__PURE__ */ new Map();
|
|
268
645
|
const setUp = (from, to) => {
|
|
269
|
-
const
|
|
270
|
-
|
|
646
|
+
const key = `${String(from)}:${String(to)}`;
|
|
647
|
+
if (setUps.has(key)) return setUps.get(key);
|
|
648
|
+
const own = sectionFromPdfPages(pages.slice(from, to), shown[from]);
|
|
649
|
+
let section = mode === "positional" ? own : measured(own, from, to);
|
|
650
|
+
const [start, end] = sheetRun(from);
|
|
651
|
+
if (mode !== "positional" && section?.margins && (start < from || end > to)) {
|
|
652
|
+
const sheet = measured(own, start, end)?.margins;
|
|
653
|
+
if (sheet) section = {
|
|
654
|
+
...section,
|
|
655
|
+
margins: {
|
|
656
|
+
...section.margins,
|
|
657
|
+
top: sheet.top,
|
|
658
|
+
bottom: sheet.bottom
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
setUps.set(key, section);
|
|
663
|
+
return section;
|
|
271
664
|
};
|
|
665
|
+
const lastFeet = columnFeet[pages.length - 1];
|
|
666
|
+
const floor = setUp(sectionFrom, pages.length)?.margins?.bottom;
|
|
667
|
+
if (mode !== "positional" && curColumns > 1 && lastFeet !== void 0 && floor !== void 0 && balancedEnd(lastFeet, floor, medianFont)) {
|
|
668
|
+
sectionEnds.push({
|
|
669
|
+
at: body.length,
|
|
670
|
+
from: sectionFrom,
|
|
671
|
+
to: pages.length,
|
|
672
|
+
columns: curColumns,
|
|
673
|
+
spacePt: curSpace,
|
|
674
|
+
continuous: pendingContinuous
|
|
675
|
+
});
|
|
676
|
+
body.push({
|
|
677
|
+
kind: "paragraph",
|
|
678
|
+
paragraph: {
|
|
679
|
+
properties: {
|
|
680
|
+
spacingLine: CARRIER_LINE_PT,
|
|
681
|
+
spacingLineRule: "exact"
|
|
682
|
+
},
|
|
683
|
+
runs: []
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
curColumns = 1;
|
|
687
|
+
curSpace = 0;
|
|
688
|
+
pendingContinuous = true;
|
|
689
|
+
}
|
|
272
690
|
sectionEnds.push({
|
|
273
691
|
at: body.length,
|
|
274
692
|
from: sectionFrom,
|
|
@@ -277,12 +695,96 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
277
695
|
spacePt: curSpace,
|
|
278
696
|
continuous: pendingContinuous
|
|
279
697
|
});
|
|
698
|
+
for (const lead of leads) {
|
|
699
|
+
const end = sectionEnds.find((e) => e.at > lead.at);
|
|
700
|
+
const top = (end ? setUp(end.from, end.to) : setUp(0, pages.length))?.margins?.top;
|
|
701
|
+
const page = shown[lead.page];
|
|
702
|
+
const el = body[lead.at];
|
|
703
|
+
if (top === void 0 || page === void 0) continue;
|
|
704
|
+
const border = el?.kind === "paragraph" ? el.paragraph.properties.borders?.top?.width ?? 0 : 0;
|
|
705
|
+
const before = page.height - top - lead.baseline - BASELINE_AT * lead.lineHeight - border;
|
|
706
|
+
if (before <= SPACING_NOISE_PT) continue;
|
|
707
|
+
if (el?.kind === "paragraph") body[lead.at] = {
|
|
708
|
+
...el,
|
|
709
|
+
paragraph: {
|
|
710
|
+
...el.paragraph,
|
|
711
|
+
properties: {
|
|
712
|
+
...el.paragraph.properties,
|
|
713
|
+
spacingBefore: pt(before)
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
else if (el?.kind === "shape") body[lead.at] = {
|
|
718
|
+
...el,
|
|
719
|
+
shape: {
|
|
720
|
+
...el.shape,
|
|
721
|
+
paragraphProperties: {
|
|
722
|
+
...el.shape.paragraphProperties,
|
|
723
|
+
spacingBefore: pt(before)
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
const line = medianFont * NATURAL_LINE_EM;
|
|
729
|
+
const breaks = [];
|
|
730
|
+
for (const turn of columnTurns) {
|
|
731
|
+
const end = sectionEnds.find((e) => e.at > turn.at);
|
|
732
|
+
const margins = (end ? setUp(end.from, end.to) : void 0)?.margins;
|
|
733
|
+
if (turn.closed) {
|
|
734
|
+
if (Math.abs(turn.foot - turn.next) <= line * BALANCED_LINES) continue;
|
|
735
|
+
} else if (margins?.bottom === void 0 || turn.foot - margins.bottom < line * SHORT_LINES) continue;
|
|
736
|
+
const { head: start, opens } = turn;
|
|
737
|
+
const opener = opens !== void 0 ? body[opens.at] : void 0;
|
|
738
|
+
const openerProps = opener?.kind === "paragraph" ? opener.paragraph.properties : opener?.kind === "shape" ? opener.shape.paragraphProperties : void 0;
|
|
739
|
+
const before = start !== void 0 && opens !== void 0 && openerProps !== void 0 ? (openerProps.spacingBefore ?? 0) + (openerProps.borders?.top?.width ?? 0) + (opens.baseline + BASELINE_AT * opens.lineHeight) - (start.baseline + BASELINE_AT * start.lineHeight) : void 0;
|
|
740
|
+
breaks.push({
|
|
741
|
+
at: turn.at,
|
|
742
|
+
...start !== void 0 && before !== void 0 ? { head: {
|
|
743
|
+
at: start.at,
|
|
744
|
+
before
|
|
745
|
+
} } : {}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
for (const brk of [...breaks].sort((x, y) => y.at - x.at)) {
|
|
749
|
+
const el = brk.head !== void 0 ? body[brk.head.at] : void 0;
|
|
750
|
+
if (brk.head !== void 0 && el?.kind === "paragraph") {
|
|
751
|
+
const border = el.paragraph.properties.borders?.top?.width ?? 0;
|
|
752
|
+
const before = brk.head.before - border;
|
|
753
|
+
const { spacingBefore: _, ...rest } = el.paragraph.properties;
|
|
754
|
+
body[brk.head.at] = {
|
|
755
|
+
...el,
|
|
756
|
+
paragraph: {
|
|
757
|
+
...el.paragraph,
|
|
758
|
+
properties: before > SPACING_NOISE_PT ? {
|
|
759
|
+
...rest,
|
|
760
|
+
spacingBefore: pt(before)
|
|
761
|
+
} : rest
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
body.splice(brk.at, 0, {
|
|
766
|
+
kind: "paragraph",
|
|
767
|
+
paragraph: {
|
|
768
|
+
properties: {
|
|
769
|
+
spacingLine: CARRIER_LINE_PT,
|
|
770
|
+
spacingLineRule: "exact"
|
|
771
|
+
},
|
|
772
|
+
runs: [{
|
|
773
|
+
text: "\n",
|
|
774
|
+
properties: {},
|
|
775
|
+
columnBreak: true
|
|
776
|
+
}]
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
for (const e of sectionEnds) if (e.at > brk.at) e.at++;
|
|
780
|
+
}
|
|
280
781
|
const sections = sectionEnds.length > 1 ? sectionEnds.flatMap((end) => {
|
|
281
|
-
const base = setUp(end.from, end.to);
|
|
782
|
+
const base = sectionOnSheet(setUp(end.from, end.to), shown[end.from]);
|
|
282
783
|
if (!base) return [];
|
|
283
784
|
return [{
|
|
284
785
|
properties: {
|
|
285
786
|
...base,
|
|
787
|
+
...numberedFrom(numbering, end.from),
|
|
286
788
|
...end.columns > 1 && mode !== "positional" ? { columns: {
|
|
287
789
|
count: end.columns,
|
|
288
790
|
spacePt: end.spacePt
|
|
@@ -292,26 +794,47 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
292
794
|
endIndex: end.at
|
|
293
795
|
}];
|
|
294
796
|
}) : [];
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
797
|
+
let sectionStart = 0;
|
|
798
|
+
for (const end of sectionEnds) {
|
|
799
|
+
const sheet = shown[end.from]?.sheet;
|
|
800
|
+
if (sheet) for (let k = sectionStart; k < end.at; k++) body[k] = floatOntoSheet(body[k], sheet.width);
|
|
801
|
+
sectionStart = end.at;
|
|
802
|
+
}
|
|
803
|
+
const setOn = (runs, page, numbered, numeral) => footerBand(runs, stepsBetweenWords(allRuns[page] ?? []), pageTextEdges(allRuns[page] ?? []), numbered, numeral);
|
|
804
|
+
const firstOf = (of) => Math.max(0, of?.lift.findIndex((set) => set.size > 0) ?? 0);
|
|
805
|
+
const numeralOf = (of) => {
|
|
806
|
+
if (of === void 0 || of !== numberedBand || numbering === void 0) return void 0;
|
|
807
|
+
const first = of.lift.findIndex((set) => set.size > 0);
|
|
808
|
+
return first >= 0 ? numbering.numbers[first]?.text : void 0;
|
|
809
|
+
};
|
|
810
|
+
const sides = foot !== void 0 && foot === numberedBand && numbering !== void 0 ? sidesOf(foot, numbering, shown.map((s) => s.width / 2)) : void 0;
|
|
811
|
+
const band = foot ? sides ? setOn(sides.odd.runs, sides.odd.page, true, sides.odd.numeral) : setOn(foot.band, firstOf(foot), foot.numbered, numeralOf(foot)) : [];
|
|
812
|
+
const evenBand = sides ? setOn(sides.even.runs, sides.even.page, true, sides.even.numeral) : [];
|
|
813
|
+
const headBand = head ? setOn(head.band, firstOf(head), head.numbered, numeralOf(head)) : [];
|
|
299
814
|
const withFooter = (properties) => properties ? {
|
|
300
815
|
...properties,
|
|
301
816
|
...band.length > 0 ? { footers: [{
|
|
302
817
|
type: "default",
|
|
303
818
|
relationshipId: FOOTER_PART
|
|
304
|
-
}
|
|
819
|
+
}, ...evenBand.length > 0 ? [{
|
|
820
|
+
type: "even",
|
|
821
|
+
relationshipId: EVEN_FOOTER_PART
|
|
822
|
+
}] : []] } : {},
|
|
823
|
+
...evenBand.length > 0 ? { evenAndOddHeaders: true } : {},
|
|
305
824
|
...headBand.length > 0 ? { headers: [{
|
|
306
825
|
type: "default",
|
|
307
826
|
relationshipId: HEADER_PART
|
|
308
827
|
}] } : {}
|
|
309
828
|
} : properties;
|
|
310
829
|
return {
|
|
311
|
-
doc: buildFlowDoc(body, resources, withFooter(setUp(0, pages.length)), collectEmbeddedFonts(file, pages, losses), sections.map((s) => ({
|
|
830
|
+
doc: buildFlowDoc(body, resources, withFooter(numberedAs(sectionOnSheet(setUp(0, pages.length), shown[0]), numbering)), collectEmbeddedFonts(file, pages, losses), sections.map((s) => ({
|
|
312
831
|
...s,
|
|
313
832
|
properties: withFooter(s.properties) ?? s.properties
|
|
314
|
-
})), band.length > 0 || headBand.length > 0 ? new Map([
|
|
833
|
+
})), band.length > 0 || headBand.length > 0 ? new Map([
|
|
834
|
+
...band.length > 0 ? [[FOOTER_PART, band]] : [],
|
|
835
|
+
...evenBand.length > 0 ? [[EVEN_FOOTER_PART, evenBand]] : [],
|
|
836
|
+
...headBand.length > 0 ? [[HEADER_PART, headBand]] : []
|
|
837
|
+
]) : void 0, collectFaceFamilies(file, pages), outlines, kernedFaces(spacing)),
|
|
315
838
|
losses: dedupeLosses(losses)
|
|
316
839
|
};
|
|
317
840
|
}
|
|
@@ -324,7 +847,8 @@ function detectGutters(runs, pageWidth) {
|
|
|
324
847
|
});
|
|
325
848
|
if (spans.length === 0) return [];
|
|
326
849
|
const minX = Math.min(...spans.map((iv) => iv[0]));
|
|
327
|
-
const
|
|
850
|
+
const maxX = Math.max(...spans.map((iv) => iv[1]));
|
|
851
|
+
const span = maxX - minX;
|
|
328
852
|
if (span < pageWidth * .5) return [];
|
|
329
853
|
const sorted = [...spans].sort((a, b) => a[0] - b[0]);
|
|
330
854
|
let curEnd = sorted[0][1];
|
|
@@ -365,6 +889,7 @@ function detectGutters(runs, pageWidth) {
|
|
|
365
889
|
const after = row.filter(([l]) => l >= x);
|
|
366
890
|
if (before.length === 0 || after.length === 0) continue;
|
|
367
891
|
if (Math.min(...after.map(([l]) => l)) - Math.max(...before.map(([, r]) => r)) >= fontSize * MIN_GUTTER_EM) columned++;
|
|
892
|
+
else crossing++;
|
|
368
893
|
}
|
|
369
894
|
if (columned < MIN_COLUMNED_ROWS || crossing >= columned) continue;
|
|
370
895
|
const last = voted[voted.length - 1];
|
|
@@ -380,9 +905,124 @@ function detectGutters(runs, pageWidth) {
|
|
|
380
905
|
});
|
|
381
906
|
}
|
|
382
907
|
const near = (band) => empty.some((e) => band.mid > e.from - fontSize && band.mid < e.to + fontSize);
|
|
383
|
-
return separating([...empty, ...voted.filter((v) => !near(v))].sort((a, b) => a.mid - b.mid), spans);
|
|
908
|
+
return withoutNarrowColumns(separating([...empty, ...voted.filter((v) => !near(v))].sort((a, b) => a.mid - b.mid), spans), empty, rows, [minX, maxX], fontSize);
|
|
384
909
|
}
|
|
385
910
|
/**
|
|
911
|
+
* The gutters left when no column beside a gap the page's lines run across is
|
|
912
|
+
* narrower than a page's column can be.
|
|
913
|
+
*
|
|
914
|
+
* A table's columns stand apart as a page's do, line after line, and a page
|
|
915
|
+
* set in columns of tables has gaps between its tables' columns as well as
|
|
916
|
+
* its own: canvas.pdf sets two columns of Name, Type and Default, and read at
|
|
917
|
+
* every gap its sheets came back in three and five columns, a word wide each,
|
|
918
|
+
* on seven pages where it has two. What gives such a gap away is that the
|
|
919
|
+
* page's other lines — the headings over each table, the prose between them —
|
|
920
|
+
* run across it, and that a column it leaves is a few ems wide: such a gap
|
|
921
|
+
* goes, the one with the less white beside the narrowest column first, until
|
|
922
|
+
* no column is left so narrow. A gap NO line crosses stays however narrow the
|
|
923
|
+
* columns beside it — a page that is one table from edge to edge is read by
|
|
924
|
+
* its columns, as a table — and so does a page's one gap.
|
|
925
|
+
*
|
|
926
|
+
* The gaps that stay are then measured afresh, as the white every line split
|
|
927
|
+
* there leaves. The vote answers wherever more lines stand apart than run
|
|
928
|
+
* across, which is where MOST of a column's lines end and not where its
|
|
929
|
+
* longest does: a gutter voted across a column of short cells reaches back
|
|
930
|
+
* into that column, and a section set with it stood its columns sixty-five
|
|
931
|
+
* points further apart than the page does. freeculture.pdf's index sets its
|
|
932
|
+
* entries ragged, two columns six to twenty points apart, and set seventy
|
|
933
|
+
* apart its columns came back a third narrower than the page's: the thirteen
|
|
934
|
+
* pages of the index ran to twenty-seven, and its longest entries ran on past
|
|
935
|
+
* the middle of the white and were read as lines across the page.
|
|
936
|
+
*
|
|
937
|
+
* @param gutters The gutters found, left to right.
|
|
938
|
+
* @param firm The gutters no line crosses, which stay.
|
|
939
|
+
* @param rows Each line's ink across the page, left to right.
|
|
940
|
+
* @param extent Where the page's ink begins and ends.
|
|
941
|
+
* @param fontSize The page's body size.
|
|
942
|
+
*/
|
|
943
|
+
function withoutNarrowColumns(gutters, firm, rows, extent, fontSize) {
|
|
944
|
+
const kept = [...gutters];
|
|
945
|
+
const least = fontSize * NARROWEST_COLUMN_EM;
|
|
946
|
+
const white = (g) => g.to - g.from;
|
|
947
|
+
while (kept.length > 1) {
|
|
948
|
+
const edges = [
|
|
949
|
+
extent[0],
|
|
950
|
+
...kept.flatMap((g) => [g.from, g.to]),
|
|
951
|
+
extent[1]
|
|
952
|
+
];
|
|
953
|
+
let narrowest;
|
|
954
|
+
for (let k = 0; k <= kept.length; k++) {
|
|
955
|
+
const width = edges[2 * k + 1] - edges[2 * k];
|
|
956
|
+
if (width >= least || narrowest && narrowest.width <= width) continue;
|
|
957
|
+
const beside = [kept[k - 1], kept[k]].filter((g) => g !== void 0 && !firm.includes(g));
|
|
958
|
+
if (beside.length === 0) continue;
|
|
959
|
+
narrowest = {
|
|
960
|
+
width,
|
|
961
|
+
gap: beside.reduce((a, b) => white(b) < white(a) ? b : a)
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
if (!narrowest) break;
|
|
965
|
+
kept.splice(kept.indexOf(narrowest.gap), 1);
|
|
966
|
+
}
|
|
967
|
+
return kept.map((g) => {
|
|
968
|
+
if (firm.includes(g)) return g;
|
|
969
|
+
const starts = [];
|
|
970
|
+
const ends = [];
|
|
971
|
+
for (const row of rows) {
|
|
972
|
+
let widest;
|
|
973
|
+
let reach = row[0]?.[1] ?? -Infinity;
|
|
974
|
+
for (const [l, r] of row.slice(1)) {
|
|
975
|
+
const overlaps = l > g.from && reach < g.to;
|
|
976
|
+
if (l - reach >= fontSize * MIN_GUTTER_EM && overlaps) {
|
|
977
|
+
if (!widest || l - reach > widest[1] - widest[0]) widest = [reach, l];
|
|
978
|
+
}
|
|
979
|
+
reach = Math.max(reach, r);
|
|
980
|
+
}
|
|
981
|
+
if (!widest) continue;
|
|
982
|
+
starts.push(widest[0]);
|
|
983
|
+
ends.push(widest[1]);
|
|
984
|
+
}
|
|
985
|
+
if (starts.length === 0) return g;
|
|
986
|
+
const from = sharedEdge(starts) ?? Math.max(...starts);
|
|
987
|
+
const to = Math.min(...ends);
|
|
988
|
+
return to > from ? {
|
|
989
|
+
from,
|
|
990
|
+
to,
|
|
991
|
+
mid: (from + to) / 2
|
|
992
|
+
} : g;
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* The edge at least half of `ends` agree on, to {@link FLUSH_PT}: the furthest
|
|
997
|
+
* out of the agreeing ones, which every one of them clears — where no more
|
|
998
|
+
* than a line in twenty stops past it, set over the measure. Lines of one
|
|
999
|
+
* length agree on where they stop too, and the column's longer lines past
|
|
1000
|
+
* them are its measure.
|
|
1001
|
+
*
|
|
1002
|
+
* @param ends Where each line stops.
|
|
1003
|
+
* @returns The edge, or `undefined` where no half of them agree on one.
|
|
1004
|
+
*/
|
|
1005
|
+
function sharedEdge(ends) {
|
|
1006
|
+
const sorted = [...ends].sort((a, b) => a - b);
|
|
1007
|
+
let best;
|
|
1008
|
+
let low = 0;
|
|
1009
|
+
for (let high = 0; high < sorted.length; high++) {
|
|
1010
|
+
while (sorted[high] - sorted[low] > FLUSH_PT) low++;
|
|
1011
|
+
if (!best || high - low > best[1] - best[0]) best = [low, high];
|
|
1012
|
+
}
|
|
1013
|
+
if (!best || (best[1] - best[0] + 1) * 2 < sorted.length) return void 0;
|
|
1014
|
+
if (sorted.length - 1 - best[1] > Math.max(1, Math.floor(sorted.length * OVERSET_SHARE))) return void 0;
|
|
1015
|
+
return sorted[best[1]];
|
|
1016
|
+
}
|
|
1017
|
+
/** How many of a column's lines, as a share of them, may be set over its measure. */
|
|
1018
|
+
var OVERSET_SHARE = .05;
|
|
1019
|
+
/**
|
|
1020
|
+
* How narrow, in ems of the page's body, a column of the page may be: narrower
|
|
1021
|
+
* is a table's. The columns canvas.pdf's tables leave are two to six ems wide,
|
|
1022
|
+
* and eight is four or five words of prose.
|
|
1023
|
+
*/
|
|
1024
|
+
var NARROWEST_COLUMN_EM = 8;
|
|
1025
|
+
/**
|
|
386
1026
|
* The candidate gutters that actually separate something, left to right.
|
|
387
1027
|
*
|
|
388
1028
|
* A gutter with nothing on one side of it is a margin, and two of them with a
|
|
@@ -636,6 +1276,13 @@ function assignColumns(runs, gutters) {
|
|
|
636
1276
|
for (const run of row) columnOf.set(run, SPANNING_COLUMN);
|
|
637
1277
|
breaks.push(Math.max(...row.map((r) => r.y)));
|
|
638
1278
|
}
|
|
1279
|
+
const inRows = rows.filter((row) => row.some((r) => r.text.trim() !== ""));
|
|
1280
|
+
const across = (block) => block.some((row) => row.some((r) => columnOf.get(r) === SPANNING_COLUMN));
|
|
1281
|
+
const blocks = [...blocksOf(inRows, isCode, 1).filter(across), ...blocksOf(inRows, isTableRow, LEAST_TABLE_ROWS).filter((block) => across(block) || block.length >= SPANNING_TABLE_ROWS)];
|
|
1282
|
+
for (const block of blocks) for (const row of block) {
|
|
1283
|
+
if (row.some((r) => columnOf.get(r) !== SPANNING_COLUMN)) breaks.push(Math.max(...row.map((r) => r.y)));
|
|
1284
|
+
for (const run of row) columnOf.set(run, SPANNING_COLUMN);
|
|
1285
|
+
}
|
|
639
1286
|
return {
|
|
640
1287
|
columnOf,
|
|
641
1288
|
breaks: breaks.sort((a, b) => b - a)
|
|
@@ -691,6 +1338,32 @@ function bandOf(breaks, top, epsilon) {
|
|
|
691
1338
|
* does this — a flowing paragraph is meant to be read across.
|
|
692
1339
|
*/
|
|
693
1340
|
var SPACE_GAP_EM = .25;
|
|
1341
|
+
/**
|
|
1342
|
+
* §12.5.5 — the runs the page's own content paints, and then the ones an
|
|
1343
|
+
* annotation's appearance does: two layers, grouped into lines apart.
|
|
1344
|
+
*/
|
|
1345
|
+
function byPainter(runs) {
|
|
1346
|
+
return [runs.filter((r) => r.annotation !== true), runs.filter((r) => r.annotation === true)].filter((group) => group.length > 0);
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* §8.5.3 / §12.5.5 — where a placed line stands among the page's marks, which
|
|
1350
|
+
* are ordered as they were painted (see `compareOrder`): the page's own words
|
|
1351
|
+
* over everything its content drew, and under every annotation, whose
|
|
1352
|
+
* appearances paint after it (`[MAX_SAFE_INTEGER, index, …]`); an
|
|
1353
|
+
* annotation's words over all of it. Keyed as one layer, the placed lines
|
|
1354
|
+
* fell among the appearances by their count: evaljs.pdf's "Execute" went
|
|
1355
|
+
* under the grey of the button it names.
|
|
1356
|
+
*
|
|
1357
|
+
* @param runs The layer the line was grouped from (see `byPainter`).
|
|
1358
|
+
* @param count How many lines are placed already.
|
|
1359
|
+
*/
|
|
1360
|
+
function placedKey(runs, count) {
|
|
1361
|
+
return runs[0]?.annotation === true ? [
|
|
1362
|
+
Number.MAX_SAFE_INTEGER,
|
|
1363
|
+
Number.MAX_SAFE_INTEGER,
|
|
1364
|
+
count
|
|
1365
|
+
] : [Number.MAX_SAFE_INTEGER - 1, count];
|
|
1366
|
+
}
|
|
694
1367
|
/** Runs by the direction of their baseline, upright first, each angle rounded. */
|
|
695
1368
|
function byAngle(runs) {
|
|
696
1369
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -762,7 +1435,7 @@ function rotation60kOf(angleDeg) {
|
|
|
762
1435
|
* 0.05, and thirty at 0.05 em or more. A twentieth of an em sits in that gap.
|
|
763
1436
|
*/
|
|
764
1437
|
var BASELINE_STEP_EM = .05;
|
|
765
|
-
function groupIntoLines(runs, split = false, stepped = false) {
|
|
1438
|
+
function groupIntoLines(runs, split = false, stepped = false, spaces, rules = []) {
|
|
766
1439
|
const sorted = [...runs].sort((a, b) => b.y - a.y || a.x - b.x);
|
|
767
1440
|
const clusters = [];
|
|
768
1441
|
for (const run of sorted) {
|
|
@@ -770,6 +1443,7 @@ function groupIntoLines(runs, split = false, stepped = false) {
|
|
|
770
1443
|
const tol = Math.max(1, (run.fontSizePt || 10) * .5);
|
|
771
1444
|
if (last && Math.abs(last.y - run.y) <= tol) {
|
|
772
1445
|
last.runs.push(run);
|
|
1446
|
+
if ((run.fontSizePt || 0) > last.fontSize) last.y = run.y;
|
|
773
1447
|
last.fontSize = Math.max(last.fontSize, run.fontSizePt || 0);
|
|
774
1448
|
} else clusters.push({
|
|
775
1449
|
y: run.y,
|
|
@@ -777,10 +1451,12 @@ function groupIntoLines(runs, split = false, stepped = false) {
|
|
|
777
1451
|
runs: [run]
|
|
778
1452
|
});
|
|
779
1453
|
}
|
|
1454
|
+
for (const c of clusters) c.runs.sort((a, b) => a.x - b.x);
|
|
1455
|
+
const stops = sharedStops(clusters.map((c) => c.runs), rules);
|
|
780
1456
|
return clusters.flatMap((c) => {
|
|
781
|
-
const ordered = c.runs
|
|
1457
|
+
const ordered = c.runs;
|
|
782
1458
|
const fontSize = c.fontSize || 10;
|
|
783
|
-
if (!split) return [lineOf(ordered, c.y, fontSize, stepped)];
|
|
1459
|
+
if (!split) return [lineOf(ordered, c.y, fontSize, stepped, stops, spaces)];
|
|
784
1460
|
const pieces = [[]];
|
|
785
1461
|
for (const run of ordered) {
|
|
786
1462
|
const prev = pieces[pieces.length - 1];
|
|
@@ -789,10 +1465,103 @@ function groupIntoLines(runs, split = false, stepped = false) {
|
|
|
789
1465
|
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
1466
|
pieces[pieces.length - 1].push(run);
|
|
791
1467
|
}
|
|
792
|
-
return pieces.map((piece) => lineOf(piece, piece[0].y, Math.max(...piece.map((r) => r.fontSizePt || 0)) || fontSize, stepped));
|
|
1468
|
+
return pieces.map((piece) => lineOf(piece, piece[0].y, Math.max(...piece.map((r) => r.fontSizePt || 0)) || fontSize, stepped, stops, spaces));
|
|
793
1469
|
});
|
|
794
1470
|
}
|
|
795
1471
|
/**
|
|
1472
|
+
* Whether a block's lines are PROSE: many of them, across a measure a sentence
|
|
1473
|
+
* is set to, and most running out to the edge that measure breaks them at, as
|
|
1474
|
+
* a column of text does — where a stack of labels and values, or an address,
|
|
1475
|
+
* is short lines of their own lengths.
|
|
1476
|
+
*
|
|
1477
|
+
* @param lines The block's lines, top first.
|
|
1478
|
+
* @param own The block's own left and right edges.
|
|
1479
|
+
* @returns True for a column of running text.
|
|
1480
|
+
*/
|
|
1481
|
+
function isProse(lines, own) {
|
|
1482
|
+
if (lines.length < PROSE_LINES) return false;
|
|
1483
|
+
const size = median(lines.map((l) => l.fontSize)) || 10;
|
|
1484
|
+
if (own.right - own.left < size * PROSE_MEASURE_EM) return false;
|
|
1485
|
+
const asked = lines.slice(0, -1);
|
|
1486
|
+
return asked.filter((l) => l.x + l.width >= own.right - l.fontSize * FULL_LINE_EM).length >= asked.length * PROSE_FULL_SHARE;
|
|
1487
|
+
}
|
|
1488
|
+
/** How many lines a block needs before it can be told for prose. */
|
|
1489
|
+
var PROSE_LINES = 5;
|
|
1490
|
+
/** The narrowest measure, in ems, a column of prose is set to. */
|
|
1491
|
+
var PROSE_MEASURE_EM = 20;
|
|
1492
|
+
/** How near the block's edge, in ems, a line has to end to have run out to it. */
|
|
1493
|
+
var FULL_LINE_EM = 2;
|
|
1494
|
+
/** The share of a block's lines that run out to its edge in a column of prose. */
|
|
1495
|
+
var PROSE_FULL_SHARE = .6;
|
|
1496
|
+
/**
|
|
1497
|
+
* §17.3.1.38 — the stops a block of lines is set out on: where text resumes,
|
|
1498
|
+
* after a gap wider than a word space, at the same place on more than one line
|
|
1499
|
+
* — and on at least one of them after a gap only a tab makes.
|
|
1500
|
+
*
|
|
1501
|
+
* A tab's gap is as wide as what stands before it leaves it. An invoice sets
|
|
1502
|
+
* "Date due", "Date of issue" and "Invoice number" over one column of values
|
|
1503
|
+
* at 108pt, and only the shortest label leaves a gap no space could make: the
|
|
1504
|
+
* other two came back with their values a word's width after the label
|
|
1505
|
+
* instead of in the column.
|
|
1506
|
+
*
|
|
1507
|
+
* …or a gap a rule is drawn down. A table ruled into columns needs no white
|
|
1508
|
+
* wider than a space between them, the rule says where one ends: comments.pdf's
|
|
1509
|
+
* Figure 9 sets its codes "xx1", "000" in a column all as wide as each other,
|
|
1510
|
+
* twelve points from the types beside them and a rule between, and the two
|
|
1511
|
+
* columns came back one, "xx1 number", with the rule struck through it.
|
|
1512
|
+
*
|
|
1513
|
+
* @param lines Each line's runs, left to right.
|
|
1514
|
+
* @param rules The rules drawn down the block (see {@link columnRules}).
|
|
1515
|
+
* @returns The x of every stop two lines share.
|
|
1516
|
+
*/
|
|
1517
|
+
function sharedStops(lines, rules = []) {
|
|
1518
|
+
const seen = [];
|
|
1519
|
+
lines.forEach((runs, line) => {
|
|
1520
|
+
let end;
|
|
1521
|
+
for (const run of runs) {
|
|
1522
|
+
if (run.text.replaceAll("�", "").trim() === "") continue;
|
|
1523
|
+
const size = run.fontSizePt || 10;
|
|
1524
|
+
if (end !== void 0 && run.x - end >= size * STOP_GAP_EM) {
|
|
1525
|
+
const from = end;
|
|
1526
|
+
const ruled = rules.some((r) => r.x > from && r.x < run.x && r.minY <= run.y + size * .5 && r.maxY >= run.y);
|
|
1527
|
+
seen.push({
|
|
1528
|
+
x: run.x,
|
|
1529
|
+
line,
|
|
1530
|
+
tab: ruled || run.x - end >= size * TAB_GAP_EM
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
end = Math.max(end ?? run.endX, run.endX);
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
const stops = [];
|
|
1537
|
+
for (const tab of seen) {
|
|
1538
|
+
if (!tab.tab || stops.some((x) => Math.abs(x - tab.x) <= STOP_SLACK_PT)) continue;
|
|
1539
|
+
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);
|
|
1540
|
+
}
|
|
1541
|
+
return stops;
|
|
1542
|
+
}
|
|
1543
|
+
/** The least gap, in ems, that lands a run on a stop the lines around it share. */
|
|
1544
|
+
var STOP_GAP_EM = .5;
|
|
1545
|
+
/**
|
|
1546
|
+
* The rules a page draws down its lines: thin upright paths, a line's height
|
|
1547
|
+
* long at the least — the edges of a table's columns.
|
|
1548
|
+
*
|
|
1549
|
+
* @param vectors The page's paths.
|
|
1550
|
+
*/
|
|
1551
|
+
function columnRules(vectors) {
|
|
1552
|
+
return vectors.filter((v) => v.glyph !== true && v.maxX - v.minX <= COLUMN_RULE_THIN_PT && v.maxY - v.minY >= COLUMN_RULE_LEAST_PT).map((v) => ({
|
|
1553
|
+
x: (v.minX + v.maxX) / 2,
|
|
1554
|
+
minY: v.minY,
|
|
1555
|
+
maxY: v.maxY
|
|
1556
|
+
}));
|
|
1557
|
+
}
|
|
1558
|
+
/** No thicker than this, a path drawn down the page is a rule. */
|
|
1559
|
+
var COLUMN_RULE_THIN_PT = 1;
|
|
1560
|
+
/** …and no shorter than this: a rule stands at least a line of small type tall. */
|
|
1561
|
+
var COLUMN_RULE_LEAST_PT = 5;
|
|
1562
|
+
/** How far from a shared stop a run may start and still stand on it. */
|
|
1563
|
+
var STOP_SLACK_PT = .75;
|
|
1564
|
+
/**
|
|
796
1565
|
* Where a line's INK is, which is not how far its pen travelled.
|
|
797
1566
|
*
|
|
798
1567
|
* A run of spaces advances the pen and marks nothing. basicapi.pdf sets its
|
|
@@ -823,9 +1592,154 @@ function inkSpan(runs) {
|
|
|
823
1592
|
width: Math.max(0, last.endX - trail - x)
|
|
824
1593
|
};
|
|
825
1594
|
}
|
|
1595
|
+
/**
|
|
1596
|
+
* A document's runs as its listings are read (see {@link isCode}).
|
|
1597
|
+
*
|
|
1598
|
+
* A typewriter face is a listing's only where the document is set in another:
|
|
1599
|
+
* a letter typed throughout, a screenplay, a page set in Courier for its even
|
|
1600
|
+
* widths, is PROSE in that face, and its lines run on and its double spaces
|
|
1601
|
+
* are spaces. Where most of what the document shows is typewritten, its runs
|
|
1602
|
+
* are read as any other face's.
|
|
1603
|
+
*
|
|
1604
|
+
* @param pages Each page's runs.
|
|
1605
|
+
* @returns The same runs, or copies without `fixedPitch` where the typewriter
|
|
1606
|
+
* face is the document's own.
|
|
1607
|
+
*/
|
|
1608
|
+
function typewritten(pages) {
|
|
1609
|
+
let typed = 0;
|
|
1610
|
+
let all = 0;
|
|
1611
|
+
for (const runs of pages) for (const run of runs) {
|
|
1612
|
+
const letters = run.text.replace(/\s/gu, "").length;
|
|
1613
|
+
all += letters;
|
|
1614
|
+
if (run.fixedPitch === true) typed += letters;
|
|
1615
|
+
}
|
|
1616
|
+
if (typed === 0 || typed * 2 <= all) return pages;
|
|
1617
|
+
return pages.map((runs) => runs.map((run) => {
|
|
1618
|
+
if (run.fixedPitch !== true) return run;
|
|
1619
|
+
const { fixedPitch: _typed, ...prose } = run;
|
|
1620
|
+
return prose;
|
|
1621
|
+
}));
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Whether a line is CODE: every word of it set in a typewriter face (§9.8.2).
|
|
1625
|
+
*
|
|
1626
|
+
* A listing is set line by line with its indents and its comments lined up in
|
|
1627
|
+
* columns of the typewriter's cells, and re-set as prose it is no program:
|
|
1628
|
+
* comments.pdf's "v0 := ld state[748]" and "st sp[0], v0" ran together as
|
|
1629
|
+
* one line of a paragraph, and so did line 4 of Figure 1 and its line 5.
|
|
1630
|
+
*/
|
|
1631
|
+
function isCode(runs) {
|
|
1632
|
+
const inked = runs.filter((r) => r.text.trim() !== "");
|
|
1633
|
+
return inked.length > 0 && inked.every((r) => r.fixedPitch === true);
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Whether the white between two runs of a typewriter face LINES UP what
|
|
1637
|
+
* follows it: wider than a cell and a half, where a word space is one cell.
|
|
1638
|
+
* A listing puts its comments in a column that way, and a space in their
|
|
1639
|
+
* place set each comment against its own line's code.
|
|
1640
|
+
*/
|
|
1641
|
+
function aligned(before, after) {
|
|
1642
|
+
if (before.fixedPitch !== true || after.fixedPitch !== true) return false;
|
|
1643
|
+
const letters = [...before.text].length;
|
|
1644
|
+
if (letters === 0) return false;
|
|
1645
|
+
const cell = (before.endX - before.x) / letters;
|
|
1646
|
+
return cell > 0 && after.x - before.endX >= cell * ALIGNED_CELLS;
|
|
1647
|
+
}
|
|
1648
|
+
/** How many of a typewriter's cells of white line up what follows them. */
|
|
1649
|
+
var ALIGNED_CELLS = 1.5;
|
|
1650
|
+
/**
|
|
1651
|
+
* A page's runs less its code listings: the rows every word of which is set
|
|
1652
|
+
* in a typewriter face (see {@link isCode}).
|
|
1653
|
+
*
|
|
1654
|
+
* A listing says nothing about the page's columns. comments.pdf sets two
|
|
1655
|
+
* across the head of its third page, the code at the left and each comment
|
|
1656
|
+
* lined up at one x: the white between them — thirty-five lines of it at the
|
|
1657
|
+
* same place — was taken for the gutter of a page in two columns, while the
|
|
1658
|
+
* real one, under the listings, was crossed by every comment and not found.
|
|
1659
|
+
* The code came back as a column of prose and the comments as another.
|
|
1660
|
+
*/
|
|
1661
|
+
function withoutListings(runs) {
|
|
1662
|
+
if (!runs.some((r) => r.fixedPitch === true)) return runs;
|
|
1663
|
+
const fontSize = median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
|
|
1664
|
+
const listed = new Set(rowsOf(runs, fontSize).filter(isCode).flat());
|
|
1665
|
+
return listed.size > 0 ? runs.filter((r) => !listed.has(r)) : runs;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* A page's runs less its tables (see {@link isTableRow}): the rows of every
|
|
1669
|
+
* block of three and more table rows. The same runs where it has none.
|
|
1670
|
+
*
|
|
1671
|
+
* The white between a table's columns says nothing about the columns of the
|
|
1672
|
+
* text around it. comments.pdf sets Figure 13, a benchmark and nine figures a
|
|
1673
|
+
* row, twenty-six rows deep, over two columns of text: its nine gaps were
|
|
1674
|
+
* taken for the page's gutters, the text's own was crossed by its rows and
|
|
1675
|
+
* not found, and the whole page came back as one table, each line of the text
|
|
1676
|
+
* a row of it.
|
|
1677
|
+
*/
|
|
1678
|
+
function withoutTables(runs) {
|
|
1679
|
+
const rows = rowsOf(runs, median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10).filter((row) => row.some((r) => r.text.trim() !== ""));
|
|
1680
|
+
const tabled = new Set(blocksOf(rows, isTableRow, LEAST_TABLE_ROWS).flat(2));
|
|
1681
|
+
return tabled.size > 0 ? runs.filter((r) => !tabled.has(r)) : runs;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* The rows of a page that stand in runs of at least `least` for which `holds`
|
|
1685
|
+
* is true, each run of them a block.
|
|
1686
|
+
*/
|
|
1687
|
+
function blocksOf(rows, holds, least) {
|
|
1688
|
+
const out = [];
|
|
1689
|
+
for (let from = 0; from < rows.length;) {
|
|
1690
|
+
let to = from;
|
|
1691
|
+
while (to < rows.length && holds(rows[to])) to++;
|
|
1692
|
+
if (to - from >= least) out.push(rows.slice(from, to));
|
|
1693
|
+
from = Math.max(to, from + 1);
|
|
1694
|
+
}
|
|
1695
|
+
return out;
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Whether a row is a TABLE's: four cells or more — its words as far as each
|
|
1699
|
+
* gap wider than an em — and none of them as long as a line of prose. A row
|
|
1700
|
+
* of a page in columns is two long pieces; comments.pdf's Figure 13 is ten
|
|
1701
|
+
* short ones a row, a benchmark and nine figures, twenty-six rows deep.
|
|
1702
|
+
*/
|
|
1703
|
+
function isTableRow(runs) {
|
|
1704
|
+
const cells = cellsOf(runs);
|
|
1705
|
+
if (cells.length < TABLE_CELLS) return false;
|
|
1706
|
+
const size = median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
|
|
1707
|
+
return cells.every((c) => c.to - c.from <= size * WIDEST_CELL_EM);
|
|
1708
|
+
}
|
|
1709
|
+
/** A row's ink as its cells: its words, as far as each gap wider than an em. */
|
|
1710
|
+
function cellsOf(runs) {
|
|
1711
|
+
const inks = runs.map((run) => ({
|
|
1712
|
+
run,
|
|
1713
|
+
ink: runInk(run)
|
|
1714
|
+
})).filter((r) => r.ink !== void 0).sort((a, b) => a.ink[0] - b.ink[0]);
|
|
1715
|
+
const cells = [];
|
|
1716
|
+
for (const { run, ink } of inks) {
|
|
1717
|
+
const last = cells[cells.length - 1];
|
|
1718
|
+
if (last && ink[0] - last.to < (run.fontSizePt || 10) * CELL_GAP_EM) last.to = Math.max(last.to, ink[1]);
|
|
1719
|
+
else cells.push({
|
|
1720
|
+
from: ink[0],
|
|
1721
|
+
to: ink[1]
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
return cells;
|
|
1725
|
+
}
|
|
1726
|
+
/** How many cells make a row a table's. */
|
|
1727
|
+
var TABLE_CELLS = 4;
|
|
1728
|
+
/** How many such rows make a table. */
|
|
1729
|
+
var LEAST_TABLE_ROWS = 3;
|
|
1730
|
+
/**
|
|
1731
|
+
* …and how many make one across the page's columns with no row of it
|
|
1732
|
+
* reaching over the gutter on its own: more than two columns' rows fall
|
|
1733
|
+
* together by chance.
|
|
1734
|
+
*/
|
|
1735
|
+
var SPANNING_TABLE_ROWS = 6;
|
|
1736
|
+
/** The white between two cells of a table row, in ems: wider than a word space ever is. */
|
|
1737
|
+
var CELL_GAP_EM = 1;
|
|
1738
|
+
/** The longest a table's cell runs, in ems; a line of a column of prose runs further. */
|
|
1739
|
+
var WIDEST_CELL_EM = 12;
|
|
826
1740
|
/** 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)));
|
|
1741
|
+
function lineOf(runs, y, fontSize, stepped, shared = [], spaces) {
|
|
1742
|
+
const { spans: ordered, stops, pieces } = lineSpans(runs, fontSize, stepped || runs.length > 1 && !runs.some((r) => SPACE.test(r.text)), shared, spaces);
|
|
829
1743
|
const spans = ordered.every((s) => s.text.trim() === "" || isRightToLeft(s.text)) ? [...ordered].reverse() : ordered;
|
|
830
1744
|
const ink = inkSpan(runs);
|
|
831
1745
|
return {
|
|
@@ -833,8 +1747,12 @@ function lineOf(runs, y, fontSize, stepped) {
|
|
|
833
1747
|
width: ink.width,
|
|
834
1748
|
y,
|
|
835
1749
|
fontSize,
|
|
836
|
-
...tabbed(runs, fontSize) ? { tabbed: true } : {},
|
|
837
|
-
...stops.length > 0 ? {
|
|
1750
|
+
...tabbed(runs, fontSize) || stops.length > 0 ? { tabbed: true } : {},
|
|
1751
|
+
...stops.length > 0 ? {
|
|
1752
|
+
stops,
|
|
1753
|
+
pieces
|
|
1754
|
+
} : {},
|
|
1755
|
+
...isCode(runs) ? { code: true } : {},
|
|
838
1756
|
text: spans.map((s) => s.text).join("").replace(/\s+/g, " ").trim(),
|
|
839
1757
|
spans
|
|
840
1758
|
};
|
|
@@ -888,10 +1806,25 @@ var TAB_GAP_EM = 2.5;
|
|
|
888
1806
|
* {@link stepsBetweenWords}). The tight one still clears the gaps a producer
|
|
889
1807
|
* leaves INSIDE a word when it splits one for kerning, which measure eight
|
|
890
1808
|
* hundredths of an em at their widest across this corpus.
|
|
1809
|
+
*
|
|
1810
|
+
* …and a page that draws its spaces may step some of them all the same: a
|
|
1811
|
+
* gap as wide as most of the face's own space is one, set by a step instead
|
|
1812
|
+
* of a glyph. freeculture.pdf's index draws the space in "academic journals"
|
|
1813
|
+
* and steps the one after "journals," — 1.71 points against a space of 2.03,
|
|
1814
|
+
* short of a quarter em — and the entry came back "journals,262,280–82".
|
|
891
1815
|
*/
|
|
892
1816
|
function spaceGap(prev, fontSize, stepped) {
|
|
893
|
-
|
|
1817
|
+
const size = prev.fontSizePt || fontSize;
|
|
1818
|
+
if (stepped) return size * STEPPED_SPACE_EM;
|
|
1819
|
+
const space = prev.spaceWidthPt;
|
|
1820
|
+
const face = space !== void 0 && space >= size * LEAST_FACE_SPACE_EM && space <= size * MOST_FACE_SPACE_EM ? space * STEPPED_SPACE_SHARE : Infinity;
|
|
1821
|
+
return Math.min(size * DRAWN_SPACE_EM, face);
|
|
894
1822
|
}
|
|
1823
|
+
/** How much of the face's own space a step has to be to stand for one. */
|
|
1824
|
+
var STEPPED_SPACE_SHARE = .75;
|
|
1825
|
+
/** The narrowest and widest space, in ems, a face is taken at its word for. */
|
|
1826
|
+
var LEAST_FACE_SPACE_EM = .15;
|
|
1827
|
+
var MOST_FACE_SPACE_EM = .5;
|
|
895
1828
|
/**
|
|
896
1829
|
* §17.3.1.25 — one character of a LEADER, the dotted rule that carries the eye
|
|
897
1830
|
* across a table of contents.
|
|
@@ -906,6 +1839,17 @@ function spaceGap(prev, fontSize, stepped) {
|
|
|
906
1839
|
function isLeader(text) {
|
|
907
1840
|
return text.length === 1 && LEADER_CHARS.has(text);
|
|
908
1841
|
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Whether `run` carries on the leader `prev` is a character of: the same
|
|
1844
|
+
* character, a step no wider than a word space on from it. A dash a column
|
|
1845
|
+
* further on is a cell's: comments.pdf's Figure 13 marks three empty figures
|
|
1846
|
+
* "-", a column apart, and joined as one leader they came back "---", one
|
|
1847
|
+
* cell standing where three had.
|
|
1848
|
+
*/
|
|
1849
|
+
function continuesLeader(prev, run, fontSize) {
|
|
1850
|
+
const dots = prev.text.endsWith(".") && run.text.startsWith(".");
|
|
1851
|
+
return (isLeader(prev.text) && prev.text === run.text || dots) && run.x - prev.endX < (run.fontSizePt || fontSize) * CELL_GAP_EM;
|
|
1852
|
+
}
|
|
909
1853
|
var LEADER_CHARS = new Set([
|
|
910
1854
|
".",
|
|
911
1855
|
"·",
|
|
@@ -930,17 +1874,48 @@ function stepsBetweenWords(runs) {
|
|
|
930
1874
|
if (runs.length < 8) return false;
|
|
931
1875
|
return runs.filter((r) => /\s/u.test(r.text)).length / runs.length < .05;
|
|
932
1876
|
}
|
|
933
|
-
function lineSpans(runs, fontSize, stepped) {
|
|
1877
|
+
function lineSpans(runs, fontSize, stepped, shared = [], spaces) {
|
|
934
1878
|
const spans = [];
|
|
935
1879
|
const stops = [];
|
|
1880
|
+
const pieces = [{
|
|
1881
|
+
from: Infinity,
|
|
1882
|
+
to: -Infinity
|
|
1883
|
+
}];
|
|
1884
|
+
const table = isTableRow(runs);
|
|
1885
|
+
const code = isCode(runs);
|
|
936
1886
|
const body = runs.filter((r) => (r.fontSizePt || fontSize) > fontSize * SCRIPT_SIZE);
|
|
937
1887
|
const baseline = median((body.length > 0 ? body : runs).map((r) => r.y));
|
|
938
1888
|
let prev;
|
|
939
|
-
|
|
940
|
-
|
|
1889
|
+
let inked;
|
|
1890
|
+
for (const [i, run] of runs.entries()) {
|
|
1891
|
+
if (run.text.trim() === "" && inked !== void 0) {
|
|
1892
|
+
const next = runs.slice(i + 1).find((r) => r.text.trim() !== "");
|
|
1893
|
+
if ((run.endX - run.x <= 0 || next !== void 0 && next.x < (run.x + run.endX) / 2) && next !== void 0 && next.x - inked < spaceGap(next, fontSize, true)) continue;
|
|
1894
|
+
}
|
|
1895
|
+
if (run.text.replaceAll("�", "") === "" && run.text !== "") continue;
|
|
1896
|
+
if (!code && prev !== void 0 && prev.text.trim() === "" && run.text.trim() !== "" && inked !== void 0 && run.x - inked >= (run.fontSizePt || fontSize) * STOP_GAP_EM && shared.some((x) => Math.abs(x - run.x) <= STOP_SLACK_PT)) {
|
|
1897
|
+
while (spans.length > 0 && spans[spans.length - 1].text.trim() === "") {
|
|
1898
|
+
if (spans[spans.length - 1].text === " ") break;
|
|
1899
|
+
spans.pop();
|
|
1900
|
+
}
|
|
941
1901
|
spans.push({ text: " " });
|
|
942
1902
|
stops.push(run.x);
|
|
943
|
-
|
|
1903
|
+
pieces.push({
|
|
1904
|
+
from: Infinity,
|
|
1905
|
+
to: -Infinity
|
|
1906
|
+
});
|
|
1907
|
+
} else if (prev !== void 0 && run.x - prev.endX > spaceGap(prev, fontSize, stepped) && !continuesLeader(prev, run, fontSize)) {
|
|
1908
|
+
const size = run.fontSizePt || fontSize;
|
|
1909
|
+
const onStop = inked !== void 0 && run.x - inked >= size * STOP_GAP_EM && shared.some((x) => Math.abs(x - run.x) <= STOP_SLACK_PT);
|
|
1910
|
+
if (run.x - prev.endX >= size * TAB_GAP_EM || onStop || aligned(prev, run) || table && run.x - prev.endX >= size * CELL_GAP_EM) {
|
|
1911
|
+
spans.push({ text: " " });
|
|
1912
|
+
stops.push(run.x);
|
|
1913
|
+
pieces.push({
|
|
1914
|
+
from: Infinity,
|
|
1915
|
+
to: -Infinity
|
|
1916
|
+
});
|
|
1917
|
+
} else if (!/\s$/u.test(prev.text) && !/^\s/u.test(run.text) || run.x - prev.endX > (prev.fontSizePt || fontSize) * DRAWN_SPACE_EM) spans.push(fittedSpace(spaceAfter(spans[spans.length - 1], run), prev, run, spaces));
|
|
1918
|
+
}
|
|
944
1919
|
spans.push({
|
|
945
1920
|
text: run.text.replaceAll("�", ""),
|
|
946
1921
|
sizePt: run.fontSizePt,
|
|
@@ -957,12 +1932,70 @@ function lineSpans(runs, fontSize, stepped) {
|
|
|
957
1932
|
...run.href !== void 0 ? { href: run.href } : {}
|
|
958
1933
|
});
|
|
959
1934
|
prev = run;
|
|
1935
|
+
if (run.text.trim() !== "") {
|
|
1936
|
+
inked = Math.max(inked ?? run.endX, run.endX);
|
|
1937
|
+
const piece = pieces[pieces.length - 1];
|
|
1938
|
+
piece.from = Math.min(piece.from, run.x);
|
|
1939
|
+
piece.to = Math.max(piece.to, run.endX);
|
|
1940
|
+
}
|
|
960
1941
|
}
|
|
961
1942
|
return {
|
|
962
1943
|
spans,
|
|
963
|
-
stops
|
|
1944
|
+
stops,
|
|
1945
|
+
pieces
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
/**
|
|
1949
|
+
* §17.3.2.43 — a word space set as narrow as the page set it.
|
|
1950
|
+
*
|
|
1951
|
+
* TeX justifies a line by stretching its word spaces or SHRINKING them, and a
|
|
1952
|
+
* line it shrank holds more than the same words with the face's own spaces:
|
|
1953
|
+
* re-set with those, its last word no longer fits and goes to the next line,
|
|
1954
|
+
* and the paragraph grows by a line. comments.pdf sets its 9pt Times with
|
|
1955
|
+
* word spaces of 1.70 to 3.44 points against the face's 2.24, and every column
|
|
1956
|
+
* of it ran one to four lines long. A space the page set narrower than the
|
|
1957
|
+
* face's is set at that share of the face's width (`w:w`, whole percent
|
|
1958
|
+
* rounded down, so the line still fits). A wider one is left alone: a line TeX
|
|
1959
|
+
* stretched fits with the face's spaces too.
|
|
1960
|
+
*
|
|
1961
|
+
* The share of the glyph and not a spacing after it: LibreOffice lays no
|
|
1962
|
+
* character spacing (§17.3.2.35) after the last character of a run, and a
|
|
1963
|
+
* run that is one space has nothing else — Word and LibreOffice both narrow
|
|
1964
|
+
* the glyph itself.
|
|
1965
|
+
*
|
|
1966
|
+
* Only in a face the document carries, whose space is known: a substitute's
|
|
1967
|
+
* could be anything.
|
|
1968
|
+
*
|
|
1969
|
+
* @param space The space as the line would have it.
|
|
1970
|
+
* @param before The run the space follows.
|
|
1971
|
+
* @param after The run it precedes.
|
|
1972
|
+
* @param spaces The space each face the document carries is set with.
|
|
1973
|
+
* @returns The space, narrowed where the page set it narrower.
|
|
1974
|
+
*/
|
|
1975
|
+
function fittedSpace(space, before, after, spaces) {
|
|
1976
|
+
if (spaces === void 0 || /\s$/u.test(before.text) || /^\s/u.test(after.text)) return space;
|
|
1977
|
+
const advance = before.fontName !== void 0 ? spaces.get(before.fontName) : void 0;
|
|
1978
|
+
const size = space.sizePt ?? before.fontSizePt;
|
|
1979
|
+
if (advance === void 0 || !(size > 0)) return space;
|
|
1980
|
+
const own = advance / 1e3 * size;
|
|
1981
|
+
const gap = after.x - before.endX;
|
|
1982
|
+
if (gap > own - FIT_SLACK_PT) return space;
|
|
1983
|
+
const share = Math.floor(gap / own * 100) / 100;
|
|
1984
|
+
return {
|
|
1985
|
+
...space,
|
|
1986
|
+
widthScale: Math.max(share, 1 - MOST_SHRINK)
|
|
964
1987
|
};
|
|
965
1988
|
}
|
|
1989
|
+
/** A space this much narrower than the face's is the page's rounding, not a shrink. */
|
|
1990
|
+
var FIT_SLACK_PT = .02;
|
|
1991
|
+
/**
|
|
1992
|
+
* The most of a space a line may take back. TeX shrinks a Times space by a
|
|
1993
|
+
* quarter of itself and a Computer Modern one by a third — but the space after
|
|
1994
|
+
* a word in type is written in the type's face, and comments.pdf sets its
|
|
1995
|
+
* `primes` in 9pt Courier-wide cmtt9 and the space after it in Times: 2.25
|
|
1996
|
+
* points of a 4.72-point space. A gap narrower still is not a word space.
|
|
1997
|
+
*/
|
|
1998
|
+
var MOST_SHRINK = .6;
|
|
966
1999
|
/**
|
|
967
2000
|
* §17.3.2.42 — a run set off the line's baseline, and smaller, as the script it
|
|
968
2001
|
* is.
|
|
@@ -1030,41 +2063,234 @@ function ruleOfCharacters(line) {
|
|
|
1030
2063
|
}
|
|
1031
2064
|
/** How many of them it takes before a row of marks is a rule. */
|
|
1032
2065
|
var RULE_CHARS = 3;
|
|
1033
|
-
function groupIntoParagraphs(lines, column, pageHeight = 0) {
|
|
2066
|
+
function groupIntoParagraphs(lines, column, pageHeight = 0, before, eachLine = false, reach) {
|
|
1034
2067
|
const groups = [];
|
|
1035
2068
|
const gaps = [];
|
|
2069
|
+
const flushEnds = column ? lines.filter((l) => Math.abs(column.right - (l.x + l.width)) <= JUSTIFIED_SLACK_PT).length : 0;
|
|
2070
|
+
const setJustified = column !== void 0 && lines.length >= JUSTIFIED_COLUMN_LINES && flushEnds >= lines.length * JUSTIFIED_COLUMN_SHARE;
|
|
2071
|
+
const hangsAt = column !== void 0 ? hangingEdge(lines, column) : void 0;
|
|
1036
2072
|
let prev;
|
|
2073
|
+
let beforePrev;
|
|
1037
2074
|
for (const line of lines) {
|
|
1038
2075
|
const gap = prev !== void 0 ? prev.y - line.y : 0;
|
|
1039
2076
|
const opened = prev !== void 0 && gap > line.fontSize * 1.5;
|
|
1040
|
-
|
|
2077
|
+
const goesOn = hangsAt !== void 0 && line.x >= hangsAt;
|
|
2078
|
+
if (groups.length === 0 || eachLine || opened || ruleOfCharacters(line) || line.code === true || hangsAt !== void 0 && line.x < hangsAt || line.tabbed === true || prev !== void 0 && opensItem(prev, line) || prev !== void 0 && (ruleOfCharacters(prev) || prev.code === true || !goesOn && (endedParagraph(prev, line, column) || reach !== void 0 && roomForWord(prev, line, reach) || setJustified && endedJustified(beforePrev, prev, line, column) || column !== void 0 && endedCentred(prev, line, column)) || carriesLeader(prev) || prev.tabbed === true)) {
|
|
1041
2079
|
groups.push([]);
|
|
1042
|
-
gaps.push(prev === void 0 ? 0 : gap);
|
|
2080
|
+
gaps.push(prev === void 0 ? before !== void 0 ? before.y - line.y : 0 : gap);
|
|
1043
2081
|
}
|
|
1044
2082
|
groups[groups.length - 1].push(line);
|
|
2083
|
+
beforePrev = prev;
|
|
1045
2084
|
prev = line;
|
|
1046
2085
|
}
|
|
1047
2086
|
const starts = lines.map((l) => l.x).sort((a, b) => a - b);
|
|
1048
|
-
const columnLeft = starts[Math.floor(starts.length * COLUMN_LEFT_QUANTILE)] ?? 0;
|
|
2087
|
+
const columnLeft = column?.left ?? starts[Math.floor(starts.length * COLUMN_LEFT_QUANTILE)] ?? 0;
|
|
2088
|
+
const heights = [];
|
|
1049
2089
|
return groups.map((g, i) => {
|
|
1050
2090
|
const first = g[0];
|
|
1051
2091
|
const fontSize = Math.max(...g.map((l) => l.fontSize));
|
|
1052
|
-
const
|
|
2092
|
+
const inner = g.slice(1).map((l, k) => g[k].y - l.y);
|
|
2093
|
+
let lineHeight = Math.min(Math.max(inner.length > 0 ? median(inner) : fontSize * NATURAL_LINE_EM, fontSize), fontSize * TALLEST_LINE_EM);
|
|
2094
|
+
const above = i > 0 ? heights[i - 1] : before?.lineHeight;
|
|
2095
|
+
const gap = gaps[i] ?? 0;
|
|
2096
|
+
if (inner.length === 0 && above !== void 0) {
|
|
2097
|
+
const room = (gap - (1 - BASELINE_AT) * above) / BASELINE_AT;
|
|
2098
|
+
if (room < lineHeight) lineHeight = Math.max(room, fontSize);
|
|
2099
|
+
}
|
|
2100
|
+
heights.push(lineHeight);
|
|
2101
|
+
const opened = above !== void 0 ? gap - (1 - BASELINE_AT) * above - BASELINE_AT * lineHeight : 0;
|
|
1053
2102
|
const most = pageHeight > 0 ? pageHeight / 3 : fontSize * 3;
|
|
1054
|
-
const spacingBefore = opened >
|
|
2103
|
+
const spacingBefore = opened > SPACING_NOISE_PT ? Math.min(opened, most) : void 0;
|
|
1055
2104
|
const stops = g.length === 1 ? first.stops ?? [] : [];
|
|
2105
|
+
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
2106
|
return {
|
|
1057
2107
|
spans: joinLines(g),
|
|
1058
2108
|
fontSize,
|
|
1059
2109
|
top: first.y,
|
|
1060
|
-
|
|
2110
|
+
bottom: g[g.length - 1].y,
|
|
2111
|
+
...stops.length > 0 ? {
|
|
2112
|
+
stops: stops.map((x) => x - columnLeft),
|
|
2113
|
+
pieces: (first.pieces ?? []).map((p) => ({
|
|
2114
|
+
from: p.from - columnLeft,
|
|
2115
|
+
to: p.to - columnLeft
|
|
2116
|
+
}))
|
|
2117
|
+
} : {},
|
|
1061
2118
|
...spacingBefore !== void 0 ? { spacingBefore } : {},
|
|
1062
|
-
|
|
1063
|
-
...
|
|
2119
|
+
lineHeight,
|
|
2120
|
+
...aligned,
|
|
2121
|
+
...indentOf(g, columnLeft, aligned.alignment),
|
|
2122
|
+
lineCount: g.length,
|
|
2123
|
+
left: Math.min(...g.map((l) => l.x)),
|
|
2124
|
+
right: Math.max(...g.map((l) => l.x + l.width)),
|
|
2125
|
+
...opened > most ? { far: g } : {}
|
|
1064
2126
|
};
|
|
1065
2127
|
});
|
|
1066
2128
|
}
|
|
1067
2129
|
/**
|
|
2130
|
+
* Where a column's paragraphs go on, where they HANG: the first line of each
|
|
2131
|
+
* at the column's edge — or at a level set in from it — and the lines after it
|
|
2132
|
+
* set in further, as an index sets its entries and a bibliography its
|
|
2133
|
+
* references. freeculture.pdf's index sets "democracy:" at the edge, "digital
|
|
2134
|
+
* sharing within, 184" and the other entries under it a level in, and the
|
|
2135
|
+
* lines that carry an entry on — "41–42, 43, 44–45" — at a level further in
|
|
2136
|
+
* again; read by the measure alone, its short entries ran together, and a line
|
|
2137
|
+
* back at the edge after one set in was taken for the second line of a
|
|
2138
|
+
* paragraph whose first is set in.
|
|
2139
|
+
*
|
|
2140
|
+
* The level a paragraph goes on at is reached from full lines, the ones the
|
|
2141
|
+
* entry ran to the measure and turned from, and many of its lines are short:
|
|
2142
|
+
* they end their entries. The level a paragraph's own first line is set in to
|
|
2143
|
+
* is reached from short lines — the paragraph before ended there — or runs to
|
|
2144
|
+
* the measure itself.
|
|
2145
|
+
*
|
|
2146
|
+
* @param lines The column's lines, top to bottom.
|
|
2147
|
+
* @param column The column they are set in.
|
|
2148
|
+
* @returns How far in a line goes on its paragraph; a line nearer the edge
|
|
2149
|
+
* opens one. `undefined` where the column's paragraphs do not hang.
|
|
2150
|
+
*/
|
|
2151
|
+
function hangingEdge(lines, column) {
|
|
2152
|
+
const width = column.right - column.left;
|
|
2153
|
+
if (!(width > 0) || lines.length < HANGING_LINES) return void 0;
|
|
2154
|
+
const size = median(lines.map((l) => l.fontSize)) || 10;
|
|
2155
|
+
const edge = Math.min(...lines.map((l) => l.x));
|
|
2156
|
+
const short = (l) => column.right - (l.x + l.width) > width * .25;
|
|
2157
|
+
const levels = [];
|
|
2158
|
+
for (let k = 1; k < lines.length; k++) {
|
|
2159
|
+
const prev = lines[k - 1];
|
|
2160
|
+
const line = lines[k];
|
|
2161
|
+
const setIn = line.x - edge;
|
|
2162
|
+
if (setIn < size * .5 || setIn > size * HANGING_DEEPEST_EM) continue;
|
|
2163
|
+
let level = levels.find((v) => Math.abs(v.x - line.x) <= size * HANGING_SLACK_EM);
|
|
2164
|
+
if (level === void 0) {
|
|
2165
|
+
level = {
|
|
2166
|
+
x: line.x,
|
|
2167
|
+
lines: 0,
|
|
2168
|
+
afterFull: 0,
|
|
2169
|
+
short: 0
|
|
2170
|
+
};
|
|
2171
|
+
levels.push(level);
|
|
2172
|
+
}
|
|
2173
|
+
level.lines++;
|
|
2174
|
+
if (!short(prev)) level.afterFull++;
|
|
2175
|
+
if (short(line)) level.short++;
|
|
2176
|
+
}
|
|
2177
|
+
const goesOn = levels.filter((v) => v.lines >= HANGING_LEAST && v.afterFull >= v.lines * HANGING_FULL_SHARE && v.short >= v.lines * HANGING_SHORT_SHARE);
|
|
2178
|
+
if (goesOn.length === 0) return void 0;
|
|
2179
|
+
let pairs = 0;
|
|
2180
|
+
let fullPairs = 0;
|
|
2181
|
+
for (let k = 1; k < lines.length; k++) {
|
|
2182
|
+
if (lines[k - 1].x - edge >= size * .5 || lines[k].x - edge >= size * .5) continue;
|
|
2183
|
+
pairs++;
|
|
2184
|
+
if (!short(lines[k - 1])) fullPairs++;
|
|
2185
|
+
}
|
|
2186
|
+
if (fullPairs > Math.max(1, pairs * HANGING_RUN_ON_SHARE)) return void 0;
|
|
2187
|
+
return Math.min(...goesOn.map((v) => v.x)) - size * HANGING_SLACK_EM;
|
|
2188
|
+
}
|
|
2189
|
+
/** How many lines a column needs before its paragraphs can be seen to hang. */
|
|
2190
|
+
var HANGING_LINES = 8;
|
|
2191
|
+
/** …and how many lines a level needs before a paragraph can be seen to go on at it. */
|
|
2192
|
+
var HANGING_LEAST = 2;
|
|
2193
|
+
/** How far off a level, in ems, a line may start and still stand at it. */
|
|
2194
|
+
var HANGING_SLACK_EM = .25;
|
|
2195
|
+
/** How far in from the edge, in ems, a level may stand. */
|
|
2196
|
+
var HANGING_DEEPEST_EM = 4;
|
|
2197
|
+
/** How many of a level's lines, as a share of them, follow full lines where paragraphs go on at it. */
|
|
2198
|
+
var HANGING_FULL_SHARE = .8;
|
|
2199
|
+
/** …and how many of them are short, ending their paragraphs. */
|
|
2200
|
+
var HANGING_SHORT_SHARE = .3;
|
|
2201
|
+
/** How many lines at the edge, as a share of those after another, may follow a full one. */
|
|
2202
|
+
var HANGING_RUN_ON_SHARE = .6;
|
|
2203
|
+
/**
|
|
2204
|
+
* Whether the first word of `next` would have fit on `prev` short of `reach` —
|
|
2205
|
+
* so the page did not break `prev` for want of room, and ended it.
|
|
2206
|
+
*
|
|
2207
|
+
* The width of the word is its share of its line's: a guess good to a letter,
|
|
2208
|
+
* and the margin it is compared with is the rest of the sheet.
|
|
2209
|
+
*/
|
|
2210
|
+
function roomForWord(prev, next, reach) {
|
|
2211
|
+
const text = next.text.trimStart();
|
|
2212
|
+
const word = text.split(/\s/u)[0] ?? "";
|
|
2213
|
+
if (word.length === 0) return false;
|
|
2214
|
+
const width = next.width * word.length / text.length;
|
|
2215
|
+
return reach - (prev.x + prev.width) > width + prev.fontSize * WORD_SPACE_EM;
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Whether a centred line ended its paragraph: the line under it is centred too,
|
|
2219
|
+
* and its first word would have fit on this one. A centred paragraph that wraps
|
|
2220
|
+
* fills its lines to the measure but the last, as any paragraph does; lines a
|
|
2221
|
+
* page centres one by one do not. comments.pdf centres its authors'
|
|
2222
|
+
* affiliations a line apiece — "{gal,brendan,…}@mozilla.com", and under it
|
|
2223
|
+
* "Adobe Corporation" — and run together they came back re-wrapped a line
|
|
2224
|
+
* shorter, the columns under them risen into the white.
|
|
2225
|
+
*/
|
|
2226
|
+
function endedCentred(prev, next, column) {
|
|
2227
|
+
const width = column.right - column.left;
|
|
2228
|
+
if (!(width > 0)) return false;
|
|
2229
|
+
const centred = (l) => {
|
|
2230
|
+
const lead = l.x - column.left;
|
|
2231
|
+
const trail = column.right - (l.x + l.width);
|
|
2232
|
+
return Math.abs(lead - trail) <= width * CENTRED_SLACK && Math.min(lead, trail) > 0;
|
|
2233
|
+
};
|
|
2234
|
+
if (!centred(prev) || !centred(next)) return false;
|
|
2235
|
+
const inset = (l) => Math.min(l.x - column.left, column.right - (l.x + l.width));
|
|
2236
|
+
if (Math.max(inset(prev), inset(next)) < width * CENTRED_INSET) return false;
|
|
2237
|
+
const text = next.text.trimStart();
|
|
2238
|
+
const word = text.split(/\s/u)[0] ?? "";
|
|
2239
|
+
if (word.length === 0) return false;
|
|
2240
|
+
const wordWidth = next.width * word.length / text.length;
|
|
2241
|
+
return width - prev.width > wordWidth + prev.fontSize * WORD_SPACE_EM;
|
|
2242
|
+
}
|
|
2243
|
+
/** How far off the middle of the measure, as a share of it, a centred line may stand. */
|
|
2244
|
+
var CENTRED_SLACK = .06;
|
|
2245
|
+
/**
|
|
2246
|
+
* How far in from both edges, as a share of the measure, a centred line
|
|
2247
|
+
* stands: further than a full line stands in from a column whose edges are
|
|
2248
|
+
* its bullets' and an overfull line's (comments.pdf's, a thirtieth at most),
|
|
2249
|
+
* and no further than a block of names needs — the paper's second line of
|
|
2250
|
+
* authors stands in by a thirteenth, and asked for a tenth, the line under it
|
|
2251
|
+
* came back run into it.
|
|
2252
|
+
*/
|
|
2253
|
+
var CENTRED_INSET = .05;
|
|
2254
|
+
/**
|
|
2255
|
+
* Whether a line of a column set against both edges ended its paragraph: it
|
|
2256
|
+
* stops short of the measure, and the line after it is indented or its first
|
|
2257
|
+
* word would have fit on it.
|
|
2258
|
+
*
|
|
2259
|
+
* Not where a line beside it stops at the same place: that is the edge of a
|
|
2260
|
+
* narrower block, not the end of the text. A quotation set in from both sides
|
|
2261
|
+
* stops short of the measure on every line, and freeculture.pdf's came back a
|
|
2262
|
+
* paragraph a line.
|
|
2263
|
+
*/
|
|
2264
|
+
function endedJustified(before, prev, next, column) {
|
|
2265
|
+
const end = prev.x + prev.width;
|
|
2266
|
+
if (column.right - end <= prev.fontSize * WORD_SPACE_EM) return false;
|
|
2267
|
+
const edge = (l) => l !== void 0 && Math.abs(l.x + l.width - end) <= JUSTIFIED_SLACK_PT;
|
|
2268
|
+
if (edge(before) || edge(next)) return false;
|
|
2269
|
+
return next.x - prev.x >= prev.fontSize * INDENT_EM || roomForWord(prev, next, column.right);
|
|
2270
|
+
}
|
|
2271
|
+
/** How many lines a column needs to show it is set against both edges. */
|
|
2272
|
+
var JUSTIFIED_COLUMN_LINES = 8;
|
|
2273
|
+
/** …and how many of them run to the measure, at the least. */
|
|
2274
|
+
var JUSTIFIED_COLUMN_SHARE = .6;
|
|
2275
|
+
/** How far short of the measure a line of it may end and still run to it: the page's rounding. */
|
|
2276
|
+
var JUSTIFIED_SLACK_PT = 1;
|
|
2277
|
+
/** A word space, in ems: what stands between a line's end and the word put after it. */
|
|
2278
|
+
var WORD_SPACE_EM = .3;
|
|
2279
|
+
/**
|
|
2280
|
+
* The exact box a paragraph's first line stands in, where the paragraph is a
|
|
2281
|
+
* line of text set to one (see {@link groupIntoParagraphs}).
|
|
2282
|
+
*/
|
|
2283
|
+
function leadingLine(el) {
|
|
2284
|
+
if (el.kind === "shape" && el.shape.float === void 0) return 0;
|
|
2285
|
+
if (el.kind !== "paragraph" || el.paragraph.runs.length === 0) return void 0;
|
|
2286
|
+
const { spacingLine, spacingLineRule } = el.paragraph.properties;
|
|
2287
|
+
return spacingLineRule === "exact" && spacingLine !== void 0 && spacingLine > 0 ? spacingLine : void 0;
|
|
2288
|
+
}
|
|
2289
|
+
/** The tallest box a line is given, in ems — past it the gap is paragraph spacing. */
|
|
2290
|
+
var TALLEST_LINE_EM = 3;
|
|
2291
|
+
/** Spacing under this is the rounding of the page's own numbers, not white put there. */
|
|
2292
|
+
var SPACING_NOISE_PT = .25;
|
|
2293
|
+
/**
|
|
1068
2294
|
* A paragraph's lines as one run of spans, joined the way the page broke them.
|
|
1069
2295
|
*
|
|
1070
2296
|
* A line that ends in a hyphen was broken THERE, and the break is not part of
|
|
@@ -1077,6 +2303,11 @@ function groupIntoParagraphs(lines, column, pageHeight = 0) {
|
|
|
1077
2303
|
* "typical two-column docu ment incorporating tables, figures and mathemat
|
|
1078
2304
|
* ics" — the soft hyphens dropped by the page and a space in their place.
|
|
1079
2305
|
*
|
|
2306
|
+
* A dash closed up to the word before it binds that word to the one after,
|
|
2307
|
+
* as the hyphen does: freeculture.pdf's index breaks "167–68" after its dash,
|
|
2308
|
+
* and the entry came back "167– 68". A dash with a space before it stands
|
|
2309
|
+
* apart, and the break is a space.
|
|
2310
|
+
*
|
|
1080
2311
|
* @param lines The paragraph's lines, in order.
|
|
1081
2312
|
*/
|
|
1082
2313
|
function joinLines(lines) {
|
|
@@ -1086,12 +2317,13 @@ function joinLines(lines) {
|
|
|
1086
2317
|
const prev = out[out.length - 1];
|
|
1087
2318
|
const ends = prev?.text ?? "";
|
|
1088
2319
|
const soft = ends.endsWith(SOFT_HYPHEN);
|
|
1089
|
-
const
|
|
2320
|
+
const before = ends.length > 1 ? ends.slice(-2, -1) : out[out.length - 2]?.text.slice(-1) ?? "";
|
|
2321
|
+
const hard = HYPHENS.has(ends.slice(-1)) || DASHES.has(ends.slice(-1)) && /\S/u.test(before);
|
|
1090
2322
|
if (soft && prev) out[out.length - 1] = {
|
|
1091
2323
|
...prev,
|
|
1092
2324
|
text: ends.slice(0, -1)
|
|
1093
2325
|
};
|
|
1094
|
-
else if (!hard)
|
|
2326
|
+
else if (!hard && !/\s$/u.test(ends) && !/^\s/u.test(line.spans[0]?.text ?? "")) out.push(spaceAfter(prev, line.spans[0]));
|
|
1095
2327
|
}
|
|
1096
2328
|
out.push(...line.spans);
|
|
1097
2329
|
});
|
|
@@ -1105,11 +2337,36 @@ var HYPHENS = new Set([
|
|
|
1105
2337
|
"‐",
|
|
1106
2338
|
"‑"
|
|
1107
2339
|
]);
|
|
2340
|
+
/** The dashes, en and em, that belong to the words either side where closed up to them. */
|
|
2341
|
+
var DASHES = new Set(["–", "—"]);
|
|
1108
2342
|
/** How many lines' worth of indent still reads as a first line, not a placement. */
|
|
1109
2343
|
var INDENT_LINES = 3;
|
|
1110
2344
|
/** Where in the run of line starts the column's own left edge is looked for. */
|
|
1111
2345
|
var COLUMN_LEFT_QUANTILE = .15;
|
|
1112
2346
|
/**
|
|
2347
|
+
* Whether a line opens a list item: it begins with a bullet set apart from the
|
|
2348
|
+
* words it marks, or with a label — "[19]", "3." — standing out left of the
|
|
2349
|
+
* line over it. Where the item before it ends on a line that runs to the
|
|
2350
|
+
* measure, or nearly, nothing else says it ended: comments.pdf's "• We explain
|
|
2351
|
+
* how to speculatively generate…" came back run into the item over it, its
|
|
2352
|
+
* bullet in the middle of a line, and its reference "[19] M. Zaleski…" into
|
|
2353
|
+
* "[18]". A label that does not stand out is a line of prose that begins
|
|
2354
|
+
* with one — a citation, a figure.
|
|
2355
|
+
*
|
|
2356
|
+
* @param prev The line over it.
|
|
2357
|
+
* @param line A line of the column.
|
|
2358
|
+
*/
|
|
2359
|
+
function opensItem(prev, line) {
|
|
2360
|
+
if (BULLET_LEAD.test(line.text)) return true;
|
|
2361
|
+
return LABEL_LEAD.test(line.text) && prev.x - line.x >= line.fontSize * HANGING_EM;
|
|
2362
|
+
}
|
|
2363
|
+
/** A bullet and the white after it, at the head of a line. */
|
|
2364
|
+
var BULLET_LEAD = /^[\u2022\u2023\u2043\u2219\u25aa\u25ab\u25cb\u25cf\u25e6\u25a0\u25a1]\s/u;
|
|
2365
|
+
/** A reference's or a step's label, "[19]" or "3.", and the white after it. */
|
|
2366
|
+
var LABEL_LEAD = /^(?:\[\d{1,4}\]|\d{1,3}\.)\s/u;
|
|
2367
|
+
/** How far, in ems, a label stands out left of the line over it to hang. */
|
|
2368
|
+
var HANGING_EM = 1;
|
|
2369
|
+
/**
|
|
1113
2370
|
* §17.3.1.12 `w:ind` — how far a paragraph is set in from its column, and where
|
|
1114
2371
|
* its first line begins.
|
|
1115
2372
|
*
|
|
@@ -1145,6 +2402,69 @@ function indentOf(lines, columnLeft, alignment) {
|
|
|
1145
2402
|
};
|
|
1146
2403
|
}
|
|
1147
2404
|
/**
|
|
2405
|
+
* §17.3.1.12 — how far a line the page set in ONE piece may run on into the
|
|
2406
|
+
* right margin, rather than wrap.
|
|
2407
|
+
*
|
|
2408
|
+
* The line is re-set in a face this reader has, and a face wider than the
|
|
2409
|
+
* page's pushes the last word of a line that ran nearly to the measure onto a
|
|
2410
|
+
* line of its own: bug1252420.pdf's one italic line came back with
|
|
2411
|
+
* "Centuries" under it, and on bug1108301.pdf's sheet, fifty points tall, the
|
|
2412
|
+
* word it pushed down fell off the paper altogether. A line the page did not
|
|
2413
|
+
* break is not broken: it may run on — so far as a sixth of its measure, and
|
|
2414
|
+
* never past the edge of the sheet. Only a line set flush, with nothing on
|
|
2415
|
+
* stops: a centred one would move its centre, and a tabbed one its stops.
|
|
2416
|
+
*
|
|
2417
|
+
* @param para The paragraph.
|
|
2418
|
+
* @param measure The column it was set in.
|
|
2419
|
+
* @param pageWidth The sheet's width.
|
|
2420
|
+
* @returns How far it may run past the measure, in points; 0 where it keeps to it.
|
|
2421
|
+
*/
|
|
2422
|
+
function runOn(para, measure, pageWidth) {
|
|
2423
|
+
if (measure === void 0 || para.lineCount !== 1 || para.alignment !== void 0) return 0;
|
|
2424
|
+
if ((para.stops?.length ?? 0) > 0) return 0;
|
|
2425
|
+
const width = measure.right - measure.left;
|
|
2426
|
+
if (!(width > 0) || para.right - measure.left < width * FULL_LINE_SHARE) return 0;
|
|
2427
|
+
return Math.max(0, Math.min(width * RUN_ON_SHARE, pageWidth - measure.right));
|
|
2428
|
+
}
|
|
2429
|
+
/**
|
|
2430
|
+
* §17.3.1.33 — the last text of a sheet, set farther below everything above it
|
|
2431
|
+
* than a paragraph's spacing may say, stands where the page set it.
|
|
2432
|
+
*
|
|
2433
|
+
* The spacing before a paragraph is held to a third of the sheet, so that a
|
|
2434
|
+
* gap misread cannot throw everything after it off the page. The last text on
|
|
2435
|
+
* the sheet has nothing after it to throw, and held to a third it came back
|
|
2436
|
+
* half way up the paper: bug1989304.pdf signs its sheet "World" at the foot
|
|
2437
|
+
* and we set it in the middle. Its lines are placed instead, each where the
|
|
2438
|
+
* page drew it.
|
|
2439
|
+
*
|
|
2440
|
+
* @param blocks The sheet's blocks, in the order they are read.
|
|
2441
|
+
* @param far The paragraphs whose spacing was held, with their lines.
|
|
2442
|
+
* @param place A line as the box that places it, `k` counting them.
|
|
2443
|
+
*/
|
|
2444
|
+
function setOff(blocks, far, place) {
|
|
2445
|
+
const after = (a, b) => (a.band - b.band || columnOrder(a.col) - columnOrder(b.col) || b.top - a.top) > 0;
|
|
2446
|
+
let at = -1;
|
|
2447
|
+
blocks.forEach((b, k) => {
|
|
2448
|
+
if (b.el.kind !== "paragraph" && b.el.kind !== "table") return;
|
|
2449
|
+
if (at < 0 || after(b, blocks[at])) at = k;
|
|
2450
|
+
});
|
|
2451
|
+
const block = at >= 0 ? blocks[at] : void 0;
|
|
2452
|
+
const lines = block ? far.get(block.el) : void 0;
|
|
2453
|
+
if (!block || !lines) return;
|
|
2454
|
+
blocks.splice(at, 1, ...lines.map((line, k) => ({
|
|
2455
|
+
band: block.band,
|
|
2456
|
+
col: block.col,
|
|
2457
|
+
top: line.y,
|
|
2458
|
+
el: place(line, k)
|
|
2459
|
+
})));
|
|
2460
|
+
}
|
|
2461
|
+
/** Where the placed last text stands in the page's painting order: over it all. */
|
|
2462
|
+
var FAR_Z = 1e6;
|
|
2463
|
+
/** How far across its measure a line runs before it is at risk of wrapping when re-set. */
|
|
2464
|
+
var FULL_LINE_SHARE = .85;
|
|
2465
|
+
/** How far past its measure, as a share of it, a one-piece line may run on. */
|
|
2466
|
+
var RUN_ON_SHARE = 1 / 6;
|
|
2467
|
+
/**
|
|
1148
2468
|
* How far, in ems, a paragraph has to be set in before it is indented rather
|
|
1149
2469
|
* than merely started. Half an em clears the rounding a producer leaves at the
|
|
1150
2470
|
* head of a line and is well under the smallest indent anybody sets.
|
|
@@ -1165,8 +2485,14 @@ var INDENT_EM = .5;
|
|
|
1165
2485
|
* placed rather than set — a centred title's every line is short of the measure
|
|
1166
2486
|
* and none of them ends anything.
|
|
1167
2487
|
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
2488
|
+
* A line of a right-to-left script starts at the RIGHT and ends at the left,
|
|
2489
|
+
* and is read the other way round: ArabicCIDTrueType.pdf sets four lines, each
|
|
2490
|
+
* flush right and shorter than the one before, and read as a left-to-right
|
|
2491
|
+
* setting every pair of them ran together as one paragraph.
|
|
2492
|
+
*
|
|
2493
|
+
* @param prev The line before: where it starts, how wide it is, its face,
|
|
2494
|
+
* and its text when it has any.
|
|
2495
|
+
* @param next The line after — where it starts, and how wide it is.
|
|
1170
2496
|
* @param column The measure both were set in, when it is known.
|
|
1171
2497
|
* @returns Whether the first line ended a paragraph.
|
|
1172
2498
|
*/
|
|
@@ -1175,6 +2501,11 @@ function endedParagraph(prev, next, column) {
|
|
|
1175
2501
|
const width = column.right - column.left;
|
|
1176
2502
|
if (!(width > 0)) return false;
|
|
1177
2503
|
const step = Math.max(prev.fontSize, 4);
|
|
2504
|
+
if (prev.text !== void 0 && next.width !== void 0 && isRightToLeft(prev.text)) {
|
|
2505
|
+
const indent = prev.x + prev.width - (next.x + next.width);
|
|
2506
|
+
if (indent < -step || indent > step * INDENT_LINES) return false;
|
|
2507
|
+
return prev.x - column.left > width * .25;
|
|
2508
|
+
}
|
|
1178
2509
|
const shift = next.x - prev.x;
|
|
1179
2510
|
if (shift < -step || shift > step * INDENT_LINES) return false;
|
|
1180
2511
|
return column.right - (prev.x + prev.width) > width * .25;
|
|
@@ -1242,12 +2573,13 @@ function compareOrder(a, b) {
|
|
|
1242
2573
|
* `undefined` where the document repeats nothing.
|
|
1243
2574
|
*/
|
|
1244
2575
|
function runningFoot(pageRuns, shown, where) {
|
|
1245
|
-
const
|
|
2576
|
+
const textSize = textSizeOf(pageRuns);
|
|
2577
|
+
const feet = pageRuns.map((runs, i) => edgeLine(runs.filter((r) => !tooSmallToRead(r, textSize)), shown[i]?.height ?? 0, where));
|
|
1246
2578
|
const found = feet.filter((f) => f !== void 0);
|
|
1247
2579
|
if (found.length < 2 || found.length < pageRuns.length * FOOT_SHARE) return void 0;
|
|
1248
|
-
const
|
|
1249
|
-
const mid = median(
|
|
1250
|
-
if (
|
|
2580
|
+
const inner = (f) => where === "foot" ? Math.max(...f.runs.map((r) => r.y)) : Math.min(...f.runs.map((r) => r.y));
|
|
2581
|
+
const mid = median(found.map(inner));
|
|
2582
|
+
if (found.some((f) => (where === "foot" ? inner(f) - mid : mid - inner(f)) > FOOT_DRIFT)) return;
|
|
1251
2583
|
const texts = found.map((f) => f.runs.map((r) => r.text).join("").trim());
|
|
1252
2584
|
return {
|
|
1253
2585
|
lift: feet.map((f) => new Set(f?.runs ?? [])),
|
|
@@ -1255,7 +2587,107 @@ function runningFoot(pageRuns, shown, where) {
|
|
|
1255
2587
|
numbered: new Set(texts).size > 1
|
|
1256
2588
|
};
|
|
1257
2589
|
}
|
|
2590
|
+
/**
|
|
2591
|
+
* §17.10.1, §17.15.1.36 — the running feet of a book's two sides, where they
|
|
2592
|
+
* differ. A book signs its left-hand pages one way and its right-hand ones
|
|
2593
|
+
* another, its folio at the outer edge of each: freeculture.pdf sets "6 FREE
|
|
2594
|
+
* CULTURE" at the left of its even pages with the URL it is published at
|
|
2595
|
+
* under it, and "INTRODUCTION 5" at the right of its odd ones. Read as one
|
|
2596
|
+
* foot, every page was signed the way its first was.
|
|
2597
|
+
*
|
|
2598
|
+
* The side is the page number's, as Word takes it, printing the even foot on
|
|
2599
|
+
* the pages it numbers even; the sides differ where their numbers stand on
|
|
2600
|
+
* opposite halves of the page. A side's foot says what most of that side's
|
|
2601
|
+
* feet say, and its number: the chapter a page is in is not named alike in
|
|
2602
|
+
* any two chapters, and set once it would name every page after the first.
|
|
2603
|
+
*
|
|
2604
|
+
* @param foot The running foot, a page's runs to each page.
|
|
2605
|
+
* @param numbering The numbers its pages print.
|
|
2606
|
+
* @param middle The middle of each page, across.
|
|
2607
|
+
* @returns Each side's runs and the numeral they print, or `undefined` where
|
|
2608
|
+
* the two sides are signed alike.
|
|
2609
|
+
*/
|
|
2610
|
+
function sidesOf(foot, numbering, middle) {
|
|
2611
|
+
const bySide = [[], []];
|
|
2612
|
+
foot.lift.forEach((set, i) => {
|
|
2613
|
+
const number = numbering.numbers[i];
|
|
2614
|
+
if (set.size === 0 || number === void 0) return;
|
|
2615
|
+
const runs = [...set];
|
|
2616
|
+
const numeral = runs.find((r) => r.text.trim() === number.text);
|
|
2617
|
+
if (numeral === void 0) return;
|
|
2618
|
+
bySide[number.value % 2].push({
|
|
2619
|
+
runs,
|
|
2620
|
+
numeral,
|
|
2621
|
+
left: (numeral.x + numeral.endX) / 2 < (middle[i] ?? 0),
|
|
2622
|
+
page: i
|
|
2623
|
+
});
|
|
2624
|
+
});
|
|
2625
|
+
const [even, odd] = bySide;
|
|
2626
|
+
if (even.length < LEAST_SIDE_FEET || odd.length < LEAST_SIDE_FEET) return void 0;
|
|
2627
|
+
const leftOf = (side) => side.filter((f) => f.left).length * 2 > side.length;
|
|
2628
|
+
if (leftOf(even) === leftOf(odd)) return void 0;
|
|
2629
|
+
const wordsOf = (runs) => runs.flatMap((r) => r.text.split(/\s+/u)).filter((w) => w !== "");
|
|
2630
|
+
const sideOf = (side) => {
|
|
2631
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2632
|
+
for (const f of side) for (const w of new Set(wordsOf(f.runs.filter((r) => r !== f.numeral)))) counts.set(w, (counts.get(w) ?? 0) + 1);
|
|
2633
|
+
const said = (r) => wordsOf([r]).every((w) => (counts.get(w) ?? 0) >= side.length * SIDE_SHARE);
|
|
2634
|
+
const kept = (f) => f.runs.filter((r) => r === f.numeral || said(r));
|
|
2635
|
+
const best = side.filter((f) => f.left === leftOf(side)).reduce((a, b) => kept(b).length > kept(a).length ? b : a);
|
|
2636
|
+
return {
|
|
2637
|
+
runs: kept(best),
|
|
2638
|
+
numeral: best.numeral.text.trim(),
|
|
2639
|
+
page: best.page
|
|
2640
|
+
};
|
|
2641
|
+
};
|
|
2642
|
+
return {
|
|
2643
|
+
even: sideOf(even),
|
|
2644
|
+
odd: sideOf(odd)
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
/** How many feet each side must show before a book is taken to sign its sides apart. */
|
|
2648
|
+
var LEAST_SIDE_FEET = 2;
|
|
2649
|
+
/** How many of a side's feet, as a share of them, must say a word before its foot does. */
|
|
2650
|
+
var SIDE_SHARE = .6;
|
|
1258
2651
|
/** How many of a document's pages must carry the foot before it is running. */
|
|
2652
|
+
/**
|
|
2653
|
+
* §17.6.12 — how a document's pages are numbered, read off the band that
|
|
2654
|
+
* prints their numbers (see ./page-numbers).
|
|
2655
|
+
*
|
|
2656
|
+
* @param band The running head or foot whose numbers change from page to page.
|
|
2657
|
+
* @returns The numbering, or undefined where no band numbers the pages.
|
|
2658
|
+
*/
|
|
2659
|
+
function numberingOf(band) {
|
|
2660
|
+
if (!band) return void 0;
|
|
2661
|
+
return pageNumberingOf(band.lift.map((set) => set.size > 0 ? bandText([...set]) : void 0));
|
|
2662
|
+
}
|
|
2663
|
+
/** A band's text on one page, with a word space wherever the page shows one. */
|
|
2664
|
+
function bandText(runs) {
|
|
2665
|
+
return groupIntoLines(runs, false, stepsBetweenWords(runs)).map((line) => line.text).join(" ");
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* The numbering a section opening at `page` states: the numerals of the
|
|
2669
|
+
* sequence the page is counted in, and the number the sequence starts at where
|
|
2670
|
+
* it starts there.
|
|
2671
|
+
*
|
|
2672
|
+
* @param numbering The document's numbering.
|
|
2673
|
+
* @param page The section's first page.
|
|
2674
|
+
* @returns The section's `pageNumberFormat` and `pageNumberStart`, as needed.
|
|
2675
|
+
*/
|
|
2676
|
+
function numberedFrom(numbering, page) {
|
|
2677
|
+
const run = numbering ? runOf(numbering.runs, page) : void 0;
|
|
2678
|
+
if (!run) return {};
|
|
2679
|
+
return {
|
|
2680
|
+
...run.format !== "decimal" ? { pageNumberFormat: run.format } : {},
|
|
2681
|
+
...run.from === page && (page > 0 || run.start !== 1) ? { pageNumberStart: run.start } : {}
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
/** A document of one section, numbered from its first page. */
|
|
2685
|
+
function numberedAs(section, numbering) {
|
|
2686
|
+
return section ? {
|
|
2687
|
+
...section,
|
|
2688
|
+
...numberedFrom(numbering, 0)
|
|
2689
|
+
} : section;
|
|
2690
|
+
}
|
|
1259
2691
|
var FOOT_SHARE = .6;
|
|
1260
2692
|
/** How far, in points, a running foot may drift from page to page. */
|
|
1261
2693
|
var FOOT_DRIFT = 4;
|
|
@@ -1273,7 +2705,8 @@ var FOOT_DRIFT = 4;
|
|
|
1273
2705
|
* its own, and a two-page document came out as four.
|
|
1274
2706
|
*/
|
|
1275
2707
|
function edgeLine(runs, pageHeight, where) {
|
|
1276
|
-
|
|
2708
|
+
const head = where === "head";
|
|
2709
|
+
if (runs.length < (head ? HEAD_RUNS : 1) || pageHeight <= 0) return void 0;
|
|
1277
2710
|
const fontSize = median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
|
|
1278
2711
|
const rows = rowsOf(runs, fontSize);
|
|
1279
2712
|
if (rows.length === 0) return void 0;
|
|
@@ -1281,9 +2714,9 @@ function edgeLine(runs, pageHeight, where) {
|
|
|
1281
2714
|
const edgeY = (row) => where === "foot" ? Math.max(...row.map((r) => r.y)) : Math.min(...row.map((r) => r.y));
|
|
1282
2715
|
const inside = (row) => where === "foot" ? Math.min(...row.map((r) => r.y)) : Math.max(...row.map((r) => r.y));
|
|
1283
2716
|
const y = edgeY(at(0));
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
2717
|
+
const inked = rows.filter((row) => row.some((r) => r.text.trim() !== ""));
|
|
2718
|
+
const inBand = inked.length > 0 && inked.every((row) => where === "foot" ? edgeY(row) <= pageHeight * FOOT_BAND : edgeY(row) >= pageHeight * (1 - FOOT_BAND));
|
|
2719
|
+
if (rows.length < 4 && (inBand || head)) {
|
|
1287
2720
|
const all = inked.flat();
|
|
1288
2721
|
const text = all.map((r) => r.text).join("").trim();
|
|
1289
2722
|
return inBand && text.length > 0 && text.length <= FOOT_CHARS * inked.length ? {
|
|
@@ -1293,8 +2726,9 @@ function edgeLine(runs, pageHeight, where) {
|
|
|
1293
2726
|
}
|
|
1294
2727
|
let found;
|
|
1295
2728
|
let group = [];
|
|
1296
|
-
const most =
|
|
1297
|
-
|
|
2729
|
+
const most = head ? 1 : FOOT_ROWS;
|
|
2730
|
+
const above = head ? 2 : 1;
|
|
2731
|
+
for (let i = 0; i < Math.min(most, rows.length - above); i++) {
|
|
1298
2732
|
const row = at(i);
|
|
1299
2733
|
const edge = edgeY(row);
|
|
1300
2734
|
if (where === "foot" ? edge > pageHeight * FOOT_BAND : edge < pageHeight * (1 - FOOT_BAND)) break;
|
|
@@ -1313,6 +2747,8 @@ function edgeLine(runs, pageHeight, where) {
|
|
|
1313
2747
|
var FOOT_GAP_EM = 2;
|
|
1314
2748
|
/** How many lines a running foot may hold before it is a paragraph. */
|
|
1315
2749
|
var FOOT_ROWS = 3;
|
|
2750
|
+
/** How many runs a page must hold before its first line is asked whether it is a running head. */
|
|
2751
|
+
var HEAD_RUNS = 4;
|
|
1316
2752
|
/** How far up the sheet a running foot may sit. */
|
|
1317
2753
|
var FOOT_BAND = .12;
|
|
1318
2754
|
/** A running foot is a line, not a paragraph. */
|
|
@@ -1324,11 +2760,12 @@ var FOOT_CHARS = 90;
|
|
|
1324
2760
|
* the number of the page it is drawn on, which is what lets ONE band serve
|
|
1325
2761
|
* every page. The run is cut around it and the middle becomes the field.
|
|
1326
2762
|
*
|
|
1327
|
-
* @param run
|
|
2763
|
+
* @param run The footer's run.
|
|
2764
|
+
* @param numeral The page's number as this band prints it, where it is known.
|
|
1328
2765
|
* @returns The run, or the two or three it is cut into.
|
|
1329
2766
|
*/
|
|
1330
|
-
function pageNumbered(run) {
|
|
1331
|
-
const found = /(^|\s)(\d{1,4})(\s|$)/u.exec(run.text);
|
|
2767
|
+
function pageNumbered(run, numeral) {
|
|
2768
|
+
const found = (numeral !== void 0 ? new RegExp(`(^|\\s)(${numeral})(\\s|$)`, "u") : /(^|\s)(\d{1,4})(\s|$)/u).exec(run.text);
|
|
1332
2769
|
if (!found || run.field !== void 0) return [run];
|
|
1333
2770
|
const at = found.index + found[1].length;
|
|
1334
2771
|
const number = found[2];
|
|
@@ -1384,8 +2821,9 @@ function pageNumbered(run) {
|
|
|
1384
2821
|
* @param stepped Whether the page steps between its words.
|
|
1385
2822
|
* @param measure The measure it was set across, for its alignment.
|
|
1386
2823
|
* @param numbered Whether a number in it is the page's own.
|
|
2824
|
+
* @param numeral That number as this band prints it, where it is known.
|
|
1387
2825
|
*/
|
|
1388
|
-
function footerBand(runs, stepped, measure, numbered) {
|
|
2826
|
+
function footerBand(runs, stepped, measure, numbered, numeral) {
|
|
1389
2827
|
const fontSize = median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
|
|
1390
2828
|
return rowsOf(runs, fontSize).map((row) => bandLine(row, fontSize, stepped, measure)).filter((line) => line !== void 0).map(({ spans, properties }) => {
|
|
1391
2829
|
const el = paragraphFromRuns(spans, void 0, properties);
|
|
@@ -1394,7 +2832,7 @@ function footerBand(runs, stepped, measure, numbered) {
|
|
|
1394
2832
|
kind: "paragraph",
|
|
1395
2833
|
paragraph: {
|
|
1396
2834
|
...el.paragraph,
|
|
1397
|
-
runs: numbered ? el.paragraph.runs.flatMap((run) => pageNumbered(run)) : el.paragraph.runs
|
|
2835
|
+
runs: numbered ? el.paragraph.runs.flatMap((run) => pageNumbered(run, numeral)) : el.paragraph.runs
|
|
1398
2836
|
}
|
|
1399
2837
|
};
|
|
1400
2838
|
});
|
|
@@ -1426,9 +2864,11 @@ function bandLine(row, fontSize, stepped, measure) {
|
|
|
1426
2864
|
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
2865
|
if (lines.length === 0) return void 0;
|
|
1428
2866
|
if (lines.length === 1 || lines.length > BAND_REGIONS || !measure) {
|
|
1429
|
-
const
|
|
2867
|
+
const only = lines.length === 1 ? lines[0] : void 0;
|
|
2868
|
+
const width = measure ? measure.right - measure.left : 0;
|
|
2869
|
+
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
2870
|
return {
|
|
1431
|
-
spans: lines.flatMap((line, i) => i === 0 ? line.spans : [
|
|
2871
|
+
spans: lines.flatMap((line, i) => i === 0 ? line.spans : [spaceAfter(lines[i - 1].spans.at(-1), line.spans[0]), ...line.spans]),
|
|
1432
2872
|
properties: alignment ? { alignment } : {}
|
|
1433
2873
|
};
|
|
1434
2874
|
}
|
|
@@ -1550,9 +2990,11 @@ function proseColumns(runs, gutters, edges) {
|
|
|
1550
2990
|
for (const x of xs) most = Math.max(most, xs.filter((y) => Math.abs(y - x) <= FLUSH_PT).length);
|
|
1551
2991
|
return most;
|
|
1552
2992
|
};
|
|
1553
|
-
return edgesIn.every((lines) => {
|
|
2993
|
+
return edgesIn.every((lines, i) => {
|
|
1554
2994
|
if (lines.length < MIN_PROSE_LINES) return false;
|
|
1555
|
-
|
|
2995
|
+
const [lo, hi] = regions[i];
|
|
2996
|
+
const full = lines.filter((l) => l.right - l.left >= (hi - lo) * FULL_LINE_SHARE).length;
|
|
2997
|
+
return agreeing(lines.map((l) => l.right)) <= agreeing(lines.map((l) => l.left)) || full >= lines.length * FILLED_SHARE;
|
|
1556
2998
|
});
|
|
1557
2999
|
}
|
|
1558
3000
|
/**
|
|
@@ -1586,8 +3028,8 @@ function strayMarks(vectors, runs) {
|
|
|
1586
3028
|
const readable = runs.filter((r) => r.text.trim() !== "" && !r.text.includes("�"));
|
|
1587
3029
|
for (const v of glyphs) {
|
|
1588
3030
|
const mid = (v.minY + v.maxY) / 2;
|
|
1589
|
-
const
|
|
1590
|
-
const
|
|
3031
|
+
const words = readable.filter((r) => Math.abs(r.y - mid) <= (r.fontSizePt || 10));
|
|
3032
|
+
const size = words.length > 0 ? median(words.map((r) => r.fontSizePt || 10)) : v.maxY - v.minY || 10;
|
|
1591
3033
|
const before = words.some((r) => r.endX <= v.minX + size);
|
|
1592
3034
|
const after = words.some((r) => r.x >= v.maxX - size);
|
|
1593
3035
|
if (!before && !after) continue;
|
|
@@ -1595,6 +3037,68 @@ function strayMarks(vectors, runs) {
|
|
|
1595
3037
|
}
|
|
1596
3038
|
return out;
|
|
1597
3039
|
}
|
|
3040
|
+
/**
|
|
3041
|
+
* §9.10.2 — the runs of a line with each glyph the file names no character for
|
|
3042
|
+
* read off the shape it draws (see {@link punctuationOf}), where it is one of
|
|
3043
|
+
* the stray marks inside a readable line.
|
|
3044
|
+
*
|
|
3045
|
+
* @param runs The page's runs.
|
|
3046
|
+
* @param strays The drawn glyphs that stand inside lines of readable text.
|
|
3047
|
+
* @returns The runs, with every mark that could be read written as text.
|
|
3048
|
+
*/
|
|
3049
|
+
function readDrawnMarks(runs, strays) {
|
|
3050
|
+
if (strays.size === 0) return [...runs];
|
|
3051
|
+
const marks = [...strays];
|
|
3052
|
+
return runs.map((run) => {
|
|
3053
|
+
if (!run.text.includes("�") || run.text.replaceAll("�", "").trim() !== "") return run;
|
|
3054
|
+
const size = run.fontSizePt || 10;
|
|
3055
|
+
const mark = marks.find((v) => {
|
|
3056
|
+
const x = (v.minX + v.maxX) / 2;
|
|
3057
|
+
return x >= run.x - .5 && x <= run.endX + .5 && Math.abs((v.minY + v.maxY) / 2 - run.y) <= size;
|
|
3058
|
+
});
|
|
3059
|
+
const read = mark ? punctuationOf(mark, run.y, size) : void 0;
|
|
3060
|
+
return read === void 0 ? run : {
|
|
3061
|
+
...run,
|
|
3062
|
+
text: run.text.replaceAll("�", read)
|
|
3063
|
+
};
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
/**
|
|
3067
|
+
* §9.6.6 — the glyphs a page draws for want of characters, gathered into the
|
|
3068
|
+
* WORDS they stand in: a traced glyph that stands against the one before it,
|
|
3069
|
+
* on its line and in its colour, is drawn as one shape with it.
|
|
3070
|
+
*
|
|
3071
|
+
* Every mark kept apart is a shape of its own in the document, anchored to
|
|
3072
|
+
* the page — and a paper set in a subset whose glyphs name nothing is nothing
|
|
3073
|
+
* but such marks: TAMReview.pdf came back as forty-two thousand shapes, a
|
|
3074
|
+
* package no reader would open in under three minutes. The picture is the
|
|
3075
|
+
* same either way.
|
|
3076
|
+
*
|
|
3077
|
+
* @param vectors The page's painted paths, in painting order.
|
|
3078
|
+
* @returns The same paths, each run of traced glyphs made one.
|
|
3079
|
+
*/
|
|
3080
|
+
function drawnWords(vectors) {
|
|
3081
|
+
const out = [];
|
|
3082
|
+
for (const v of vectors) {
|
|
3083
|
+
const last = out[out.length - 1];
|
|
3084
|
+
const size = v.maxY - v.minY;
|
|
3085
|
+
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) {
|
|
3086
|
+
out[out.length - 1] = {
|
|
3087
|
+
...last,
|
|
3088
|
+
segs: [...last.segs, ...v.segs],
|
|
3089
|
+
minX: Math.min(last.minX, v.minX),
|
|
3090
|
+
minY: Math.min(last.minY, v.minY),
|
|
3091
|
+
maxX: Math.max(last.maxX, v.maxX),
|
|
3092
|
+
maxY: Math.max(last.maxY, v.maxY)
|
|
3093
|
+
};
|
|
3094
|
+
continue;
|
|
3095
|
+
}
|
|
3096
|
+
out.push(v);
|
|
3097
|
+
}
|
|
3098
|
+
return out;
|
|
3099
|
+
}
|
|
3100
|
+
/** How far apart, in heights of the taller glyph, two drawn glyphs may stand and be one word. */
|
|
3101
|
+
var WORD_GAP_EM = .6;
|
|
1598
3102
|
/** How few drawn glyphs a line may hold before they are its text, not marks in it. */
|
|
1599
3103
|
var MOST_STRAY_MARKS = 2;
|
|
1600
3104
|
/** …and how many readable runs must stand on that line for them to be strays. */
|
|
@@ -1639,41 +3143,173 @@ function tabbedRows(paras, measure) {
|
|
|
1639
3143
|
const rows = paras.slice(i, to);
|
|
1640
3144
|
const bounds = stops.map((_, k) => Math.min(...rows.map((r) => r.stops[k])));
|
|
1641
3145
|
const width = measure.right - measure.left;
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
3146
|
+
const inks = [0, ...bounds].map((_, k) => {
|
|
3147
|
+
const cells = rows.map((r) => r.pieces?.[k]);
|
|
3148
|
+
if (cells.some((c) => c === void 0 || !Number.isFinite(c.from))) return void 0;
|
|
3149
|
+
return {
|
|
3150
|
+
from: Math.min(...cells.map((c) => c.from)),
|
|
3151
|
+
to: Math.max(...cells.map((c) => c.to))
|
|
3152
|
+
};
|
|
3153
|
+
});
|
|
3154
|
+
const above = i > 0 && !out.has(i - 1) ? paras[i - 1] : void 0;
|
|
3155
|
+
const close = above !== void 0 && above.top - rows[0].top <= rows[0].lineHeight * HEADING_LINES;
|
|
3156
|
+
const columnsFor = (heading) => {
|
|
3157
|
+
const flush = inks.map((ink, k) => {
|
|
3158
|
+
if (k === 0 || ink === void 0) return false;
|
|
3159
|
+
const tos = rows.map((r) => r.pieces[k].to);
|
|
3160
|
+
const froms = rows.map((r) => r.pieces[k].from);
|
|
3161
|
+
const even = Math.max(...tos) - Math.min(...tos);
|
|
3162
|
+
if (even > FLUSH_SLACK_PT) return false;
|
|
3163
|
+
if (Math.max(...froms) - Math.min(...froms) > even + FLUSH_SLACK_PT) return true;
|
|
3164
|
+
const head = heading?.over[k];
|
|
3165
|
+
return head !== void 0 && Math.abs(head.to - Math.max(...tos)) <= FLUSH_SLACK_PT && Math.min(...froms) - head.from > FLUSH_SLACK_PT;
|
|
3166
|
+
});
|
|
3167
|
+
const holds = inks.map((ink, k) => {
|
|
3168
|
+
const head = heading?.over[k];
|
|
3169
|
+
return ink !== void 0 && head !== void 0 ? {
|
|
3170
|
+
from: Math.min(ink.from, head.from),
|
|
3171
|
+
to: Math.max(ink.to, head.to)
|
|
3172
|
+
} : ink;
|
|
3173
|
+
});
|
|
3174
|
+
const edges = [
|
|
3175
|
+
0,
|
|
3176
|
+
...bounds,
|
|
3177
|
+
width
|
|
3178
|
+
];
|
|
3179
|
+
for (let k = 1; k < bounds.length + 1; k++) {
|
|
3180
|
+
const before = holds[k - 1];
|
|
3181
|
+
const own = holds[k];
|
|
3182
|
+
if (!flush[k] || before === void 0 || own === void 0) continue;
|
|
3183
|
+
const mid = (before.to + own.from) / 2;
|
|
3184
|
+
if (mid > edges[k - 1] && mid < own.from) edges[k] = mid;
|
|
1670
3185
|
}
|
|
3186
|
+
const lead = holds[0]?.from ?? 0;
|
|
3187
|
+
if (lead > FLUSH_SLACK_PT && lead < edges[1]) edges[0] = lead;
|
|
3188
|
+
const flushRight = flush.map((right, k) => right ? Math.max(0, edges[k + 1] - inks[k].to) : void 0);
|
|
3189
|
+
return {
|
|
3190
|
+
holds,
|
|
3191
|
+
edges,
|
|
3192
|
+
flushRight,
|
|
3193
|
+
held: heading?.over.every((head, k) => head === void 0 || edges[k + 1] - edges[k] - (flushRight[k] ?? 0) >= head.to - head.from - FLUSH_SLACK_PT) ?? true
|
|
3194
|
+
};
|
|
3195
|
+
};
|
|
3196
|
+
const offered = above && close ? headingOver(above, inks) : void 0;
|
|
3197
|
+
const withHeading = offered ? columnsFor(offered) : void 0;
|
|
3198
|
+
const heading = withHeading?.held === true ? offered : void 0;
|
|
3199
|
+
const { holds, edges, flushRight } = heading && withHeading ? withHeading : columnsFor(void 0);
|
|
3200
|
+
const grid = edges.slice(0, -1).map((from, k) => pt(Math.max(edges[k + 1] - from, 1)));
|
|
3201
|
+
const tableRows = above && heading ? [{
|
|
3202
|
+
...above,
|
|
3203
|
+
spans: heading.spans
|
|
3204
|
+
}, ...rows] : rows;
|
|
3205
|
+
const table = {
|
|
3206
|
+
properties: {
|
|
3207
|
+
defaultCellMargins: {
|
|
3208
|
+
left: pt(0),
|
|
3209
|
+
right: pt(0)
|
|
3210
|
+
},
|
|
3211
|
+
layout: "fixed",
|
|
3212
|
+
widthType: "dxa",
|
|
3213
|
+
widthPt: pt(grid.reduce((sum, w) => sum + w, 0)),
|
|
3214
|
+
...edges[0] > 0 ? { indentPt: pt(edges[0]) } : {}
|
|
3215
|
+
},
|
|
3216
|
+
grid,
|
|
3217
|
+
rows: tableRows.map((row, r) => {
|
|
3218
|
+
const prev = tableRows[r - 1];
|
|
3219
|
+
const pitch = prev ? prev.top - row.top - (1 - BASELINE_AT) * prev.lineHeight - BASELINE_AT * row.lineHeight : 0;
|
|
3220
|
+
const opening = r === 0 ? row.spacingBefore : pitch > 0 ? pitch : void 0;
|
|
3221
|
+
return {
|
|
3222
|
+
properties: {},
|
|
3223
|
+
cells: splitAtTabs(row.spans).map((cell, k) => {
|
|
3224
|
+
const inset = flushRight[k];
|
|
3225
|
+
const own = grid[k];
|
|
3226
|
+
return {
|
|
3227
|
+
properties: own !== void 0 ? { width: own } : {},
|
|
3228
|
+
content: [paragraphFromRuns(cell, void 0, {
|
|
3229
|
+
...opening !== void 0 ? { spacingBefore: pt(opening) } : {},
|
|
3230
|
+
spacingLine: pt(row.lineHeight),
|
|
3231
|
+
spacingLineRule: "exact",
|
|
3232
|
+
...inset !== void 0 ? {
|
|
3233
|
+
alignment: "right",
|
|
3234
|
+
indentRight: pt(inset)
|
|
3235
|
+
} : {}
|
|
3236
|
+
})]
|
|
3237
|
+
};
|
|
3238
|
+
})
|
|
3239
|
+
};
|
|
3240
|
+
})
|
|
3241
|
+
};
|
|
3242
|
+
const ink = holds.filter((h) => h !== void 0);
|
|
3243
|
+
tableReads.set(table, {
|
|
3244
|
+
rows: tableRows.map((row) => ({
|
|
3245
|
+
top: row.top,
|
|
3246
|
+
lineHeight: row.lineHeight
|
|
3247
|
+
})),
|
|
3248
|
+
from: measure.left + Math.min(...ink.map((h) => h.from)),
|
|
3249
|
+
to: measure.left + Math.max(...ink.map((h) => h.to)),
|
|
3250
|
+
left: measure.left
|
|
3251
|
+
});
|
|
3252
|
+
out.set(heading ? i - 1 : i, {
|
|
3253
|
+
kind: "table",
|
|
3254
|
+
table
|
|
1671
3255
|
});
|
|
3256
|
+
if (heading) out.set(i, null);
|
|
1672
3257
|
for (let k = i + 1; k < to; k++) out.set(k, null);
|
|
1673
3258
|
i = to;
|
|
1674
3259
|
}
|
|
1675
3260
|
return out;
|
|
1676
3261
|
}
|
|
3262
|
+
/**
|
|
3263
|
+
* A line's cells as a heading row over a table's columns: each cell over the
|
|
3264
|
+
* column whose figures it overlaps most, left to right, and an empty cell over
|
|
3265
|
+
* a column it leaves bare. `undefined` where the line is no such row.
|
|
3266
|
+
*
|
|
3267
|
+
* @param line The line above the table, with the cells it was set out in.
|
|
3268
|
+
* @param inks The ink of each of the table's columns.
|
|
3269
|
+
* @returns The line's spans with a tab between every two columns, and the ink
|
|
3270
|
+
* of the heading over each column.
|
|
3271
|
+
*/
|
|
3272
|
+
function headingOver(line, inks) {
|
|
3273
|
+
const cells = splitAtTabs(line.spans);
|
|
3274
|
+
const pieces = line.pieces ?? [];
|
|
3275
|
+
if (cells.length < 2 || pieces.length !== cells.length || cells.length > inks.length || cells.length * 2 < inks.length) return;
|
|
3276
|
+
const columns = [];
|
|
3277
|
+
for (const piece of pieces) {
|
|
3278
|
+
let best = -1;
|
|
3279
|
+
let most = 0;
|
|
3280
|
+
inks.forEach((ink, k) => {
|
|
3281
|
+
if (ink === void 0) return;
|
|
3282
|
+
const overlap = Math.min(piece.to, ink.to) - Math.max(piece.from, ink.from);
|
|
3283
|
+
if (overlap > most) {
|
|
3284
|
+
most = overlap;
|
|
3285
|
+
best = k;
|
|
3286
|
+
}
|
|
3287
|
+
});
|
|
3288
|
+
if (best <= (columns[columns.length - 1] ?? -1)) return void 0;
|
|
3289
|
+
columns.push(best);
|
|
3290
|
+
}
|
|
3291
|
+
return {
|
|
3292
|
+
spans: inks.flatMap((_, k) => {
|
|
3293
|
+
const own = columns.indexOf(k);
|
|
3294
|
+
return [...k > 0 ? [{ text: " " }] : [], ...own >= 0 ? cells[own] : []];
|
|
3295
|
+
}),
|
|
3296
|
+
over: inks.map((_, k) => {
|
|
3297
|
+
const own = columns.indexOf(k);
|
|
3298
|
+
return own >= 0 ? pieces[own] : void 0;
|
|
3299
|
+
})
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
/**
|
|
3303
|
+
* Where a table read off the page stood on it (see `tabbedRows`): each row's
|
|
3304
|
+
* baseline and the box its line stands in, top to bottom, and how far across
|
|
3305
|
+
* the page its words reach — which is what a rule drawn between two of its
|
|
3306
|
+
* rows is measured against (see `ruleBorders`).
|
|
3307
|
+
*/
|
|
3308
|
+
var tableReads = /* @__PURE__ */ new WeakMap();
|
|
3309
|
+
/** How many of its rows' lines a heading may stand over a table and head it. */
|
|
3310
|
+
var HEADING_LINES = 3;
|
|
3311
|
+
/** How far apart the ends of a column's cells may stand and still be flush. */
|
|
3312
|
+
var FLUSH_SLACK_PT = 2;
|
|
1677
3313
|
/** How many stops a line must stand on before a run of them is a table. */
|
|
1678
3314
|
var LEAST_TABLE_STOPS = 2;
|
|
1679
3315
|
/**
|
|
@@ -1700,20 +3336,47 @@ function splitAtTabs(spans) {
|
|
|
1700
3336
|
* the rule introduces; where nothing follows closely enough, the block above
|
|
1701
3337
|
* takes it as a bottom one.
|
|
1702
3338
|
*
|
|
3339
|
+
* A rule drawn down one column is a border of a block in that column, and one
|
|
3340
|
+
* drawn across the columns of a block across them: comments.pdf rules off
|
|
3341
|
+
* Figure 9's caption in the right column, and taken by the nearest line under
|
|
3342
|
+
* it on the page, the rule came back over "Every time the trace recorder
|
|
3343
|
+
* emits…" in the left column, and none over the caption.
|
|
3344
|
+
*
|
|
1703
3345
|
* @param vectors The page's painted paths.
|
|
1704
3346
|
* @param blocks The blocks read off the page so far, which the rule joins.
|
|
1705
3347
|
* @param width The page's width, which a rule is long relative to.
|
|
3348
|
+
* @param colOf The column an x across the page stands in.
|
|
1706
3349
|
* @returns The rules that became borders, and so must not be drawn again.
|
|
1707
3350
|
*/
|
|
1708
|
-
function ruleBorders(vectors, blocks, width) {
|
|
3351
|
+
function ruleBorders(vectors, blocks, width, colOf = () => 0) {
|
|
1709
3352
|
const given = /* @__PURE__ */ new Set();
|
|
1710
3353
|
const paragraphs = blocks.filter((b) => b.el.kind === "paragraph");
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
3354
|
+
const tables = blocks.filter((b) => b.el.kind === "table");
|
|
3355
|
+
if (paragraphs.length === 0 && tables.length === 0) return given;
|
|
3356
|
+
for (const band of collinear(vectors.filter((v) => flatBar(v)))) {
|
|
3357
|
+
const v = band.pieces[0];
|
|
3358
|
+
if (band.to - band.from < width * RULE_SHARE) continue;
|
|
1714
3359
|
const y = (v.minY + v.maxY) / 2;
|
|
1715
|
-
const
|
|
1716
|
-
|
|
3360
|
+
const border = {
|
|
3361
|
+
style: "single",
|
|
3362
|
+
width: pt(Math.max(v.lineWidth ?? v.maxY - v.minY, RULE_MIN_PT)),
|
|
3363
|
+
colorHex: v.strokeHex ?? v.fillHex ?? "000000"
|
|
3364
|
+
};
|
|
3365
|
+
const seat = seatOf(tables, band, y);
|
|
3366
|
+
if (seat !== void 0 && seat.block.el.kind === "table") {
|
|
3367
|
+
seat.block.el = {
|
|
3368
|
+
kind: "table",
|
|
3369
|
+
table: ruledRow(seat.block.el.table, seat.row, border, band)
|
|
3370
|
+
};
|
|
3371
|
+
for (const piece of band.pieces) given.add(piece);
|
|
3372
|
+
continue;
|
|
3373
|
+
}
|
|
3374
|
+
if (paragraphs.length === 0) continue;
|
|
3375
|
+
const first = colOf(band.from + RULE_INSET_PT);
|
|
3376
|
+
const col = first === colOf(band.to - RULE_INSET_PT) ? first : SPANNING_COLUMN;
|
|
3377
|
+
const own = paragraphs.filter((b) => b.col === col);
|
|
3378
|
+
const below = own.filter((b) => b.top < y).sort((a, b) => b.top - a.top)[0];
|
|
3379
|
+
const above = own.filter((b) => b.top >= y).sort((a, b) => a.top - b.top)[0];
|
|
1717
3380
|
const side = below !== void 0 && y - below.top <= RULE_REACH_PT ? {
|
|
1718
3381
|
block: below,
|
|
1719
3382
|
edge: "top"
|
|
@@ -1722,50 +3385,420 @@ function ruleBorders(vectors, blocks, width) {
|
|
|
1722
3385
|
edge: "bottom"
|
|
1723
3386
|
} : void 0;
|
|
1724
3387
|
if (!side || side.block.el.kind !== "paragraph") continue;
|
|
1725
|
-
const border = {
|
|
1726
|
-
style: "single",
|
|
1727
|
-
width: pt(Math.max(v.lineWidth ?? v.maxY - v.minY, RULE_MIN_PT)),
|
|
1728
|
-
colorHex: v.strokeHex ?? v.fillHex ?? "000000"
|
|
1729
|
-
};
|
|
1730
3388
|
const { paragraph } = side.block.el;
|
|
3389
|
+
const before = paragraph.properties.spacingBefore;
|
|
3390
|
+
const room = side.edge === "top" && before !== void 0 ? { spacingBefore: pt(Math.max(0, before - border.width)) } : {};
|
|
1731
3391
|
side.block.el = {
|
|
1732
3392
|
kind: "paragraph",
|
|
1733
3393
|
paragraph: {
|
|
1734
3394
|
...paragraph,
|
|
1735
3395
|
properties: {
|
|
1736
3396
|
...paragraph.properties,
|
|
3397
|
+
...room,
|
|
1737
3398
|
borders: {
|
|
1738
3399
|
...paragraph.properties.borders,
|
|
1739
|
-
[side.edge]: border
|
|
3400
|
+
[side.edge]: border,
|
|
3401
|
+
...side.edge === "top" ? { insideH: border } : {}
|
|
1740
3402
|
}
|
|
1741
3403
|
}
|
|
1742
3404
|
}
|
|
1743
3405
|
};
|
|
1744
|
-
given.add(
|
|
3406
|
+
for (const piece of band.pieces) given.add(piece);
|
|
1745
3407
|
}
|
|
1746
3408
|
return given;
|
|
1747
3409
|
}
|
|
3410
|
+
/**
|
|
3411
|
+
* §17.3.1.33 — the white a block across the page leaves over the columns
|
|
3412
|
+
* under it, as the space after its last paragraph. Each column reads its own
|
|
3413
|
+
* lines from nothing (see `addColumn`), so the white went nowhere, and
|
|
3414
|
+
* comments.pdf's Figure 13 caption came down onto the text under it. As the
|
|
3415
|
+
* space before each column's first paragraph Word and LibreOffice drop it at
|
|
3416
|
+
* the head of the second column, which then stands higher than the first.
|
|
3417
|
+
*
|
|
3418
|
+
* @param blocks A page's blocks, as read; each block across the page with
|
|
3419
|
+
* columns under it is given its white.
|
|
3420
|
+
*/
|
|
3421
|
+
function spaceUnderSpans(blocks) {
|
|
3422
|
+
for (const span of blocks) {
|
|
3423
|
+
if (span.col !== SPANNING_COLUMN || span.foot === void 0 || span.el.kind !== "paragraph") continue;
|
|
3424
|
+
const next = blocks.filter((b) => b.col === SPANNING_COLUMN && b.top < span.top).reduce((high, b) => Math.max(high, b.top), -Infinity);
|
|
3425
|
+
const heads = blocks.filter((b) => b.col !== SPANNING_COLUMN && b.top < span.top && b.top > next).map((b) => b.top + BASELINE_AT * (leadingLine(b.el) ?? 0));
|
|
3426
|
+
if (heads.length === 0) continue;
|
|
3427
|
+
const white = span.foot - Math.max(...heads);
|
|
3428
|
+
if (white <= SPACING_NOISE_PT) continue;
|
|
3429
|
+
const { paragraph } = span.el;
|
|
3430
|
+
span.el = {
|
|
3431
|
+
kind: "paragraph",
|
|
3432
|
+
paragraph: {
|
|
3433
|
+
...paragraph,
|
|
3434
|
+
properties: {
|
|
3435
|
+
...paragraph.properties,
|
|
3436
|
+
spacingAfter: pt(white)
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
/**
|
|
3443
|
+
* Where each column of a page's last stretch of columns — under the last block
|
|
3444
|
+
* across the page — ends: the foot of its lowest paragraph, up the page.
|
|
3445
|
+
*
|
|
3446
|
+
* @param blocks The page's blocks.
|
|
3447
|
+
* @returns The feet, one a column, or nothing where fewer than two columns hold text.
|
|
3448
|
+
*/
|
|
3449
|
+
function lastColumnFeet(blocks) {
|
|
3450
|
+
const across = blocks.filter((b) => b.col === SPANNING_COLUMN).reduce((low, b) => Math.min(low, b.top), Infinity);
|
|
3451
|
+
const feet = /* @__PURE__ */ new Map();
|
|
3452
|
+
for (const b of blocks) {
|
|
3453
|
+
if (b.col === SPANNING_COLUMN || b.foot === void 0 || b.top >= across) continue;
|
|
3454
|
+
feet.set(b.col, Math.min(feet.get(b.col) ?? Infinity, b.foot));
|
|
3455
|
+
}
|
|
3456
|
+
return feet.size >= 2 ? [...feet.values()] : void 0;
|
|
3457
|
+
}
|
|
3458
|
+
/**
|
|
3459
|
+
* Whether a page's last columns are set BALANCED: they end within a couple of
|
|
3460
|
+
* lines of each other, and well above the foot of the text.
|
|
3461
|
+
*
|
|
3462
|
+
* @param feet Where each column ends, up the page.
|
|
3463
|
+
* @param floor Where the text ends at its lowest, up the page.
|
|
3464
|
+
* @param size The body's type size.
|
|
3465
|
+
*/
|
|
3466
|
+
function balancedEnd(feet, floor, size) {
|
|
3467
|
+
const line = size * NATURAL_LINE_EM;
|
|
3468
|
+
const deepest = Math.min(...feet);
|
|
3469
|
+
return Math.max(...feet) - deepest <= line * BALANCED_LINES && deepest - floor >= line * SHORT_LINES;
|
|
3470
|
+
}
|
|
3471
|
+
/** How many lines apart the ends of balanced columns may stand. */
|
|
3472
|
+
var BALANCED_LINES = 4;
|
|
3473
|
+
/** How many lines short of the foot a page's columns stop to be set short. */
|
|
3474
|
+
var SHORT_LINES = 3;
|
|
3475
|
+
/** Whether an element is a drawing anchored to the page, which takes no room in the flow. */
|
|
3476
|
+
function floating(el) {
|
|
3477
|
+
return el?.kind === "shape" && el.shape.float !== void 0 || el?.kind === "image" && el.image.float !== void 0;
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* A table with a rule over one of its rows: the top edge of each of the row's
|
|
3481
|
+
* cells (§17.4.39), the rule taking its room out of the white the row keeps
|
|
3482
|
+
* over it as a paragraph's does (see `ruleBorders`).
|
|
3483
|
+
*/
|
|
3484
|
+
function ruledRow(table, at, border, rule) {
|
|
3485
|
+
const ruled = {
|
|
3486
|
+
...table,
|
|
3487
|
+
rows: table.rows.map((row, r) => r !== at ? row : {
|
|
3488
|
+
...row,
|
|
3489
|
+
cells: row.cells.map((cell) => ({
|
|
3490
|
+
properties: {
|
|
3491
|
+
...cell.properties,
|
|
3492
|
+
borders: {
|
|
3493
|
+
...cell.properties.borders,
|
|
3494
|
+
top: border
|
|
3495
|
+
}
|
|
3496
|
+
},
|
|
3497
|
+
content: cell.content.map((block, k) => {
|
|
3498
|
+
const before = block.kind === "paragraph" && k === 0 ? block.paragraph.properties.spacingBefore : void 0;
|
|
3499
|
+
return before === void 0 || block.kind !== "paragraph" ? block : {
|
|
3500
|
+
...block,
|
|
3501
|
+
paragraph: {
|
|
3502
|
+
...block.paragraph,
|
|
3503
|
+
properties: {
|
|
3504
|
+
...block.paragraph.properties,
|
|
3505
|
+
spacingBefore: pt(Math.max(0, before - border.width))
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
})
|
|
3510
|
+
}))
|
|
3511
|
+
})
|
|
3512
|
+
};
|
|
3513
|
+
const read = tableReads.get(table);
|
|
3514
|
+
if (!read) return ruled;
|
|
3515
|
+
const fitted = fittedToRule(ruled, read, rule);
|
|
3516
|
+
tableReads.set(fitted, read);
|
|
3517
|
+
return fitted;
|
|
3518
|
+
}
|
|
3519
|
+
/**
|
|
3520
|
+
* §17.4.63 — a table as wide as the rule drawn across it.
|
|
3521
|
+
*
|
|
3522
|
+
* A table read off a page begins where its words do, and its last column runs
|
|
3523
|
+
* to the measure; a rule drawn across it is its own edge, standing a little
|
|
3524
|
+
* past its words on either side. comments.pdf rules Figure 13 from six points
|
|
3525
|
+
* left of its names to six right of its last figures, and written as the
|
|
3526
|
+
* edge of the row under its headings, the rule ran on twenty points past the
|
|
3527
|
+
* table, to the margin. The table now spans what the rule does: its first
|
|
3528
|
+
* column reaches out to where the rule begins, its words held where they
|
|
3529
|
+
* stood, and its last column ends where the rule ends.
|
|
3530
|
+
*
|
|
3531
|
+
* Only a rule that reaches past the words on both sides is the table's edge.
|
|
3532
|
+
*
|
|
3533
|
+
* @param table The table, its rule written as a row's border.
|
|
3534
|
+
* @param read Where it stood on the page.
|
|
3535
|
+
* @param rule The rule, across the page.
|
|
3536
|
+
*/
|
|
3537
|
+
function fittedToRule(table, read, rule) {
|
|
3538
|
+
const last = table.grid.length - 1;
|
|
3539
|
+
if (last < 0 || rule.from > read.from || rule.to < read.to) return table;
|
|
3540
|
+
const left = read.left + (table.properties.indentPt ?? 0);
|
|
3541
|
+
const right = left + table.grid.reduce((sum, w) => sum + w, 0);
|
|
3542
|
+
const out = left - rule.from;
|
|
3543
|
+
const inward = right - rule.to;
|
|
3544
|
+
if (Math.abs(out) < FIT_NOISE_PT && Math.abs(inward) < FIT_NOISE_PT) return table;
|
|
3545
|
+
const grid = table.grid.map((w, k) => {
|
|
3546
|
+
const wider = k === 0 ? w + out : w;
|
|
3547
|
+
return pt(Math.max(1, k === last ? wider - inward : wider));
|
|
3548
|
+
});
|
|
3549
|
+
/** A cell's paragraphs, its words held where the page set them. */
|
|
3550
|
+
const held = (content, k) => content.map((block) => {
|
|
3551
|
+
if (block.kind !== "paragraph") return block;
|
|
3552
|
+
const { indentLeft, indentRight } = block.paragraph.properties;
|
|
3553
|
+
const properties = {
|
|
3554
|
+
...block.paragraph.properties,
|
|
3555
|
+
...k === 0 && Math.abs(out) >= FIT_NOISE_PT ? { indentLeft: pt(Math.max(0, (indentLeft ?? 0) + out)) } : {},
|
|
3556
|
+
...k === last && Math.abs(inward) >= FIT_NOISE_PT && indentRight !== void 0 ? { indentRight: pt(Math.max(0, indentRight - inward)) } : {}
|
|
3557
|
+
};
|
|
3558
|
+
return {
|
|
3559
|
+
...block,
|
|
3560
|
+
paragraph: {
|
|
3561
|
+
...block.paragraph,
|
|
3562
|
+
properties
|
|
3563
|
+
}
|
|
3564
|
+
};
|
|
3565
|
+
});
|
|
3566
|
+
return {
|
|
3567
|
+
...table,
|
|
3568
|
+
properties: {
|
|
3569
|
+
...table.properties,
|
|
3570
|
+
widthPt: pt(grid.reduce((sum, w) => sum + w, 0)),
|
|
3571
|
+
indentPt: pt((table.properties.indentPt ?? 0) - out)
|
|
3572
|
+
},
|
|
3573
|
+
grid,
|
|
3574
|
+
rows: table.rows.map((row) => ({
|
|
3575
|
+
...row,
|
|
3576
|
+
cells: row.cells.map((cell, k) => k !== 0 && k !== last ? cell : {
|
|
3577
|
+
properties: {
|
|
3578
|
+
...cell.properties,
|
|
3579
|
+
width: grid[k]
|
|
3580
|
+
},
|
|
3581
|
+
content: held(cell.content, k)
|
|
3582
|
+
})
|
|
3583
|
+
}))
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
/** A fit smaller than this moves nothing a reader sees. */
|
|
3587
|
+
var FIT_NOISE_PT = .5;
|
|
3588
|
+
/**
|
|
3589
|
+
* The table row a rule is drawn on the top edge of: a row after the first,
|
|
3590
|
+
* the rule standing where its line's box meets the box of the line above it,
|
|
3591
|
+
* and reaching across the table's words. `undefined` where the rule is no
|
|
3592
|
+
* such row's.
|
|
3593
|
+
*
|
|
3594
|
+
* @param tables The page's tables, as blocks.
|
|
3595
|
+
* @param band The rule, across the page.
|
|
3596
|
+
* @param y Its height on the page.
|
|
3597
|
+
*/
|
|
3598
|
+
function seatOf(tables, band, y) {
|
|
3599
|
+
for (const block of tables) {
|
|
3600
|
+
const read = block.el.kind === "table" ? tableReads.get(block.el.table) : void 0;
|
|
3601
|
+
if (!read) continue;
|
|
3602
|
+
if (band.from > read.from + RULE_SEAT_PT || band.to < read.to - RULE_SEAT_PT) continue;
|
|
3603
|
+
for (let r = 1; r < read.rows.length; r++) {
|
|
3604
|
+
const row = read.rows[r];
|
|
3605
|
+
const prev = read.rows[r - 1];
|
|
3606
|
+
const edge = row.top + BASELINE_AT * row.lineHeight;
|
|
3607
|
+
const over = prev.top - (1 - BASELINE_AT) * prev.lineHeight;
|
|
3608
|
+
if (y > row.top && y < prev.top && y - edge <= RULE_SEAT_PT && over - y <= RULE_SEAT_PT) return {
|
|
3609
|
+
block,
|
|
3610
|
+
row: r
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
/**
|
|
3616
|
+
* How far off the edge between two rows a rule may stand and still be that
|
|
3617
|
+
* edge — a point or two either way, which the white a row keeps does not
|
|
3618
|
+
* notice.
|
|
3619
|
+
*/
|
|
3620
|
+
var RULE_SEAT_PT = 3;
|
|
1748
3621
|
/** How far from a paragraph a rule may stand and still belong to it. */
|
|
1749
3622
|
var RULE_REACH_PT = 14;
|
|
3623
|
+
/** How far in from its ends a rule is looked at to say which column it stands in. */
|
|
3624
|
+
var RULE_INSET_PT = 2;
|
|
3625
|
+
/**
|
|
3626
|
+
* §17.3.1.31 — the paragraphs a page sets on a box it fills, each given the box
|
|
3627
|
+
* as its shading.
|
|
3628
|
+
*
|
|
3629
|
+
* canvas.pdf heads each section with its name in white on a dark bar, and each
|
|
3630
|
+
* table with "Attributes" or "Methods" on a grey one. Drawn where the page drew
|
|
3631
|
+
* them, the bars stayed on the sheet while the words were set again: headings
|
|
3632
|
+
* slid off their bars, white on white, and the bars struck through the tables
|
|
3633
|
+
* under them. As a paragraph's shading the bar goes where the paragraph goes.
|
|
3634
|
+
*
|
|
3635
|
+
* A paragraph's shading fills its line's box across its indents, and the box
|
|
3636
|
+
* a page draws stands out over and under the line: that is a border of its
|
|
3637
|
+
* own colour over the line, and one under it (§17.3.1.24), as thick as the box
|
|
3638
|
+
* stands out. The room the borders take comes out of the white the page left
|
|
3639
|
+
* over the paragraph and under it — and where there is none to take, the box
|
|
3640
|
+
* reaches no further than the line's box does.
|
|
3641
|
+
*
|
|
3642
|
+
* A box is a paragraph's only where it holds the paragraph's one line and no
|
|
3643
|
+
* other, stands out by less than a line over it and under it, and is no wider
|
|
3644
|
+
* than the column: a panel drawn behind a block of text is drawn where the
|
|
3645
|
+
* page drew it. And only a box of a colour: white is the paper's, and
|
|
3646
|
+
* bug1815476.pdf backs a whole form with white boxes as wide as the sheet —
|
|
3647
|
+
* taken for shading, "A N E X O" stood off centre by as much as the box
|
|
3648
|
+
* reached past the column.
|
|
3649
|
+
*
|
|
3650
|
+
* @param paras A region's paragraphs, top to bottom.
|
|
3651
|
+
* @param boxes The page's filled boxes (see {@link solidBox}).
|
|
3652
|
+
* @param measure The column the paragraphs are set in.
|
|
3653
|
+
* @returns The paragraphs, and how far under the last one its box reaches.
|
|
3654
|
+
*/
|
|
3655
|
+
function onBoxes(paras, boxes, measure) {
|
|
3656
|
+
const out = paras.map((para) => ({ ...para }));
|
|
3657
|
+
if (boxes.length === 0 || measure === void 0) return {
|
|
3658
|
+
paras: out,
|
|
3659
|
+
under: 0
|
|
3660
|
+
};
|
|
3661
|
+
let under = 0;
|
|
3662
|
+
out.forEach((para, k) => {
|
|
3663
|
+
if (para.lineCount !== 1 || (para.stops?.length ?? 0) > 0) return;
|
|
3664
|
+
const lineTop = para.top + BASELINE_AT * para.lineHeight;
|
|
3665
|
+
const lineFoot = para.bottom - (1 - BASELINE_AT) * para.lineHeight;
|
|
3666
|
+
const reach = para.fontSize * BAR_REACH_EM;
|
|
3667
|
+
const box = boxes.find((b) => b.minX >= measure.left - reach && b.maxX <= measure.right + reach && b.minX <= para.left + FLUSH_PT && b.maxX >= para.right - FLUSH_PT && b.maxY >= lineTop - FLUSH_PT && b.maxY - lineTop <= para.lineHeight && b.minY <= lineFoot + FLUSH_PT && lineFoot - b.minY <= para.lineHeight && paras.every((o, n) => n === k || o.bottom > b.maxY || o.top < b.minY));
|
|
3668
|
+
if (box === void 0) return;
|
|
3669
|
+
const next = out[k + 1];
|
|
3670
|
+
const over = Math.min(Math.max(0, box.maxY - lineTop), para.spacingBefore ?? 0, MOST_BORDER_PT);
|
|
3671
|
+
const below = Math.min(Math.max(0, lineFoot - box.minY), next !== void 0 ? next.spacingBefore ?? 0 : Infinity, MOST_BORDER_PT);
|
|
3672
|
+
const inset = para.alignment === void 0 ? Math.max(0, para.left - box.minX) : 0;
|
|
3673
|
+
const shade = {
|
|
3674
|
+
box,
|
|
3675
|
+
over: over > SPACING_NOISE_PT ? over : 0,
|
|
3676
|
+
under: below > SPACING_NOISE_PT ? below : 0,
|
|
3677
|
+
indentLeft: box.minX - measure.left,
|
|
3678
|
+
indentFirstLine: inset,
|
|
3679
|
+
indentRight: measure.right - box.maxX
|
|
3680
|
+
};
|
|
3681
|
+
para.shade = shade;
|
|
3682
|
+
if (para.spacingBefore !== void 0) para.spacingBefore -= shade.over;
|
|
3683
|
+
if (next?.spacingBefore !== void 0) next.spacingBefore -= shade.under;
|
|
3684
|
+
if (k === out.length - 1) under = shade.under;
|
|
3685
|
+
});
|
|
3686
|
+
return {
|
|
3687
|
+
paras: out,
|
|
3688
|
+
under
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
/**
|
|
3692
|
+
* The paragraph properties a {@link Shade} sets: the box's colour behind the
|
|
3693
|
+
* line, a border of it as thick as the box stands out over the line and under
|
|
3694
|
+
* it, and the indents that put its edges where the page drew them.
|
|
3695
|
+
*/
|
|
3696
|
+
function shadeProperties(shade) {
|
|
3697
|
+
const colorHex = shade.box.fillHex ?? "FFFFFF";
|
|
3698
|
+
const edge = (width) => width > 0 ? {
|
|
3699
|
+
style: "single",
|
|
3700
|
+
width: pt(width),
|
|
3701
|
+
colorHex
|
|
3702
|
+
} : void 0;
|
|
3703
|
+
const top = edge(shade.over);
|
|
3704
|
+
const bottom = edge(shade.under);
|
|
3705
|
+
const noted = (value) => Math.abs(value) > SPACING_NOISE_PT ? pt(value) : void 0;
|
|
3706
|
+
const indentLeft = noted(shade.indentLeft);
|
|
3707
|
+
const indentFirstLine = noted(shade.indentFirstLine);
|
|
3708
|
+
const indentRight = noted(shade.indentRight);
|
|
3709
|
+
return {
|
|
3710
|
+
shading: { colorHex },
|
|
3711
|
+
...top || bottom ? { borders: {
|
|
3712
|
+
...top ? { top } : {},
|
|
3713
|
+
...bottom ? { bottom } : {}
|
|
3714
|
+
} } : {},
|
|
3715
|
+
...indentLeft !== void 0 ? { indentLeft } : {},
|
|
3716
|
+
...indentFirstLine !== void 0 ? { indentFirstLine } : {},
|
|
3717
|
+
...indentRight !== void 0 ? { indentRight } : {}
|
|
3718
|
+
};
|
|
3719
|
+
}
|
|
3720
|
+
/**
|
|
3721
|
+
* §17.3.4 — the thickest border a single line may be, twelve points: a box
|
|
3722
|
+
* standing out further than that from its line is not a bar behind it.
|
|
3723
|
+
*/
|
|
3724
|
+
var MOST_BORDER_PT = 12;
|
|
3725
|
+
/** How far past its column, in ems of its line, a bar behind a line may reach. */
|
|
3726
|
+
var BAR_REACH_EM = 2;
|
|
3727
|
+
/** How light, in each of its channels, a fill is the paper's rather than a colour. */
|
|
3728
|
+
var PAPER_LEVEL = 250;
|
|
3729
|
+
/**
|
|
3730
|
+
* Whether a painted path is a box filled in one opaque colour, square to the
|
|
3731
|
+
* page: a bar a heading is set on, a panel. A shape, a gradient, a stroked
|
|
3732
|
+
* frame or a glyph is none, and nor is a box of the paper's own white.
|
|
3733
|
+
*
|
|
3734
|
+
* @param v The painted path.
|
|
3735
|
+
*/
|
|
3736
|
+
function solidBox(v) {
|
|
3737
|
+
if (v.fillHex === void 0 || v.gradient !== void 0 || v.glyph === true) return false;
|
|
3738
|
+
if ([
|
|
3739
|
+
0,
|
|
3740
|
+
2,
|
|
3741
|
+
4
|
|
3742
|
+
].map((k) => parseInt(v.fillHex.slice(k, k + 2), 16)).every((c) => c >= PAPER_LEVEL)) return false;
|
|
3743
|
+
if ((v.alpha ?? 1) < 1 || v.darkens === true) return false;
|
|
3744
|
+
if (v.strokeHex !== void 0 && v.strokeHex !== v.fillHex) return false;
|
|
3745
|
+
if (v.segs.filter((s) => s.op === "move").length !== 1) return false;
|
|
3746
|
+
const points = v.segs.filter((s) => s.op !== "close");
|
|
3747
|
+
if (points.length < 4 || points.length > 5) return false;
|
|
3748
|
+
const at = (value, edge) => Math.abs(value - edge) <= FLUSH_PT;
|
|
3749
|
+
return points.every((s) => s.op !== "cubic" && (at(s.x, v.minX) || at(s.x, v.maxX)) && (at(s.y, v.minY) || at(s.y, v.maxY)));
|
|
3750
|
+
}
|
|
1750
3751
|
/** The thinnest a border may be drawn and still be seen. */
|
|
1751
3752
|
var RULE_MIN_PT = .5;
|
|
1752
3753
|
/**
|
|
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.
|
|
3754
|
+
* Whether a painted path is flat and thin enough to be a piece of a RULE — a
|
|
3755
|
+
* line drawn to separate one block from the next, rather than a piece of the
|
|
3756
|
+
* page's artwork. How LONG the rule is is a question for the whole of it (see
|
|
3757
|
+
* {@link collinear}): a hairline the width of a column under a table's
|
|
3758
|
+
* headings, or the line a total is written over. Everything else — boxes,
|
|
3759
|
+
* panels, drawings — keeps the place on the page it was drawn at.
|
|
3760
|
+
*
|
|
3761
|
+
* @param v The painted path.
|
|
3762
|
+
* @returns Whether it could be a rule, or a piece of one.
|
|
1763
3763
|
*/
|
|
1764
|
-
function
|
|
3764
|
+
function flatBar(v) {
|
|
1765
3765
|
const w = v.maxX - v.minX;
|
|
1766
3766
|
const h = v.maxY - v.minY;
|
|
1767
|
-
return h <= RULE_THICK_PT && w
|
|
3767
|
+
return h <= RULE_THICK_PT && w > h * RULE_RATIO;
|
|
3768
|
+
}
|
|
3769
|
+
/**
|
|
3770
|
+
* The flat bars of a page gathered into the rules they draw: pieces on one
|
|
3771
|
+
* baseline, end to end, are one rule from the first's left edge to the last's
|
|
3772
|
+
* right.
|
|
3773
|
+
*
|
|
3774
|
+
* @param bars The page's flat bars.
|
|
3775
|
+
* @returns One band per rule, with the pieces it was drawn in.
|
|
3776
|
+
*/
|
|
3777
|
+
function collinear(bars) {
|
|
3778
|
+
const out = [];
|
|
3779
|
+
for (const bar of [...bars].sort((a, b) => b.maxY - a.maxY || a.minX - b.minX)) {
|
|
3780
|
+
const y = (bar.minY + bar.maxY) / 2;
|
|
3781
|
+
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));
|
|
3782
|
+
if (band) {
|
|
3783
|
+
band.from = Math.min(band.from, bar.minX);
|
|
3784
|
+
band.to = Math.max(band.to, bar.maxX);
|
|
3785
|
+
band.pieces.push(bar);
|
|
3786
|
+
} else out.push({
|
|
3787
|
+
from: bar.minX,
|
|
3788
|
+
to: bar.maxX,
|
|
3789
|
+
pieces: [bar]
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
return out;
|
|
1768
3793
|
}
|
|
3794
|
+
/** How far apart two bars' baselines may be and still be one rule. */
|
|
3795
|
+
var RULE_SAME_LINE_PT = 1.5;
|
|
3796
|
+
/**
|
|
3797
|
+
* …and how wide a gap between two pieces of it the page may leave: an invoice
|
|
3798
|
+
* draws the rule under its item row cell by cell and skips the thirteen points
|
|
3799
|
+
* of gutter between two of them.
|
|
3800
|
+
*/
|
|
3801
|
+
var RULE_JOIN_PT = 18;
|
|
1769
3802
|
/** How thick a mark may be and still be a rule. */
|
|
1770
3803
|
var RULE_THICK_PT = 2;
|
|
1771
3804
|
/** How much of the sheet it must run across. */
|
|
@@ -1776,6 +3809,8 @@ var RULE_RATIO = 20;
|
|
|
1776
3809
|
var FLUSH_PT = 1;
|
|
1777
3810
|
/** And how many lines it takes before a region is a column at all. */
|
|
1778
3811
|
var MIN_PROSE_LINES = 6;
|
|
3812
|
+
/** How many of a column's lines fill it when it is justified prose. */
|
|
3813
|
+
var FILLED_SHARE = .5;
|
|
1779
3814
|
/**
|
|
1780
3815
|
* §17.4.38 — the page's rows as the TABLE they are.
|
|
1781
3816
|
*
|
|
@@ -1967,4 +4002,4 @@ var TABLE_ALIGNED_SHARE = .6;
|
|
|
1967
4002
|
*/
|
|
1968
4003
|
var TABLE_CELL_FILL = .65;
|
|
1969
4004
|
//#endregion
|
|
1970
|
-
export { endedParagraph, reconstructByLayout };
|
|
4005
|
+
export { FOOTER_PART, HEADER_PART, endedParagraph, footerBand, numberedFrom, numberingOf, pageTextEdges, reconstructByLayout, runningFoot, stepsBetweenWords };
|