reamkit 1.30.0 → 1.32.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 +70 -24
- package/dist/esm/core/bidi/index.d.ts +10 -0
- package/dist/esm/core/bidi/index.js +18 -2
- package/dist/esm/core/converter/facade.js +10 -5
- package/dist/esm/core/converter/project.js +4 -1
- package/dist/esm/core/converter/ream.js +17 -6
- package/dist/esm/core/converter/sheet-images.d.ts +17 -0
- package/dist/esm/core/converter/sheet-images.js +33 -0
- 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 +175 -0
- package/dist/esm/core/drawingml/chart-geometry.d.ts +21 -8
- package/dist/esm/core/drawingml/chart-geometry.js +198 -108
- package/dist/esm/core/drawingml/chart-parser.js +73 -2
- package/dist/esm/core/drawingml/colors.d.ts +6 -0
- package/dist/esm/core/drawingml/colors.js +23 -3
- package/dist/esm/core/drawingml/shape-render.d.ts +3 -1
- package/dist/esm/core/drawingml/shape-render.js +4 -2
- package/dist/esm/core/font/index.d.ts +2 -0
- package/dist/esm/core/font/measure.d.ts +9 -0
- package/dist/esm/core/font/measure.js +9 -1
- 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/families.js +4 -2
- 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.js +8 -1
- package/dist/esm/core/fonts/scripts.js +4 -2
- package/dist/esm/core/ir/flow.d.ts +77 -1
- package/dist/esm/core/ir/sheet.d.ts +43 -0
- package/dist/esm/core/number-format.js +16 -11
- 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/spreadsheet-model/index.d.ts +1 -1
- package/dist/esm/core/spreadsheet-model/types.d.ts +96 -0
- package/dist/esm/core/style-cascade/resolver.js +39 -6
- package/dist/esm/core/style-cascade/types.d.ts +19 -1
- package/dist/esm/core/style-cascade/types.js +3 -0
- package/dist/esm/excel/column-bands.d.ts +39 -6
- package/dist/esm/excel/column-bands.js +41 -17
- package/dist/esm/excel/comments-parser.js +19 -0
- package/dist/esm/excel/conditional-format.d.ts +7 -3
- package/dist/esm/excel/conditional-format.js +77 -39
- package/dist/esm/excel/formula/context.d.ts +18 -0
- package/dist/esm/excel/formula/eval.js +51 -0
- package/dist/esm/excel/formula/index.d.ts +2 -2
- package/dist/esm/excel/formula/lexer.d.ts +1 -1
- package/dist/esm/excel/formula/lexer.js +41 -0
- package/dist/esm/excel/formula/parser.d.ts +25 -0
- package/dist/esm/excel/formula/parser.js +95 -0
- package/dist/esm/excel/print-model.d.ts +38 -1
- package/dist/esm/excel/print-model.js +522 -80
- package/dist/esm/excel/shared-strings-parser.d.ts +9 -0
- package/dist/esm/excel/shared-strings-parser.js +11 -1
- package/dist/esm/excel/sheet-drawing.d.ts +38 -2
- package/dist/esm/excel/sheet-drawing.js +47 -13
- package/dist/esm/excel/sheet-shape-parser.d.ts +6 -2
- package/dist/esm/excel/sheet-shape-parser.js +18 -6
- package/dist/esm/excel/sheet-to-flow.d.ts +17 -0
- package/dist/esm/excel/sheet-to-flow.js +434 -47
- package/dist/esm/excel/styles-parser.d.ts +9 -0
- package/dist/esm/excel/styles-parser.js +80 -26
- package/dist/esm/excel/table-parser.js +5 -0
- package/dist/esm/excel/table-style-presets.d.ts +26 -0
- package/dist/esm/excel/table-style-presets.js +1555 -0
- package/dist/esm/excel/table-style.d.ts +13 -0
- package/dist/esm/excel/table-style.js +75 -0
- package/dist/esm/excel/tint.d.ts +10 -0
- package/dist/esm/excel/tint.js +75 -0
- package/dist/esm/excel/vml-drawing.d.ts +41 -3
- package/dist/esm/excel/vml-drawing.js +143 -10
- package/dist/esm/excel/worksheet-parser.js +89 -15
- package/dist/esm/excel/xls/biff-reader.js +10 -2
- package/dist/esm/excel/xlsx-reader.js +195 -14
- package/dist/esm/excel/xlsx-writer.js +8 -7
- package/dist/esm/html/html-writer.js +202 -20
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/layout/fit-pages.d.ts +11 -0
- package/dist/esm/layout/fit-pages.js +86 -0
- package/dist/esm/layout/line-paint.d.ts +58 -0
- package/dist/esm/layout/line-paint.js +107 -0
- package/dist/esm/layout/page-doc.d.ts +61 -3
- package/dist/esm/layout/page-doc.js +141 -1
- package/dist/esm/layout/styled-layout.d.ts +55 -7
- package/dist/esm/layout/styled-layout.js +1154 -169
- package/dist/esm/layout/turned-section.d.ts +38 -0
- package/dist/esm/layout/turned-section.js +205 -0
- package/dist/esm/markdown/markdown-writer.js +17 -6
- package/dist/esm/pdf/styled-page-emitter.js +114 -60
- package/dist/esm/pdf/styled-page-renderer.js +1 -1
- 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/content.d.ts +53 -0
- package/dist/esm/pdf-reader/content.js +9 -1
- package/dist/esm/pdf-reader/display.d.ts +36 -0
- package/dist/esm/pdf-reader/display.js +66 -1
- package/dist/esm/pdf-reader/document.js +5 -1
- package/dist/esm/pdf-reader/embedded-fonts.d.ts +2 -1
- package/dist/esm/pdf-reader/embedded-fonts.js +32 -2
- 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 +131 -5
- package/dist/esm/pdf-reader/flow-build.js +345 -26
- package/dist/esm/pdf-reader/font.js +321 -36
- package/dist/esm/pdf-reader/glyf-outline.d.ts +33 -0
- package/dist/esm/pdf-reader/glyf-outline.js +135 -1
- package/dist/esm/pdf-reader/glyph-names.js +154 -1
- package/dist/esm/pdf-reader/layout.d.ts +93 -0
- package/dist/esm/pdf-reader/layout.js +1703 -214
- 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/tagged.js +182 -23
- package/dist/esm/pdf-reader/text.d.ts +6 -3
- package/dist/esm/pdf-reader/text.js +98 -9
- 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.js +71 -1
- package/dist/esm/svg/svg-text.d.ts +58 -0
- package/dist/esm/svg/svg-text.js +322 -0
- package/dist/esm/svg/svg-writer.js +135 -71
- 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 +218 -23
- package/dist/esm/word/drawing-parser.d.ts +5 -3
- package/dist/esm/word/drawing-parser.js +49 -8
- 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 +14 -2
- package/dist/esm/word/run-properties.js +26 -0
- package/package.json +12 -3
|
@@ -4,18 +4,23 @@ import { FEATURES } from "../core/ir/features.js";
|
|
|
4
4
|
import { isRightToLeft } from "./content.js";
|
|
5
5
|
import { collectEmbeddedFonts } from "./embedded-fonts.js";
|
|
6
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";
|
|
7
10
|
import { extractPageText } from "./text.js";
|
|
8
11
|
import { collectPageVectors } from "./vector.js";
|
|
9
|
-
import { displayOf, placeImages, placeRuns, placeVectors } from "./display.js";
|
|
10
|
-
import { BASELINE_AT, CARRIER_LINE_PT, GUESSED_MARGIN, NATURAL_LINE_EM, buildFlowDoc, dedupeLosses, imageBlock, paragraphFromRuns, positionedText, sectionFromPdfPages, shapeBlock, spaceAfter, textSizeOf, tooSmallToRead, withMeasuredMargins } from "./flow-build.js";
|
|
11
12
|
import { collectPageImages } from "./images.js";
|
|
12
13
|
import { markDrawnRules } from "./text-rules.js";
|
|
14
|
+
import { joinLetters, pageFigures, tracedRun } from "./figures.js";
|
|
13
15
|
import { regionsOf } from "./regions.js";
|
|
14
16
|
import { punctuationOf } from "./glyph-shapes.js";
|
|
15
17
|
import { matrixBlocks } from "./math-rows.js";
|
|
18
|
+
import { pageNumberingOf, runOf } from "./page-numbers.js";
|
|
16
19
|
//#region src/pdf-reader/layout.ts
|
|
17
20
|
/** The relationships the reconstruction files its running head and foot under. */
|
|
18
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";
|
|
19
24
|
var HEADER_PART = "pdf-running-head";
|
|
20
25
|
/**
|
|
21
26
|
* Heuristically reconstruct an untagged PDF into a {@link Reconstruction}
|
|
@@ -34,19 +39,60 @@ var HEADER_PART = "pdf-running-head";
|
|
|
34
39
|
*/
|
|
35
40
|
function reconstructByLayout(file, mode = "flow") {
|
|
36
41
|
const pages = file.pages();
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
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);
|
|
41
61
|
const textSize = textSizeOf(allRuns);
|
|
42
|
-
const stamp = (r) => mode !== "positional" && (r.annotation === true || tooSmallToRead(r, textSize));
|
|
43
|
-
const stamps = allRuns.map((runs) => runs.filter(stamp));
|
|
44
|
-
const
|
|
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;
|
|
45
86
|
const pageMarks = pages.map(() => []);
|
|
46
87
|
const medianFont = median(pageRuns.flat().map((r) => r.fontSizePt).filter((s) => s > 0)) || 12;
|
|
47
88
|
const resources = new ResourceStore();
|
|
48
89
|
const losses = [];
|
|
49
|
-
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({
|
|
50
96
|
severity: "dropped",
|
|
51
97
|
feature: FEATURES.text,
|
|
52
98
|
detail: "some glyphs map to no character — the font states no /ToUnicode and its program says nothing either, so that text is unrecoverable"
|
|
@@ -61,30 +107,43 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
61
107
|
feature: FEATURES.text,
|
|
62
108
|
detail: "text filled with a shading pattern is drawn in the middle colour of the gradient, not as the gradient"
|
|
63
109
|
});
|
|
64
|
-
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));
|
|
65
116
|
const shared = commonGutters(perPage);
|
|
66
|
-
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);
|
|
67
118
|
const body = [];
|
|
68
119
|
const sectionEnds = [];
|
|
69
120
|
let sectionFrom = 0;
|
|
70
121
|
let lastSize = "";
|
|
122
|
+
const sheetSizes = [];
|
|
71
123
|
const leads = [];
|
|
72
124
|
let curColumns = 1;
|
|
73
125
|
let curSpace = 0;
|
|
74
126
|
let pendingContinuous = false;
|
|
127
|
+
const columnFeet = [];
|
|
128
|
+
const columnTurns = [];
|
|
75
129
|
pages.forEach((page, i) => {
|
|
76
130
|
const runs = pageRuns[i];
|
|
77
131
|
const display = shown[i];
|
|
132
|
+
const turned = display.sheet !== void 0;
|
|
78
133
|
const gutters = pageGutters[i];
|
|
79
134
|
const pageWidth = display.width;
|
|
80
135
|
const stepped = stepsBetweenWords(runs);
|
|
81
136
|
const blocks = [];
|
|
137
|
+
const shaded = /* @__PURE__ */ new Set();
|
|
82
138
|
const textEdges = pageTextEdges(runs);
|
|
83
139
|
const inColumns = proseColumns(runs, gutters, textEdges);
|
|
84
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) : [];
|
|
85
144
|
const bandEpsilon = (median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10) / 2;
|
|
86
|
-
const bandAt = (top) => bandOf(
|
|
87
|
-
const addColumn = (allRuns, col) => {
|
|
145
|
+
const bandAt = (top) => bandOf(breaks, top, bandEpsilon);
|
|
146
|
+
const addColumn = (allRuns, col, columnFigures = []) => {
|
|
88
147
|
const colRuns = allRuns.filter((r) => r.type3 !== true && (mode !== "positional" || r.invisible !== true));
|
|
89
148
|
if (mode === "positional") {
|
|
90
149
|
for (const [angle, turned] of byAngle(colRuns)) for (const runs of byPainter(turned)) for (const line of groupIntoLines(rotate(runs, -angle), true, stepped)) {
|
|
@@ -132,10 +191,55 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
132
191
|
});
|
|
133
192
|
}
|
|
134
193
|
let above;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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);
|
|
139
243
|
blocks.push({
|
|
140
244
|
band: bandAt(made.top),
|
|
141
245
|
col,
|
|
@@ -143,27 +247,64 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
143
247
|
el: made.el
|
|
144
248
|
});
|
|
145
249
|
above = made.below;
|
|
146
|
-
continue;
|
|
147
250
|
}
|
|
148
|
-
const regionLines = groupIntoLines(region.runs, false, stepped).filter((l) => l.text.length > 0);
|
|
149
|
-
const reach = lines.length < 3 && gutters.length === 0 ? pageWidth * (1 - GUESSED_MARGIN) : void 0;
|
|
150
|
-
const paras = groupIntoParagraphs(regionLines, measure, display.height, above, false, reach);
|
|
151
|
-
for (const set of setParagraphs(paras, measure && {
|
|
152
|
-
left: measure.left,
|
|
153
|
-
right: Math.max(measure.right, textEdges?.right ?? measure.right)
|
|
154
|
-
}, pageWidth)) blocks.push({
|
|
155
|
-
band: bandAt(set.top),
|
|
156
|
-
col,
|
|
157
|
-
top: set.top,
|
|
158
|
-
el: set.el
|
|
159
|
-
});
|
|
160
|
-
const last = paras[paras.length - 1];
|
|
161
|
-
if (last !== void 0) above = {
|
|
162
|
-
y: last.bottom,
|
|
163
|
-
lineHeight: last.lineHeight
|
|
164
|
-
};
|
|
165
251
|
}
|
|
166
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
|
+
};
|
|
167
308
|
/**
|
|
168
309
|
* A region's paragraphs as the elements they are set in: the lines that
|
|
169
310
|
* stand on the same stops as a table (§17.4.38), the rest as paragraphs
|
|
@@ -171,7 +312,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
171
312
|
*/
|
|
172
313
|
const far = /* @__PURE__ */ new WeakMap();
|
|
173
314
|
const setParagraphs = (paras, tableMeasure, sheetWidth = 0) => {
|
|
174
|
-
const asRows = tabbedRows(paras, tableMeasure);
|
|
315
|
+
const asRows = turned ? /* @__PURE__ */ new Map() : tabbedRows(paras, tableMeasure);
|
|
175
316
|
const out = [];
|
|
176
317
|
for (const [at, para] of paras.entries()) {
|
|
177
318
|
const table = asRows.get(at);
|
|
@@ -183,6 +324,8 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
183
324
|
continue;
|
|
184
325
|
}
|
|
185
326
|
const overflow = sheetWidth > 0 ? runOn(para, tableMeasure, sheetWidth) : 0;
|
|
327
|
+
const { shade } = para;
|
|
328
|
+
if (shade) shaded.add(shade.box);
|
|
186
329
|
const el = paragraphFromRuns(para.spans, headingLevel(para.fontSize, medianFont), {
|
|
187
330
|
...para.stops !== void 0 && para.stops.length > 0 ? { tabs: para.stops.filter((x) => x > 0).map((x) => ({
|
|
188
331
|
positionPt: pt(x),
|
|
@@ -194,12 +337,14 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
194
337
|
spacingLineRule: "exact",
|
|
195
338
|
...para.indentLeft !== void 0 ? { indentLeft: pt(para.indentLeft) } : {},
|
|
196
339
|
...para.indentFirstLine !== void 0 ? { indentFirstLine: pt(para.indentFirstLine) } : {},
|
|
197
|
-
...overflow > 0 ? { indentRight: pt(-overflow) } : {}
|
|
340
|
+
...overflow > 0 ? { indentRight: pt(-overflow) } : {},
|
|
341
|
+
...shade ? shadeProperties(shade) : {}
|
|
198
342
|
});
|
|
199
343
|
if (sheetWidth > 0 && para.far !== void 0) far.set(el, para.far);
|
|
200
344
|
out.push({
|
|
201
345
|
top: para.top,
|
|
202
|
-
el
|
|
346
|
+
el,
|
|
347
|
+
foot: para.bottom - (1 - BASELINE_AT) * para.lineHeight - (shade?.under ?? 0)
|
|
203
348
|
});
|
|
204
349
|
}
|
|
205
350
|
return out;
|
|
@@ -215,7 +360,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
215
360
|
const cells = region.cells.map((cell) => {
|
|
216
361
|
return {
|
|
217
362
|
cell,
|
|
218
|
-
lines: groupIntoLines(cell.runs, false, stepped).filter((l) => l.text.length > 0)
|
|
363
|
+
lines: groupIntoLines(cell.runs, false, stepped, faceSpaces).filter((l) => l.text.length > 0)
|
|
219
364
|
};
|
|
220
365
|
});
|
|
221
366
|
if (cells.some((c) => c.lines.length === 0)) return void 0;
|
|
@@ -231,7 +376,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
231
376
|
left: cell.from,
|
|
232
377
|
right: cell.to
|
|
233
378
|
};
|
|
234
|
-
const paras = groupIntoParagraphs(lines, own, display.height, edge,
|
|
379
|
+
const paras = groupIntoParagraphs(lines, own, display.height, edge, !isProse(lines, own));
|
|
235
380
|
const last = paras[paras.length - 1];
|
|
236
381
|
foot = Math.min(foot, last.bottom - (1 - BASELINE_AT) * last.lineHeight);
|
|
237
382
|
const inset = cell.from - starts[k];
|
|
@@ -295,21 +440,13 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
295
440
|
make: (z) => positionedText(line.spans, box, frame, z)
|
|
296
441
|
});
|
|
297
442
|
}
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const lifted = collectPageVectors(file, page, imgs.images.map((img) => ({
|
|
306
|
-
minX: img.x,
|
|
307
|
-
minY: img.y,
|
|
308
|
-
maxX: img.x + img.widthPt,
|
|
309
|
-
maxY: img.y + img.heightPt
|
|
310
|
-
})));
|
|
311
|
-
losses.push(...lifted.losses);
|
|
312
|
-
const placedVectors = placeVectors(lifted.vectors, display);
|
|
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;
|
|
313
450
|
const drawnRules = markDrawnRules(runs, placedVectors);
|
|
314
451
|
const strayGlyphs = strayMarks(placedVectors, runs);
|
|
315
452
|
const ruled = mode !== "positional" ? {
|
|
@@ -318,22 +455,34 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
318
455
|
} : drawnRules;
|
|
319
456
|
const vectors = placedVectors.filter((v) => !ruled.consumed.has(v)).filter((v) => !(mode !== "positional" && strayGlyphs.has(v)));
|
|
320
457
|
const drawn = drawnWords(vectors);
|
|
321
|
-
const ruledIntoColumns = mode !== "positional" && looksRuled(ruled.runs, gutters, textEdges);
|
|
458
|
+
const ruledIntoColumns = mode !== "positional" && !turned && looksRuled(ruled.runs, gutters, textEdges);
|
|
322
459
|
const asTable = ruledIntoColumns && textEdges ? tableFrom(ruled.runs, gutters, textEdges, stepped) : void 0;
|
|
323
|
-
if (asTable)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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) {
|
|
330
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);
|
|
331
479
|
const columns = Array.from({ length: gutters.length + 1 }, (_, n) => n);
|
|
332
|
-
for (const col of [SPANNING_COLUMN, ...columns]) addColumn(ruled.runs.filter((r) => columnFor(r) === col), col);
|
|
333
|
-
|
|
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);
|
|
334
483
|
const under = mode !== "positional";
|
|
335
484
|
setOff(blocks, far, (line, k) => positionedText(line.spans, turnedBox(line, 0, pageWidth), frame, FAR_Z + k));
|
|
336
|
-
const givenAway = mode !== "positional" ? ruleBorders(vectors, blocks, display.width) : void 0;
|
|
485
|
+
const givenAway = mode !== "positional" ? ruleBorders(vectors, blocks, display.width, colOf) : void 0;
|
|
337
486
|
[
|
|
338
487
|
...imgs.images.map((img) => ({
|
|
339
488
|
key: img.orderKey,
|
|
@@ -341,7 +490,7 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
341
490
|
top: img.y + img.heightPt,
|
|
342
491
|
make: (z) => imageBlock(img, resources, void 0, frame, z, under)
|
|
343
492
|
})),
|
|
344
|
-
...drawn.filter((v) => givenAway?.has(v) !== true).map((v) => ({
|
|
493
|
+
...drawn.filter((v) => givenAway?.has(v) !== true && !shaded.has(v)).map((v) => ({
|
|
345
494
|
key: v.orderKey,
|
|
346
495
|
col: colOf((v.minX + v.maxX) / 2),
|
|
347
496
|
top: v.maxY,
|
|
@@ -357,8 +506,8 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
357
506
|
});
|
|
358
507
|
});
|
|
359
508
|
blocks.sort((a, b) => a.band - b.band || columnOrder(a.col) - columnOrder(b.col) || b.top - a.top);
|
|
360
|
-
const size = `${shown[i].width.toFixed(2)}x${shown[i].height.toFixed(2)}`;
|
|
361
|
-
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);
|
|
362
511
|
if (opensSection) {
|
|
363
512
|
sectionEnds.push({
|
|
364
513
|
at: body.length,
|
|
@@ -372,6 +521,8 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
372
521
|
pendingContinuous = false;
|
|
373
522
|
}
|
|
374
523
|
lastSize = size;
|
|
524
|
+
sheetSizes[i] = size;
|
|
525
|
+
const breakAt = i > 0 && !opensSection ? body.length : void 0;
|
|
375
526
|
if (i > 0 && !opensSection) body.push({
|
|
376
527
|
kind: "paragraph",
|
|
377
528
|
paragraph: {
|
|
@@ -395,40 +546,147 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
395
546
|
});
|
|
396
547
|
const columnsHere = ruledIntoColumns || !proseColumns(runs, gutters, textEdges) ? 1 : gutters.length + 1;
|
|
397
548
|
const spacePt = columnsHere > 1 ? median(gutters.map((g) => g.to - g.from)) : 0;
|
|
549
|
+
columnFeet[i] = columnsHere > 1 ? lastColumnFeet(blocks) : void 0;
|
|
398
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();
|
|
399
559
|
for (const block of blocks) {
|
|
400
|
-
if (
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
}
|
|
409
588
|
}
|
|
589
|
+
prev = block;
|
|
410
590
|
const count = block.col === SPANNING_COLUMN ? 1 : columnsHere;
|
|
411
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
|
+
}
|
|
412
608
|
sectionEnds.push({
|
|
413
609
|
at: body.length,
|
|
414
610
|
from: sectionFrom,
|
|
415
|
-
to: i + 1,
|
|
611
|
+
to: opensPage ? i : i + 1,
|
|
416
612
|
columns: curColumns,
|
|
417
613
|
spacePt: curSpace,
|
|
418
614
|
continuous: pendingContinuous
|
|
419
615
|
});
|
|
420
616
|
sectionFrom = i;
|
|
421
|
-
pendingContinuous =
|
|
617
|
+
pendingContinuous = !opensPage;
|
|
422
618
|
curColumns = count;
|
|
423
619
|
curSpace = spacePt;
|
|
424
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);
|
|
425
632
|
body.push(block.el);
|
|
633
|
+
begun = true;
|
|
426
634
|
}
|
|
427
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();
|
|
428
645
|
const setUp = (from, to) => {
|
|
429
|
-
const
|
|
430
|
-
|
|
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;
|
|
431
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
|
+
}
|
|
432
690
|
sectionEnds.push({
|
|
433
691
|
at: body.length,
|
|
434
692
|
from: sectionFrom,
|
|
@@ -437,30 +695,16 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
437
695
|
spacePt: curSpace,
|
|
438
696
|
continuous: pendingContinuous
|
|
439
697
|
});
|
|
440
|
-
const sections = sectionEnds.length > 1 ? sectionEnds.flatMap((end) => {
|
|
441
|
-
const base = setUp(end.from, end.to);
|
|
442
|
-
if (!base) return [];
|
|
443
|
-
return [{
|
|
444
|
-
properties: {
|
|
445
|
-
...base,
|
|
446
|
-
...end.columns > 1 && mode !== "positional" ? { columns: {
|
|
447
|
-
count: end.columns,
|
|
448
|
-
spacePt: end.spacePt
|
|
449
|
-
} } : {},
|
|
450
|
-
...end.continuous ? { sectionStart: "continuous" } : {}
|
|
451
|
-
},
|
|
452
|
-
endIndex: end.at
|
|
453
|
-
}];
|
|
454
|
-
}) : [];
|
|
455
698
|
for (const lead of leads) {
|
|
456
699
|
const end = sectionEnds.find((e) => e.at > lead.at);
|
|
457
700
|
const top = (end ? setUp(end.from, end.to) : setUp(0, pages.length))?.margins?.top;
|
|
458
701
|
const page = shown[lead.page];
|
|
459
702
|
const el = body[lead.at];
|
|
460
|
-
if (top === void 0 || page === void 0
|
|
461
|
-
const
|
|
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;
|
|
462
706
|
if (before <= SPACING_NOISE_PT) continue;
|
|
463
|
-
body[lead.at] = {
|
|
707
|
+
if (el?.kind === "paragraph") body[lead.at] = {
|
|
464
708
|
...el,
|
|
465
709
|
paragraph: {
|
|
466
710
|
...el.paragraph,
|
|
@@ -470,27 +714,127 @@ function reconstructByLayout(file, mode = "flow") {
|
|
|
470
714
|
}
|
|
471
715
|
}
|
|
472
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
|
+
});
|
|
473
747
|
}
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
+
}
|
|
781
|
+
const sections = sectionEnds.length > 1 ? sectionEnds.flatMap((end) => {
|
|
782
|
+
const base = sectionOnSheet(setUp(end.from, end.to), shown[end.from]);
|
|
783
|
+
if (!base) return [];
|
|
784
|
+
return [{
|
|
785
|
+
properties: {
|
|
786
|
+
...base,
|
|
787
|
+
...numberedFrom(numbering, end.from),
|
|
788
|
+
...end.columns > 1 && mode !== "positional" ? { columns: {
|
|
789
|
+
count: end.columns,
|
|
790
|
+
spacePt: end.spacePt
|
|
791
|
+
} } : {},
|
|
792
|
+
...end.continuous ? { sectionStart: "continuous" } : {}
|
|
793
|
+
},
|
|
794
|
+
endIndex: end.at
|
|
795
|
+
}];
|
|
796
|
+
}) : [];
|
|
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)) : [];
|
|
478
814
|
const withFooter = (properties) => properties ? {
|
|
479
815
|
...properties,
|
|
480
816
|
...band.length > 0 ? { footers: [{
|
|
481
817
|
type: "default",
|
|
482
818
|
relationshipId: FOOTER_PART
|
|
483
|
-
}
|
|
819
|
+
}, ...evenBand.length > 0 ? [{
|
|
820
|
+
type: "even",
|
|
821
|
+
relationshipId: EVEN_FOOTER_PART
|
|
822
|
+
}] : []] } : {},
|
|
823
|
+
...evenBand.length > 0 ? { evenAndOddHeaders: true } : {},
|
|
484
824
|
...headBand.length > 0 ? { headers: [{
|
|
485
825
|
type: "default",
|
|
486
826
|
relationshipId: HEADER_PART
|
|
487
827
|
}] } : {}
|
|
488
828
|
} : properties;
|
|
489
829
|
return {
|
|
490
|
-
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) => ({
|
|
491
831
|
...s,
|
|
492
832
|
properties: withFooter(s.properties) ?? s.properties
|
|
493
|
-
})), 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)),
|
|
494
838
|
losses: dedupeLosses(losses)
|
|
495
839
|
};
|
|
496
840
|
}
|
|
@@ -503,7 +847,8 @@ function detectGutters(runs, pageWidth) {
|
|
|
503
847
|
});
|
|
504
848
|
if (spans.length === 0) return [];
|
|
505
849
|
const minX = Math.min(...spans.map((iv) => iv[0]));
|
|
506
|
-
const
|
|
850
|
+
const maxX = Math.max(...spans.map((iv) => iv[1]));
|
|
851
|
+
const span = maxX - minX;
|
|
507
852
|
if (span < pageWidth * .5) return [];
|
|
508
853
|
const sorted = [...spans].sort((a, b) => a[0] - b[0]);
|
|
509
854
|
let curEnd = sorted[0][1];
|
|
@@ -544,6 +889,7 @@ function detectGutters(runs, pageWidth) {
|
|
|
544
889
|
const after = row.filter(([l]) => l >= x);
|
|
545
890
|
if (before.length === 0 || after.length === 0) continue;
|
|
546
891
|
if (Math.min(...after.map(([l]) => l)) - Math.max(...before.map(([, r]) => r)) >= fontSize * MIN_GUTTER_EM) columned++;
|
|
892
|
+
else crossing++;
|
|
547
893
|
}
|
|
548
894
|
if (columned < MIN_COLUMNED_ROWS || crossing >= columned) continue;
|
|
549
895
|
const last = voted[voted.length - 1];
|
|
@@ -559,9 +905,124 @@ function detectGutters(runs, pageWidth) {
|
|
|
559
905
|
});
|
|
560
906
|
}
|
|
561
907
|
const near = (band) => empty.some((e) => band.mid > e.from - fontSize && band.mid < e.to + fontSize);
|
|
562
|
-
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);
|
|
909
|
+
}
|
|
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
|
+
});
|
|
563
994
|
}
|
|
564
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
|
+
/**
|
|
565
1026
|
* The candidate gutters that actually separate something, left to right.
|
|
566
1027
|
*
|
|
567
1028
|
* A gutter with nothing on one side of it is a margin, and two of them with a
|
|
@@ -815,6 +1276,13 @@ function assignColumns(runs, gutters) {
|
|
|
815
1276
|
for (const run of row) columnOf.set(run, SPANNING_COLUMN);
|
|
816
1277
|
breaks.push(Math.max(...row.map((r) => r.y)));
|
|
817
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
|
+
}
|
|
818
1286
|
return {
|
|
819
1287
|
columnOf,
|
|
820
1288
|
breaks: breaks.sort((a, b) => b - a)
|
|
@@ -967,7 +1435,7 @@ function rotation60kOf(angleDeg) {
|
|
|
967
1435
|
* 0.05, and thirty at 0.05 em or more. A twentieth of an em sits in that gap.
|
|
968
1436
|
*/
|
|
969
1437
|
var BASELINE_STEP_EM = .05;
|
|
970
|
-
function groupIntoLines(runs, split = false, stepped = false) {
|
|
1438
|
+
function groupIntoLines(runs, split = false, stepped = false, spaces, rules = []) {
|
|
971
1439
|
const sorted = [...runs].sort((a, b) => b.y - a.y || a.x - b.x);
|
|
972
1440
|
const clusters = [];
|
|
973
1441
|
for (const run of sorted) {
|
|
@@ -975,6 +1443,7 @@ function groupIntoLines(runs, split = false, stepped = false) {
|
|
|
975
1443
|
const tol = Math.max(1, (run.fontSizePt || 10) * .5);
|
|
976
1444
|
if (last && Math.abs(last.y - run.y) <= tol) {
|
|
977
1445
|
last.runs.push(run);
|
|
1446
|
+
if ((run.fontSizePt || 0) > last.fontSize) last.y = run.y;
|
|
978
1447
|
last.fontSize = Math.max(last.fontSize, run.fontSizePt || 0);
|
|
979
1448
|
} else clusters.push({
|
|
980
1449
|
y: run.y,
|
|
@@ -983,11 +1452,11 @@ function groupIntoLines(runs, split = false, stepped = false) {
|
|
|
983
1452
|
});
|
|
984
1453
|
}
|
|
985
1454
|
for (const c of clusters) c.runs.sort((a, b) => a.x - b.x);
|
|
986
|
-
const stops = sharedStops(clusters.map((c) => c.runs));
|
|
1455
|
+
const stops = sharedStops(clusters.map((c) => c.runs), rules);
|
|
987
1456
|
return clusters.flatMap((c) => {
|
|
988
1457
|
const ordered = c.runs;
|
|
989
1458
|
const fontSize = c.fontSize || 10;
|
|
990
|
-
if (!split) return [lineOf(ordered, c.y, fontSize, stepped, stops)];
|
|
1459
|
+
if (!split) return [lineOf(ordered, c.y, fontSize, stepped, stops, spaces)];
|
|
991
1460
|
const pieces = [[]];
|
|
992
1461
|
for (const run of ordered) {
|
|
993
1462
|
const prev = pieces[pieces.length - 1];
|
|
@@ -996,10 +1465,35 @@ function groupIntoLines(runs, split = false, stepped = false) {
|
|
|
996
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([]);
|
|
997
1466
|
pieces[pieces.length - 1].push(run);
|
|
998
1467
|
}
|
|
999
|
-
return pieces.map((piece) => lineOf(piece, piece[0].y, Math.max(...piece.map((r) => r.fontSizePt || 0)) || fontSize, stepped, stops));
|
|
1468
|
+
return pieces.map((piece) => lineOf(piece, piece[0].y, Math.max(...piece.map((r) => r.fontSizePt || 0)) || fontSize, stepped, stops, spaces));
|
|
1000
1469
|
});
|
|
1001
1470
|
}
|
|
1002
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
|
+
/**
|
|
1003
1497
|
* §17.3.1.38 — the stops a block of lines is set out on: where text resumes,
|
|
1004
1498
|
* after a gap wider than a word space, at the same place on more than one line
|
|
1005
1499
|
* — and on at least one of them after a gap only a tab makes.
|
|
@@ -1010,21 +1504,32 @@ function groupIntoLines(runs, split = false, stepped = false) {
|
|
|
1010
1504
|
* other two came back with their values a word's width after the label
|
|
1011
1505
|
* instead of in the column.
|
|
1012
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
|
+
*
|
|
1013
1513
|
* @param lines Each line's runs, left to right.
|
|
1514
|
+
* @param rules The rules drawn down the block (see {@link columnRules}).
|
|
1014
1515
|
* @returns The x of every stop two lines share.
|
|
1015
1516
|
*/
|
|
1016
|
-
function sharedStops(lines) {
|
|
1517
|
+
function sharedStops(lines, rules = []) {
|
|
1017
1518
|
const seen = [];
|
|
1018
1519
|
lines.forEach((runs, line) => {
|
|
1019
1520
|
let end;
|
|
1020
1521
|
for (const run of runs) {
|
|
1021
1522
|
if (run.text.replaceAll("�", "").trim() === "") continue;
|
|
1022
1523
|
const size = run.fontSizePt || 10;
|
|
1023
|
-
if (end !== void 0 && run.x - end >= size * STOP_GAP_EM)
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
+
}
|
|
1028
1533
|
end = Math.max(end ?? run.endX, run.endX);
|
|
1029
1534
|
}
|
|
1030
1535
|
});
|
|
@@ -1037,6 +1542,23 @@ function sharedStops(lines) {
|
|
|
1037
1542
|
}
|
|
1038
1543
|
/** The least gap, in ems, that lands a run on a stop the lines around it share. */
|
|
1039
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;
|
|
1040
1562
|
/** How far from a shared stop a run may start and still stand on it. */
|
|
1041
1563
|
var STOP_SLACK_PT = .75;
|
|
1042
1564
|
/**
|
|
@@ -1070,9 +1592,154 @@ function inkSpan(runs) {
|
|
|
1070
1592
|
width: Math.max(0, last.endX - trail - x)
|
|
1071
1593
|
};
|
|
1072
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;
|
|
1073
1740
|
/** One run of runs, left to right on a shared baseline, as a {@link Line}. */
|
|
1074
|
-
function lineOf(runs, y, fontSize, stepped, shared = []) {
|
|
1075
|
-
const { spans: ordered, stops, pieces } = lineSpans(runs, fontSize, stepped || runs.length > 1 && !runs.some((r) => SPACE.test(r.text)), shared);
|
|
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);
|
|
1076
1743
|
const spans = ordered.every((s) => s.text.trim() === "" || isRightToLeft(s.text)) ? [...ordered].reverse() : ordered;
|
|
1077
1744
|
const ink = inkSpan(runs);
|
|
1078
1745
|
return {
|
|
@@ -1085,6 +1752,7 @@ function lineOf(runs, y, fontSize, stepped, shared = []) {
|
|
|
1085
1752
|
stops,
|
|
1086
1753
|
pieces
|
|
1087
1754
|
} : {},
|
|
1755
|
+
...isCode(runs) ? { code: true } : {},
|
|
1088
1756
|
text: spans.map((s) => s.text).join("").replace(/\s+/g, " ").trim(),
|
|
1089
1757
|
spans
|
|
1090
1758
|
};
|
|
@@ -1138,10 +1806,25 @@ var TAB_GAP_EM = 2.5;
|
|
|
1138
1806
|
* {@link stepsBetweenWords}). The tight one still clears the gaps a producer
|
|
1139
1807
|
* leaves INSIDE a word when it splits one for kerning, which measure eight
|
|
1140
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".
|
|
1141
1815
|
*/
|
|
1142
1816
|
function spaceGap(prev, fontSize, stepped) {
|
|
1143
|
-
|
|
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);
|
|
1144
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;
|
|
1145
1828
|
/**
|
|
1146
1829
|
* §17.3.1.25 — one character of a LEADER, the dotted rule that carries the eye
|
|
1147
1830
|
* across a table of contents.
|
|
@@ -1156,6 +1839,17 @@ function spaceGap(prev, fontSize, stepped) {
|
|
|
1156
1839
|
function isLeader(text) {
|
|
1157
1840
|
return text.length === 1 && LEADER_CHARS.has(text);
|
|
1158
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
|
+
}
|
|
1159
1853
|
var LEADER_CHARS = new Set([
|
|
1160
1854
|
".",
|
|
1161
1855
|
"·",
|
|
@@ -1180,34 +1874,47 @@ function stepsBetweenWords(runs) {
|
|
|
1180
1874
|
if (runs.length < 8) return false;
|
|
1181
1875
|
return runs.filter((r) => /\s/u.test(r.text)).length / runs.length < .05;
|
|
1182
1876
|
}
|
|
1183
|
-
function lineSpans(runs, fontSize, stepped, shared = []) {
|
|
1877
|
+
function lineSpans(runs, fontSize, stepped, shared = [], spaces) {
|
|
1184
1878
|
const spans = [];
|
|
1185
1879
|
const stops = [];
|
|
1186
1880
|
const pieces = [{
|
|
1187
1881
|
from: Infinity,
|
|
1188
1882
|
to: -Infinity
|
|
1189
1883
|
}];
|
|
1884
|
+
const table = isTableRow(runs);
|
|
1885
|
+
const code = isCode(runs);
|
|
1190
1886
|
const body = runs.filter((r) => (r.fontSizePt || fontSize) > fontSize * SCRIPT_SIZE);
|
|
1191
1887
|
const baseline = median((body.length > 0 ? body : runs).map((r) => r.y));
|
|
1192
1888
|
let prev;
|
|
1193
1889
|
let inked;
|
|
1194
1890
|
for (const [i, run] of runs.entries()) {
|
|
1195
|
-
if (run.text.trim() === "" &&
|
|
1891
|
+
if (run.text.trim() === "" && inked !== void 0) {
|
|
1196
1892
|
const next = runs.slice(i + 1).find((r) => r.text.trim() !== "");
|
|
1197
|
-
if (next !== void 0 && next.x - inked < spaceGap(next, fontSize, true)) continue;
|
|
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;
|
|
1198
1894
|
}
|
|
1199
1895
|
if (run.text.replaceAll("�", "") === "" && run.text !== "") continue;
|
|
1200
|
-
if (prev !== void 0 && run.x -
|
|
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
|
+
}
|
|
1901
|
+
spans.push({ text: " " });
|
|
1902
|
+
stops.push(run.x);
|
|
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)) {
|
|
1201
1908
|
const size = run.fontSizePt || fontSize;
|
|
1202
1909
|
const onStop = inked !== void 0 && run.x - inked >= size * STOP_GAP_EM && shared.some((x) => Math.abs(x - run.x) <= STOP_SLACK_PT);
|
|
1203
|
-
if (run.x - prev.endX >= size * TAB_GAP_EM || onStop) {
|
|
1910
|
+
if (run.x - prev.endX >= size * TAB_GAP_EM || onStop || aligned(prev, run) || table && run.x - prev.endX >= size * CELL_GAP_EM) {
|
|
1204
1911
|
spans.push({ text: " " });
|
|
1205
1912
|
stops.push(run.x);
|
|
1206
1913
|
pieces.push({
|
|
1207
1914
|
from: Infinity,
|
|
1208
1915
|
to: -Infinity
|
|
1209
1916
|
});
|
|
1210
|
-
} else spans.push(spaceAfter(spans[spans.length - 1]));
|
|
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));
|
|
1211
1918
|
}
|
|
1212
1919
|
spans.push({
|
|
1213
1920
|
text: run.text.replaceAll("�", ""),
|
|
@@ -1239,6 +1946,57 @@ function lineSpans(runs, fontSize, stepped, shared = []) {
|
|
|
1239
1946
|
};
|
|
1240
1947
|
}
|
|
1241
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)
|
|
1987
|
+
};
|
|
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;
|
|
1999
|
+
/**
|
|
1242
2000
|
* §17.3.2.42 — a run set off the line's baseline, and smaller, as the script it
|
|
1243
2001
|
* is.
|
|
1244
2002
|
*
|
|
@@ -1308,19 +2066,25 @@ var RULE_CHARS = 3;
|
|
|
1308
2066
|
function groupIntoParagraphs(lines, column, pageHeight = 0, before, eachLine = false, reach) {
|
|
1309
2067
|
const groups = [];
|
|
1310
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;
|
|
1311
2072
|
let prev;
|
|
2073
|
+
let beforePrev;
|
|
1312
2074
|
for (const line of lines) {
|
|
1313
2075
|
const gap = prev !== void 0 ? prev.y - line.y : 0;
|
|
1314
2076
|
const opened = prev !== void 0 && gap > line.fontSize * 1.5;
|
|
1315
|
-
|
|
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)) {
|
|
1316
2079
|
groups.push([]);
|
|
1317
2080
|
gaps.push(prev === void 0 ? before !== void 0 ? before.y - line.y : 0 : gap);
|
|
1318
2081
|
}
|
|
1319
2082
|
groups[groups.length - 1].push(line);
|
|
2083
|
+
beforePrev = prev;
|
|
1320
2084
|
prev = line;
|
|
1321
2085
|
}
|
|
1322
2086
|
const starts = lines.map((l) => l.x).sort((a, b) => a - b);
|
|
1323
|
-
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;
|
|
1324
2088
|
const heights = [];
|
|
1325
2089
|
return groups.map((g, i) => {
|
|
1326
2090
|
const first = g[0];
|
|
@@ -1345,10 +2109,10 @@ function groupIntoParagraphs(lines, column, pageHeight = 0, before, eachLine = f
|
|
|
1345
2109
|
top: first.y,
|
|
1346
2110
|
bottom: g[g.length - 1].y,
|
|
1347
2111
|
...stops.length > 0 ? {
|
|
1348
|
-
stops: stops.map((x) => x -
|
|
2112
|
+
stops: stops.map((x) => x - columnLeft),
|
|
1349
2113
|
pieces: (first.pieces ?? []).map((p) => ({
|
|
1350
|
-
from: p.from -
|
|
1351
|
-
to: p.to -
|
|
2114
|
+
from: p.from - columnLeft,
|
|
2115
|
+
to: p.to - columnLeft
|
|
1352
2116
|
}))
|
|
1353
2117
|
} : {},
|
|
1354
2118
|
...spacingBefore !== void 0 ? { spacingBefore } : {},
|
|
@@ -1356,12 +2120,87 @@ function groupIntoParagraphs(lines, column, pageHeight = 0, before, eachLine = f
|
|
|
1356
2120
|
...aligned,
|
|
1357
2121
|
...indentOf(g, columnLeft, aligned.alignment),
|
|
1358
2122
|
lineCount: g.length,
|
|
2123
|
+
left: Math.min(...g.map((l) => l.x)),
|
|
1359
2124
|
right: Math.max(...g.map((l) => l.x + l.width)),
|
|
1360
2125
|
...opened > most ? { far: g } : {}
|
|
1361
2126
|
};
|
|
1362
2127
|
});
|
|
1363
2128
|
}
|
|
1364
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
|
+
/**
|
|
1365
2204
|
* Whether the first word of `next` would have fit on `prev` short of `reach` —
|
|
1366
2205
|
* so the page did not break `prev` for want of room, and ended it.
|
|
1367
2206
|
*
|
|
@@ -1375,6 +2214,66 @@ function roomForWord(prev, next, reach) {
|
|
|
1375
2214
|
const width = next.width * word.length / text.length;
|
|
1376
2215
|
return reach - (prev.x + prev.width) > width + prev.fontSize * WORD_SPACE_EM;
|
|
1377
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;
|
|
1378
2277
|
/** A word space, in ems: what stands between a line's end and the word put after it. */
|
|
1379
2278
|
var WORD_SPACE_EM = .3;
|
|
1380
2279
|
/**
|
|
@@ -1382,6 +2281,7 @@ var WORD_SPACE_EM = .3;
|
|
|
1382
2281
|
* line of text set to one (see {@link groupIntoParagraphs}).
|
|
1383
2282
|
*/
|
|
1384
2283
|
function leadingLine(el) {
|
|
2284
|
+
if (el.kind === "shape" && el.shape.float === void 0) return 0;
|
|
1385
2285
|
if (el.kind !== "paragraph" || el.paragraph.runs.length === 0) return void 0;
|
|
1386
2286
|
const { spacingLine, spacingLineRule } = el.paragraph.properties;
|
|
1387
2287
|
return spacingLineRule === "exact" && spacingLine !== void 0 && spacingLine > 0 ? spacingLine : void 0;
|
|
@@ -1403,6 +2303,11 @@ var SPACING_NOISE_PT = .25;
|
|
|
1403
2303
|
* "typical two-column docu ment incorporating tables, figures and mathemat
|
|
1404
2304
|
* ics" — the soft hyphens dropped by the page and a space in their place.
|
|
1405
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
|
+
*
|
|
1406
2311
|
* @param lines The paragraph's lines, in order.
|
|
1407
2312
|
*/
|
|
1408
2313
|
function joinLines(lines) {
|
|
@@ -1412,12 +2317,13 @@ function joinLines(lines) {
|
|
|
1412
2317
|
const prev = out[out.length - 1];
|
|
1413
2318
|
const ends = prev?.text ?? "";
|
|
1414
2319
|
const soft = ends.endsWith(SOFT_HYPHEN);
|
|
1415
|
-
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);
|
|
1416
2322
|
if (soft && prev) out[out.length - 1] = {
|
|
1417
2323
|
...prev,
|
|
1418
2324
|
text: ends.slice(0, -1)
|
|
1419
2325
|
};
|
|
1420
|
-
else if (!hard && !/\s$/u.test(ends) && !/^\s/u.test(line.spans[0]?.text ?? "")) out.push(spaceAfter(prev));
|
|
2326
|
+
else if (!hard && !/\s$/u.test(ends) && !/^\s/u.test(line.spans[0]?.text ?? "")) out.push(spaceAfter(prev, line.spans[0]));
|
|
1421
2327
|
}
|
|
1422
2328
|
out.push(...line.spans);
|
|
1423
2329
|
});
|
|
@@ -1431,11 +2337,36 @@ var HYPHENS = new Set([
|
|
|
1431
2337
|
"‐",
|
|
1432
2338
|
"‑"
|
|
1433
2339
|
]);
|
|
2340
|
+
/** The dashes, en and em, that belong to the words either side where closed up to them. */
|
|
2341
|
+
var DASHES = new Set(["–", "—"]);
|
|
1434
2342
|
/** How many lines' worth of indent still reads as a first line, not a placement. */
|
|
1435
2343
|
var INDENT_LINES = 3;
|
|
1436
2344
|
/** Where in the run of line starts the column's own left edge is looked for. */
|
|
1437
2345
|
var COLUMN_LEFT_QUANTILE = .15;
|
|
1438
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
|
+
/**
|
|
1439
2370
|
* §17.3.1.12 `w:ind` — how far a paragraph is set in from its column, and where
|
|
1440
2371
|
* its first line begins.
|
|
1441
2372
|
*
|
|
@@ -1646,9 +2577,9 @@ function runningFoot(pageRuns, shown, where) {
|
|
|
1646
2577
|
const feet = pageRuns.map((runs, i) => edgeLine(runs.filter((r) => !tooSmallToRead(r, textSize)), shown[i]?.height ?? 0, where));
|
|
1647
2578
|
const found = feet.filter((f) => f !== void 0);
|
|
1648
2579
|
if (found.length < 2 || found.length < pageRuns.length * FOOT_SHARE) return void 0;
|
|
1649
|
-
const
|
|
1650
|
-
const mid = median(
|
|
1651
|
-
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;
|
|
1652
2583
|
const texts = found.map((f) => f.runs.map((r) => r.text).join("").trim());
|
|
1653
2584
|
return {
|
|
1654
2585
|
lift: feet.map((f) => new Set(f?.runs ?? [])),
|
|
@@ -1656,7 +2587,107 @@ function runningFoot(pageRuns, shown, where) {
|
|
|
1656
2587
|
numbered: new Set(texts).size > 1
|
|
1657
2588
|
};
|
|
1658
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;
|
|
1659
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
|
+
}
|
|
1660
2691
|
var FOOT_SHARE = .6;
|
|
1661
2692
|
/** How far, in points, a running foot may drift from page to page. */
|
|
1662
2693
|
var FOOT_DRIFT = 4;
|
|
@@ -1729,11 +2760,12 @@ var FOOT_CHARS = 90;
|
|
|
1729
2760
|
* the number of the page it is drawn on, which is what lets ONE band serve
|
|
1730
2761
|
* every page. The run is cut around it and the middle becomes the field.
|
|
1731
2762
|
*
|
|
1732
|
-
* @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.
|
|
1733
2765
|
* @returns The run, or the two or three it is cut into.
|
|
1734
2766
|
*/
|
|
1735
|
-
function pageNumbered(run) {
|
|
1736
|
-
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);
|
|
1737
2769
|
if (!found || run.field !== void 0) return [run];
|
|
1738
2770
|
const at = found.index + found[1].length;
|
|
1739
2771
|
const number = found[2];
|
|
@@ -1789,8 +2821,9 @@ function pageNumbered(run) {
|
|
|
1789
2821
|
* @param stepped Whether the page steps between its words.
|
|
1790
2822
|
* @param measure The measure it was set across, for its alignment.
|
|
1791
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.
|
|
1792
2825
|
*/
|
|
1793
|
-
function footerBand(runs, stepped, measure, numbered) {
|
|
2826
|
+
function footerBand(runs, stepped, measure, numbered, numeral) {
|
|
1794
2827
|
const fontSize = median(runs.map((r) => r.fontSizePt).filter((s) => s > 0)) || 10;
|
|
1795
2828
|
return rowsOf(runs, fontSize).map((row) => bandLine(row, fontSize, stepped, measure)).filter((line) => line !== void 0).map(({ spans, properties }) => {
|
|
1796
2829
|
const el = paragraphFromRuns(spans, void 0, properties);
|
|
@@ -1799,7 +2832,7 @@ function footerBand(runs, stepped, measure, numbered) {
|
|
|
1799
2832
|
kind: "paragraph",
|
|
1800
2833
|
paragraph: {
|
|
1801
2834
|
...el.paragraph,
|
|
1802
|
-
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
|
|
1803
2836
|
}
|
|
1804
2837
|
};
|
|
1805
2838
|
});
|
|
@@ -1835,7 +2868,7 @@ function bandLine(row, fontSize, stepped, measure) {
|
|
|
1835
2868
|
const width = measure ? measure.right - measure.left : 0;
|
|
1836
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;
|
|
1837
2870
|
return {
|
|
1838
|
-
spans: lines.flatMap((line, i) => i === 0 ? line.spans : [spaceAfter(lines[i - 1].spans.at(-1)), ...line.spans]),
|
|
2871
|
+
spans: lines.flatMap((line, i) => i === 0 ? line.spans : [spaceAfter(lines[i - 1].spans.at(-1), line.spans[0]), ...line.spans]),
|
|
1839
2872
|
properties: alignment ? { alignment } : {}
|
|
1840
2873
|
};
|
|
1841
2874
|
}
|
|
@@ -1957,9 +2990,11 @@ function proseColumns(runs, gutters, edges) {
|
|
|
1957
2990
|
for (const x of xs) most = Math.max(most, xs.filter((y) => Math.abs(y - x) <= FLUSH_PT).length);
|
|
1958
2991
|
return most;
|
|
1959
2992
|
};
|
|
1960
|
-
return edgesIn.every((lines) => {
|
|
2993
|
+
return edgesIn.every((lines, i) => {
|
|
1961
2994
|
if (lines.length < MIN_PROSE_LINES) return false;
|
|
1962
|
-
|
|
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;
|
|
1963
2998
|
});
|
|
1964
2999
|
}
|
|
1965
3000
|
/**
|
|
@@ -2116,71 +3151,163 @@ function tabbedRows(paras, measure) {
|
|
|
2116
3151
|
to: Math.max(...cells.map((c) => c.to))
|
|
2117
3152
|
};
|
|
2118
3153
|
});
|
|
2119
|
-
const
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
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;
|
|
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);
|
|
2139
3200
|
const grid = edges.slice(0, -1).map((from, k) => pt(Math.max(edges[k + 1] - from, 1)));
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
widthType: "dxa",
|
|
2150
|
-
widthPt: pt(grid.reduce((sum, w) => sum + w, 0))
|
|
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)
|
|
2151
3210
|
},
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
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
|
|
2178
3255
|
});
|
|
3256
|
+
if (heading) out.set(i, null);
|
|
2179
3257
|
for (let k = i + 1; k < to; k++) out.set(k, null);
|
|
2180
3258
|
i = to;
|
|
2181
3259
|
}
|
|
2182
3260
|
return out;
|
|
2183
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;
|
|
2184
3311
|
/** How far apart the ends of a column's cells may stand and still be flush. */
|
|
2185
3312
|
var FLUSH_SLACK_PT = 2;
|
|
2186
3313
|
/** How many stops a line must stand on before a run of them is a table. */
|
|
@@ -2209,21 +3336,47 @@ function splitAtTabs(spans) {
|
|
|
2209
3336
|
* the rule introduces; where nothing follows closely enough, the block above
|
|
2210
3337
|
* takes it as a bottom one.
|
|
2211
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
|
+
*
|
|
2212
3345
|
* @param vectors The page's painted paths.
|
|
2213
3346
|
* @param blocks The blocks read off the page so far, which the rule joins.
|
|
2214
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.
|
|
2215
3349
|
* @returns The rules that became borders, and so must not be drawn again.
|
|
2216
3350
|
*/
|
|
2217
|
-
function ruleBorders(vectors, blocks, width) {
|
|
3351
|
+
function ruleBorders(vectors, blocks, width, colOf = () => 0) {
|
|
2218
3352
|
const given = /* @__PURE__ */ new Set();
|
|
2219
3353
|
const paragraphs = blocks.filter((b) => b.el.kind === "paragraph");
|
|
2220
|
-
|
|
3354
|
+
const tables = blocks.filter((b) => b.el.kind === "table");
|
|
3355
|
+
if (paragraphs.length === 0 && tables.length === 0) return given;
|
|
2221
3356
|
for (const band of collinear(vectors.filter((v) => flatBar(v)))) {
|
|
2222
3357
|
const v = band.pieces[0];
|
|
2223
3358
|
if (band.to - band.from < width * RULE_SHARE) continue;
|
|
2224
3359
|
const y = (v.minY + v.maxY) / 2;
|
|
2225
|
-
const
|
|
2226
|
-
|
|
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];
|
|
2227
3380
|
const side = below !== void 0 && y - below.top <= RULE_REACH_PT ? {
|
|
2228
3381
|
block: below,
|
|
2229
3382
|
edge: "top"
|
|
@@ -2232,11 +3385,6 @@ function ruleBorders(vectors, blocks, width) {
|
|
|
2232
3385
|
edge: "bottom"
|
|
2233
3386
|
} : void 0;
|
|
2234
3387
|
if (!side || side.block.el.kind !== "paragraph") continue;
|
|
2235
|
-
const border = {
|
|
2236
|
-
style: "single",
|
|
2237
|
-
width: pt(Math.max(v.lineWidth ?? v.maxY - v.minY, RULE_MIN_PT)),
|
|
2238
|
-
colorHex: v.strokeHex ?? v.fillHex ?? "000000"
|
|
2239
|
-
};
|
|
2240
3388
|
const { paragraph } = side.block.el;
|
|
2241
3389
|
const before = paragraph.properties.spacingBefore;
|
|
2242
3390
|
const room = side.edge === "top" && before !== void 0 ? { spacingBefore: pt(Math.max(0, before - border.width)) } : {};
|
|
@@ -2259,8 +3407,347 @@ function ruleBorders(vectors, blocks, width) {
|
|
|
2259
3407
|
}
|
|
2260
3408
|
return given;
|
|
2261
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;
|
|
2262
3621
|
/** How far from a paragraph a rule may stand and still belong to it. */
|
|
2263
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
|
+
}
|
|
2264
3751
|
/** The thinnest a border may be drawn and still be seen. */
|
|
2265
3752
|
var RULE_MIN_PT = .5;
|
|
2266
3753
|
/**
|
|
@@ -2322,6 +3809,8 @@ var RULE_RATIO = 20;
|
|
|
2322
3809
|
var FLUSH_PT = 1;
|
|
2323
3810
|
/** And how many lines it takes before a region is a column at all. */
|
|
2324
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;
|
|
2325
3814
|
/**
|
|
2326
3815
|
* §17.4.38 — the page's rows as the TABLE they are.
|
|
2327
3816
|
*
|
|
@@ -2513,4 +4002,4 @@ var TABLE_ALIGNED_SHARE = .6;
|
|
|
2513
4002
|
*/
|
|
2514
4003
|
var TABLE_CELL_FILL = .65;
|
|
2515
4004
|
//#endregion
|
|
2516
|
-
export { endedParagraph, reconstructByLayout };
|
|
4005
|
+
export { FOOTER_PART, HEADER_PART, endedParagraph, footerBand, numberedFrom, numberingOf, pageTextEdges, reconstructByLayout, runningFoot, stepsBetweenWords };
|