reamkit 1.29.0 → 1.31.0

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