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
@@ -2,6 +2,7 @@ import { pt } from "../core/ir/units.js";
2
2
  import { ResourceStore } from "../core/ir/resources.js";
3
3
  import { EMPTY_STYLE_SHEET, resolveBodyStyles } from "../core/style-cascade/resolver.js";
4
4
  import "../core/style-cascade/index.js";
5
+ import { gradientOverBox } from "./shading.js";
5
6
  import { displayOf } from "./display.js";
6
7
  //#region src/pdf-reader/flow-build.ts
7
8
  /**
@@ -21,6 +22,35 @@ function paragraphBlock(text, outlineLevel) {
21
22
  }
22
23
  };
23
24
  }
25
+ /** One piece of reconstructed text, carrying any hyperlink (E-PDF EP8). */
26
+ /**
27
+ * The space set between a span and what follows it, in the span's own size and
28
+ * face: a word space is as wide as the type it stands in.
29
+ *
30
+ * Written bare, the space took the document's default size, whatever the words
31
+ * around it were set at: issue10665_reduced.pdf sets "78" and "110" twenty
32
+ * points apart in 60-point type, and the eleven-point space between them closed
33
+ * them up to "78110"; in 7-point footnotes the same space is half as wide
34
+ * again as the page's, and pushes their lines over.
35
+ *
36
+ * A space between two words one text markup marks is marked with them: the
37
+ * band a highlighter lays runs across the gap. comments.pdf highlights two
38
+ * lines on its sixth page, and with its spaces left bare the band came back
39
+ * broken at every word.
40
+ *
41
+ * @param before The span the space follows, where there is one.
42
+ * @param after What follows the space, where anything does.
43
+ * @returns The space.
44
+ */
45
+ function spaceAfter(before, after) {
46
+ const markup = before?.markup;
47
+ return {
48
+ text: " ",
49
+ ...before?.sizePt !== void 0 ? { sizePt: before.sizePt } : {},
50
+ ...before?.fontName !== void 0 ? { fontName: before.fontName } : {},
51
+ ...markup !== void 0 && after?.markup !== void 0 && sameMarkup(markup, after.markup) ? { markup } : {}
52
+ };
53
+ }
24
54
  /**
25
55
  * Build a paragraph {@link BodyElement} from positioned {@link TextSpan}s,
26
56
  * coalescing consecutive spans that share an `href` into one run (so a link
@@ -31,7 +61,7 @@ function paragraphFromRuns(spans, outlineLevel, placement) {
31
61
  const merged = [];
32
62
  for (const s of spans) {
33
63
  const last = merged[merged.length - 1];
34
- if (last && last.href === s.href && last.sizePt === s.sizePt && last.colorHex === s.colorHex && last.fontName === s.fontName && last.outline?.colorHex === s.outline?.colorHex && last.outline?.widthPt === s.outline?.widthPt && last.bold === s.bold && last.italic === s.italic && last.script === s.script && sameMarkup(last.markup, s.markup)) last.text += s.text;
64
+ if (last && last.href === s.href && last.sizePt === s.sizePt && last.colorHex === s.colorHex && last.fontName === s.fontName && last.outline?.colorHex === s.outline?.colorHex && last.outline?.widthPt === s.outline?.widthPt && last.bold === s.bold && last.italic === s.italic && last.script === s.script && last.widthScale === s.widthScale && sameMarkup(last.markup, s.markup)) last.text += s.text;
35
65
  else merged.push({
36
66
  text: s.text,
37
67
  ...s.href !== void 0 ? { href: s.href } : {},
@@ -42,7 +72,8 @@ function paragraphFromRuns(spans, outlineLevel, placement) {
42
72
  ...s.bold !== void 0 ? { bold: s.bold } : {},
43
73
  ...s.italic !== void 0 ? { italic: s.italic } : {},
44
74
  ...s.script !== void 0 ? { script: s.script } : {},
45
- ...s.markup !== void 0 ? { markup: s.markup } : {}
75
+ ...s.markup !== void 0 ? { markup: s.markup } : {},
76
+ ...s.widthScale !== void 0 ? { widthScale: s.widthScale } : {}
46
77
  });
47
78
  }
48
79
  const runs = merged.map((m) => ({
@@ -65,7 +96,10 @@ function paragraphFromRuns(spans, outlineLevel, placement) {
65
96
  properties: {
66
97
  ...r.sizePt !== void 0 ? { fontSizePt: pt(r.sizePt) } : {},
67
98
  ...r.colorHex !== void 0 ? { colorHex: r.colorHex } : {},
68
- ...r.fontName !== void 0 ? { fontFamily: { ascii: r.fontName } } : {},
99
+ ...r.fontName !== void 0 ? { fontFamily: {
100
+ ascii: r.fontName,
101
+ hAnsi: r.fontName
102
+ } } : {},
69
103
  ...r.outline !== void 0 ? { textOutline: r.outline } : {},
70
104
  ...r.bold ? { bold: true } : {},
71
105
  ...r.italic ? { italic: true } : {},
@@ -73,7 +107,8 @@ function paragraphFromRuns(spans, outlineLevel, placement) {
73
107
  ...r.markup?.highlightHex !== void 0 ? { shadingColorHex: r.markup.highlightHex } : {},
74
108
  ...r.markup?.underline !== void 0 ? { underline: r.markup.underline } : {},
75
109
  ...r.markup?.underlineHex !== void 0 ? { underlineColorHex: r.markup.underlineHex } : {},
76
- ...r.markup?.strike === true ? { strike: true } : {}
110
+ ...r.markup?.strike === true ? { strike: true } : {},
111
+ ...r.widthScale !== void 0 ? { widthScale: r.widthScale } : {}
77
112
  },
78
113
  ...r.href ? { href: r.href } : {}
79
114
  }))
@@ -85,6 +120,23 @@ function sameMarkup(a, b) {
85
120
  return a?.highlightHex === b?.highlightHex && a?.underline === b?.underline && a?.underlineHex === b?.underlineHex && a?.strike === b?.strike;
86
121
  }
87
122
  /**
123
+ * The height of a line that carries nothing to read: one twip, the least
124
+ * §17.3.1.33 can state. Zero is not a height a writer states at all — it is
125
+ * read as "no line spacing given", and the reader's single spacing comes back.
126
+ */
127
+ var CARRIER_LINE_PT = pt(.05);
128
+ /**
129
+ * §17.3.1.33 — the paragraph a floating mark is anchored in, which takes no
130
+ * room: the mark stands where the page drew it and the line carrying it is on
131
+ * no page. Left at a reader's single spacing, every rule and fill an invoice
132
+ * draws was a blank line in the flow, and the text under it moved down a line
133
+ * for each.
134
+ */
135
+ var FLOAT_CARRIER = {
136
+ spacingLine: CARRIER_LINE_PT,
137
+ spacingLineRule: "exact"
138
+ };
139
+ /**
88
140
  * Store a {@link PdfImage}'s bytes (content-addressed dedup) and build the image
89
141
  * {@link BodyElement} that references them, sized in points from the placement
90
142
  * CTM. `alt` becomes the block's alt text when given.
@@ -112,9 +164,10 @@ function imageBlock(image, resources, alt, frame, zOrder, behind = false) {
112
164
  width: pt(image.widthPt),
113
165
  height: pt(image.heightPt),
114
166
  ...image.rotationDeg !== void 0 ? { rotation60k: Math.round(-image.rotationDeg * 6e4) } : {},
167
+ ...image.flipV === true ? { flipV: true } : {},
115
168
  ...image.crop ? { crop: image.crop } : {},
116
169
  ...image.alpha !== void 0 ? { alpha: image.alpha } : {},
117
- paragraphProperties: {},
170
+ paragraphProperties: float ? FLOAT_CARRIER : {},
118
171
  ...alt ? { altText: alt } : {}
119
172
  }
120
173
  };
@@ -138,7 +191,6 @@ function imageBlock(image, resources, alt, frame, zOrder, behind = false) {
138
191
  * @returns A shape carrying the text, anchored where the glyphs were.
139
192
  */
140
193
  function positionedText(spans, box, frame, zOrder, rotation60k) {
141
- const paragraph = paragraphFromRuns(spans);
142
194
  return {
143
195
  kind: "shape",
144
196
  shape: {
@@ -154,22 +206,134 @@ function positionedText(spans, box, frame, zOrder, rotation60k) {
154
206
  offsetPt: pt(Math.max(0, frame.top - box.y - box.height))
155
207
  }
156
208
  },
157
- width: pt(Math.max(1, box.width)),
158
- height: pt(Math.max(1, box.height)),
159
- ...rotation60k !== void 0 ? { transform: { rotation60k } } : {},
209
+ ...lineBox(spans, box, rotation60k),
210
+ paragraphProperties: FLOAT_CARRIER
211
+ }
212
+ };
213
+ }
214
+ /** A line of text in a box of its own, sized to the box, which nobody sees. */
215
+ function lineBox(spans, box, rotation60k, noWrap = false) {
216
+ const paragraph = paragraphFromRuns(spans, void 0, {
217
+ spacingLine: pt(Math.max(1, box.height)),
218
+ spacingLineRule: "exact"
219
+ });
220
+ return {
221
+ width: pt(Math.max(1, box.width)),
222
+ height: pt(Math.max(1, box.height)),
223
+ ...rotation60k !== void 0 ? { transform: { rotation60k } } : {},
224
+ geometry: {
225
+ kind: "preset",
226
+ preset: "rect"
227
+ },
228
+ fill: { kind: "none" },
229
+ text: {
230
+ content: [paragraph],
231
+ insetLeft: pt(0),
232
+ insetTop: pt(0),
233
+ insetRight: pt(0),
234
+ insetBottom: pt(0),
235
+ ...noWrap ? { noWrap: true } : {}
236
+ }
237
+ };
238
+ }
239
+ /** A path of a figure, drawn where the page drew it. */
240
+ function vectorMember(v) {
241
+ const el = shapeBlock(v);
242
+ if (el.kind !== "shape") throw new Error("a path is drawn as a shape");
243
+ return {
244
+ shape: el.shape,
245
+ left: v.minX,
246
+ top: v.maxY
247
+ };
248
+ }
249
+ /**
250
+ * A picture of a figure: a box filled with it (§20.1.8.14), which is what a
251
+ * `pic:pic` inside a group is — the shadows comments.pdf lays under each of
252
+ * its boxes.
253
+ */
254
+ function imageMember(image, resources) {
255
+ const turned = image.rotationDeg !== void 0 || image.flipV === true;
256
+ return {
257
+ shape: {
258
+ width: pt(image.widthPt),
259
+ height: pt(image.heightPt),
260
+ ...turned ? { transform: {
261
+ ...image.rotationDeg !== void 0 ? { rotation60k: Math.round(-image.rotationDeg * 6e4) } : {},
262
+ ...image.flipV === true ? { flipV: true } : {}
263
+ } } : {},
160
264
  geometry: {
161
265
  kind: "preset",
162
266
  preset: "rect"
163
267
  },
164
- fill: { kind: "none" },
165
- text: {
166
- content: [paragraph],
167
- insetLeft: pt(0),
168
- insetTop: pt(0),
169
- insetRight: pt(0),
170
- insetBottom: pt(0)
268
+ fill: {
269
+ kind: "picture",
270
+ imageResource: resources.put(image.bytes),
271
+ ...image.crop ? { imageCrop: image.crop } : {},
272
+ ...image.alpha !== void 0 ? { alpha: image.alpha } : {}
171
273
  },
172
274
  paragraphProperties: {}
275
+ },
276
+ left: image.x,
277
+ top: image.y + image.heightPt
278
+ };
279
+ }
280
+ /**
281
+ * A label of a figure: a line of its words, in a box as wide as they are.
282
+ * The box never wraps — set in a face a little wider than the page's, one
283
+ * word would break in two.
284
+ */
285
+ function labelMember(spans, box, rotation60k) {
286
+ return {
287
+ shape: {
288
+ ...lineBox(spans, box, rotation60k, true),
289
+ paragraphProperties: {}
290
+ },
291
+ left: box.x,
292
+ top: box.y + box.height
293
+ };
294
+ }
295
+ /** The box a figure's parts stand in, on the page (y-up). */
296
+ function membersBox(members) {
297
+ return {
298
+ left: Math.min(...members.map((m) => m.left)),
299
+ top: Math.max(...members.map((m) => m.top)),
300
+ right: Math.max(...members.map((m) => m.left + m.shape.width)),
301
+ bottom: Math.min(...members.map((m) => m.top - m.shape.height))
302
+ };
303
+ }
304
+ /**
305
+ * §20.5.2.17 — a figure as the one drawing it is: a group holding its paths,
306
+ * its pictures and its labels at their places in it, set in a paragraph of
307
+ * its own. An inline drawing takes the room it is tall (a line holding
308
+ * nothing but a picture is the picture's height in Word), so the text after
309
+ * it follows it, and the figure goes with its paragraph wherever the text
310
+ * puts that.
311
+ *
312
+ * @param members Its parts, in the order they are painted.
313
+ * @param properties The paragraph it stands in.
314
+ */
315
+ function figureBlock(members, properties) {
316
+ const box = membersBox(members);
317
+ return {
318
+ kind: "shape",
319
+ shape: {
320
+ width: pt(box.right - box.left),
321
+ height: pt(box.top - box.bottom),
322
+ children: members.map((m) => ({
323
+ shape: m.shape,
324
+ xPt: pt(m.left - box.left),
325
+ yPt: pt(box.top - m.top)
326
+ })),
327
+ geometry: {
328
+ kind: "custom",
329
+ custom: {
330
+ pathWidth: 0,
331
+ pathHeight: 0,
332
+ commands: []
333
+ }
334
+ },
335
+ fill: { kind: "none" },
336
+ paragraphProperties: properties
173
337
  }
174
338
  };
175
339
  }
@@ -226,17 +390,21 @@ function shapeBlock(v, frame, zOrder, behind = false) {
226
390
  const alpha = v.alpha !== void 0 ? { alpha: v.alpha } : {};
227
391
  const fill = v.gradient !== void 0 ? {
228
392
  kind: "gradient",
229
- gradient: v.gradient,
393
+ gradient: gradientOverBox(v.gradient, v),
230
394
  ...alpha
231
395
  } : v.fillHex !== void 0 ? {
232
396
  kind: "solid",
233
397
  colorHex: v.fillHex,
234
398
  ...alpha
235
399
  } : { kind: "none" };
400
+ const dash = v.strokeHex !== void 0 && v.dash !== void 0 ? penDash(v.dash, pen) : [];
236
401
  const line = v.strokeHex !== void 0 ? {
237
402
  width: pt(pen),
238
403
  colorHex: v.strokeHex,
239
- fill: "solid"
404
+ fill: "solid",
405
+ ...dash.length > 0 ? { customDash: dash } : {},
406
+ cap: v.cap ?? "flat",
407
+ ...v.strokeAlpha !== void 0 ? { alpha: v.strokeAlpha } : {}
240
408
  } : void 0;
241
409
  const float = frame !== void 0 ? {
242
410
  wrap: "none",
@@ -267,7 +435,7 @@ function shapeBlock(v, frame, zOrder, behind = false) {
267
435
  },
268
436
  fill,
269
437
  ...line ? { line } : {},
270
- paragraphProperties: {}
438
+ paragraphProperties: float ? FLOAT_CARRIER : {}
271
439
  }
272
440
  };
273
441
  }
@@ -282,10 +450,10 @@ function shapeBlock(v, frame, zOrder, behind = false) {
282
450
  * this single geometry — a known approximation, still far better than a fixed
283
451
  * `A4`. Returns `undefined` when there is no usable first-page box.
284
452
  */
285
- function sectionFromPdfPages(pages) {
453
+ function sectionFromPdfPages(pages, display) {
286
454
  const first = pages[0];
287
455
  if (!first) return void 0;
288
- const shown = displayOf(first);
456
+ const shown = display ?? displayOf(first);
289
457
  const width = shown.width;
290
458
  const height = shown.height;
291
459
  if (!(width > 0 && height > 0)) return void 0;
@@ -306,6 +474,227 @@ function sectionFromPdfPages(pages) {
306
474
  };
307
475
  }
308
476
  /**
477
+ * §17.6.20 — a section read in a page's text frame (see `textFrameOf`), set
478
+ * back on the sheet the page is shown on, its lines running down it (`tbRl`).
479
+ *
480
+ * The frame is the sheet turned back a quarter, so its measurements carry
481
+ * over edge for edge: the frame's left margin is where each line starts, the
482
+ * sheet's top one; its top margin is where the first line stands, the
483
+ * sheet's right one.
484
+ *
485
+ * @param section The section as the frame measured it.
486
+ * @param display The geometry its pages were read in.
487
+ * @returns The section on the sheet, or the section unchanged for a page read
488
+ * on its sheet.
489
+ */
490
+ function sectionOnSheet(section, display) {
491
+ const sheet = display?.sheet;
492
+ if (!section || !sheet) return section;
493
+ const m = section.margins;
494
+ return {
495
+ ...section,
496
+ pageSize: {
497
+ width: pt(sheet.width),
498
+ height: pt(sheet.height),
499
+ orientation: sheet.width > sheet.height ? "landscape" : "portrait"
500
+ },
501
+ ...m ? { margins: {
502
+ ...m,
503
+ top: m.left,
504
+ right: m.top,
505
+ bottom: m.right,
506
+ left: m.bottom
507
+ } } : {},
508
+ textDirection: "tbRl"
509
+ };
510
+ }
511
+ /**
512
+ * A mark anchored to a page read in its text frame, set on the SHEET the page
513
+ * is shown on: where the page drew it, turned with the page.
514
+ *
515
+ * Word lays a `tbRl` section's text down the sheet but stands a drawing
516
+ * anchored to the page at its offsets on the sheet, upright (§17.6.20) — so a
517
+ * picture the frame placed is carried onto the sheet, and turned a quarter
518
+ * clockwise as the words around it are.
519
+ *
520
+ * @param el A body element, anchored or not.
521
+ * @param sheetWidth The sheet's width, which is the frame's height.
522
+ * @returns The element, anchored on the sheet.
523
+ */
524
+ function floatOntoSheet(el, sheetWidth) {
525
+ const onto = (float, width, height) => {
526
+ if (float.posH?.relativeFrom !== "page" || float.posV?.relativeFrom !== "page") return void 0;
527
+ const cx = (float.posH.offsetPt ?? 0) + width / 2;
528
+ const cy = (float.posV.offsetPt ?? 0) + height / 2;
529
+ return {
530
+ ...float,
531
+ posH: {
532
+ relativeFrom: "page",
533
+ offsetPt: pt(sheetWidth - cy - width / 2)
534
+ },
535
+ posV: {
536
+ relativeFrom: "page",
537
+ offsetPt: pt(cx - height / 2)
538
+ }
539
+ };
540
+ };
541
+ const quarter = (rotation60k) => ((rotation60k ?? 0) + 90 * 6e4) % (360 * 6e4);
542
+ if (el.kind === "image" && el.image.float) {
543
+ const float = onto(el.image.float, el.image.width, el.image.height);
544
+ if (!float) return el;
545
+ return {
546
+ ...el,
547
+ image: {
548
+ ...el.image,
549
+ float,
550
+ rotation60k: quarter(el.image.rotation60k)
551
+ }
552
+ };
553
+ }
554
+ if (el.kind === "shape" && el.shape.float) {
555
+ const float = onto(el.shape.float, el.shape.width, el.shape.height);
556
+ if (!float) return el;
557
+ const rotation60k = quarter(el.shape.transform?.rotation60k);
558
+ return {
559
+ ...el,
560
+ shape: {
561
+ ...el.shape,
562
+ float,
563
+ transform: {
564
+ ...el.shape.transform,
565
+ rotation60k
566
+ }
567
+ }
568
+ };
569
+ }
570
+ return el;
571
+ }
572
+ /**
573
+ * §8.4.3.6 — a page's dash pattern as DrawingML states one (§20.1.8.21
574
+ * `a:custDash`): dash and gap in turn, as multiples of the pen's width.
575
+ *
576
+ * An odd count of lengths runs twice before it repeats, so the dashes and the
577
+ * gaps trade places the second time round; written out once more, the pairs
578
+ * come out even. A dash of no length is a DOT, drawn by the pen's cap alone —
579
+ * which no .docx line carries — so it is set a pen's width long and the gap
580
+ * after it gives that back, keeping the pattern's period.
581
+ *
582
+ * @param dash The lengths in page-space points.
583
+ * @param pen The pen's width in points.
584
+ * @returns The pattern in pen widths, or an empty array for a solid line.
585
+ */
586
+ function penDash(dash, pen) {
587
+ if (dash.length === 0 || !(pen > 0)) return [];
588
+ const out = (dash.length % 2 === 1 ? [...dash, ...dash] : [...dash]).map((n) => n / pen);
589
+ for (let i = 0; i < out.length; i += 2) {
590
+ if (out[i] > 0) continue;
591
+ out[i] = 1;
592
+ out[i + 1] = Math.max(out[i + 1] - 1, MIN_DASH_GAP);
593
+ }
594
+ return out;
595
+ }
596
+ /** The least gap a dash pattern keeps, in pen widths, so a dotted line stays dotted. */
597
+ var MIN_DASH_GAP = .5;
598
+ /** What the measure gives back, so the widest line still fits when re-set. */
599
+ var SLACK = .01;
600
+ /** How much of the sheet a margin down the page may take. */
601
+ var DEEPEST_MARGIN = .5;
602
+ /**
603
+ * How far in, as a share of the sheet, a right margin the lines do not show
604
+ * may come: what a sheet of a line or two is re-set across is at least the
605
+ * rest of it.
606
+ */
607
+ var GUESSED_MARGIN = 1 / 3;
608
+ /** The narrowest measure a sheet's own lines may leave, as a share of the sheet. */
609
+ var NARROWEST_MEASURE = .2;
610
+ /**
611
+ * The lines a sheet's runs stand on — baselines further apart than half the
612
+ * size of the type on them — and how many of them END at the sheet's right
613
+ * edge, within an em of the farthest: the lines a measure broke, or a margin
614
+ * justified, rather than the ones their author stopped.
615
+ */
616
+ function linesOf(runs) {
617
+ const ink = runs.filter((r) => r.text.trim() !== "").map((r) => ({
618
+ y: r.y,
619
+ end: r.endX,
620
+ size: r.fontSizePt || 10
621
+ })).sort((a, b) => b.y - a.y);
622
+ const lines = [];
623
+ for (const at of ink) {
624
+ const last = lines[lines.length - 1];
625
+ if (last !== void 0 && last.y - at.y <= Math.max(last.size, at.size) / 2) {
626
+ last.end = Math.max(last.end, at.end);
627
+ last.size = Math.max(last.size, at.size);
628
+ } else lines.push({ ...at });
629
+ }
630
+ const edge = Math.max(...lines.map((l) => l.end));
631
+ return {
632
+ count: lines.length,
633
+ atEdge: lines.filter((l) => edge - l.end <= l.size).length
634
+ };
635
+ }
636
+ /**
637
+ * The white, in ems of the text, a running foot keeps above it — which is
638
+ * what the reader asked of it before it took the line for a foot at all.
639
+ */
640
+ var FOOT_CLEAR_EM = 2;
641
+ /**
642
+ * §17.3.1.33 — where the baseline of an EXACT line stands in its box, from the
643
+ * top. LibreOffice puts it at four fifths of the height; Word at the height
644
+ * less the face's descent, which for the faces documents use is within a tenth
645
+ * of a line of the same place.
646
+ */
647
+ var BASELINE_AT = .8;
648
+ /** A line's box where no pitch was measured: the ordinary single spacing, in ems. */
649
+ var NATURAL_LINE_EM = 1.2;
650
+ /**
651
+ * How far above its baseline the first line's BOX reaches, as a fraction of the
652
+ * size — which is what the top margin has to leave room for. The lines are set
653
+ * in exact boxes (see `groupIntoParagraphs`), so this is where the box's top
654
+ * stands, not where a face's ascender happens to.
655
+ */
656
+ var ASCENDER = BASELINE_AT * NATURAL_LINE_EM;
657
+ /**
658
+ * And how far below its baseline the last line's box reaches. A box set half
659
+ * as deep again as its size reaches three tenths of it down; measured to a
660
+ * face's descender instead, bug1337429.pdf's last line did not fit the sheet
661
+ * it was drawn on and went to a second one.
662
+ */
663
+ var DESCENDER = (1 - BASELINE_AT) * 1.5;
664
+ /**
665
+ * How small, against the document's own text, type may be set and still be
666
+ * read as text rather than as a mark the producer left on the sheet.
667
+ */
668
+ var LEGIBLE_SHARE = .25;
669
+ /**
670
+ * The size a document's text is set in: the middle of every size its runs
671
+ * carry.
672
+ *
673
+ * @param pageRuns Each page's runs.
674
+ * @returns The size, in points; 0 where no run states one.
675
+ */
676
+ function textSizeOf(pageRuns) {
677
+ const sizes = pageRuns.flat().map((r) => r.fontSizePt).filter((s) => s > 0).sort((a, b) => a - b);
678
+ return sizes[Math.floor(sizes.length / 2)] ?? 0;
679
+ }
680
+ /**
681
+ * Whether a run is set too small to be read, beside the text of its document —
682
+ * a mark the producer leaves on the sheet, not a line of the page.
683
+ *
684
+ * TCPDF signs the last page of everything it makes "Powered by TCPDF
685
+ * (www.tcpdf.org)" in type one point high, three points from the corner of the
686
+ * paper. Taken for text it was the leftmost and the lowest thing on the page:
687
+ * the margins came in at the edge of the sheet and every line of basicapi.pdf
688
+ * and alphatrans.pdf was set against it.
689
+ *
690
+ * @param run The run.
691
+ * @param textSize The size the document's text is set in (see {@link textSizeOf}).
692
+ * @returns `true` where the run is a mark rather than text.
693
+ */
694
+ function tooSmallToRead(run, textSize) {
695
+ return run.fontSizePt > 0 && run.fontSizePt < textSize * LEGIBLE_SHARE;
696
+ }
697
+ /**
309
698
  * The margins the SOURCE used, measured off where its words actually sit.
310
699
  *
311
700
  * A PDF states none — text is placed anywhere on the MediaBox — so the reader
@@ -322,18 +711,13 @@ function sectionFromPdfPages(pages) {
322
711
  * @param section The section the page box gave, or `undefined`.
323
712
  * @param shown Each page as it is shown, for its own width and height.
324
713
  * @param pageRuns Each page's runs, already placed on the shown page.
714
+ * @param pageMarks Each page's pictures, which the measure has to hold.
715
+ * @param foot The running foot lifted off the pages, as the first page
716
+ * showed it — the band the text block stands above.
325
717
  * @returns The section with measured margins, or `section` when nothing is
326
718
  * measurable.
327
719
  */
328
- /** What the measure gives back, so the widest line still fits when re-set. */
329
- var SLACK = .01;
330
- /** How much of the sheet a margin down the page may take. */
331
- var DEEPEST_MARGIN = .5;
332
- /** How far a face's ascender stands above its baseline, as a fraction of the size. */
333
- var ASCENDER = .8;
334
- /** And its descender below — the two together are a little over one em. */
335
- var DESCENDER = .22;
336
- function withMeasuredMargins(section, shown, pageRuns, pageMarks = []) {
720
+ function withMeasuredMargins(section, shown, pageRuns, pageMarks = [], foot = []) {
337
721
  if (!section?.pageSize) return section;
338
722
  const width = section.pageSize.width;
339
723
  const height = section.pageSize.height;
@@ -341,8 +725,10 @@ function withMeasuredMargins(section, shown, pageRuns, pageMarks = []) {
341
725
  const rights = [];
342
726
  const tops = [];
343
727
  const bottoms = [];
344
- pageRuns.forEach((runs, i) => {
728
+ const textSize = textSizeOf(pageRuns);
729
+ pageRuns.forEach((all, i) => {
345
730
  const page = shown[i];
731
+ const runs = all.filter((r) => r.annotation !== true && !tooSmallToRead(r, textSize));
346
732
  if (!page || runs.length === 0) return;
347
733
  let minX = Infinity;
348
734
  let maxX = -Infinity;
@@ -365,6 +751,7 @@ function withMeasuredMargins(section, shown, pageRuns, pageMarks = []) {
365
751
  topSize = r.fontSizePt;
366
752
  }
367
753
  }
754
+ const textEnd = maxX;
368
755
  for (const mark of pageMarks[i] ?? []) {
369
756
  if (!Number.isFinite(mark.x) || !Number.isFinite(mark.y)) continue;
370
757
  maxX = Math.max(maxX, mark.x + mark.widthPt);
@@ -372,7 +759,12 @@ function withMeasuredMargins(section, shown, pageRuns, pageMarks = []) {
372
759
  }
373
760
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) return;
374
761
  lefts.push(minX);
375
- rights.push(page.width - maxX);
762
+ const lines = linesOf(runs);
763
+ rights.push({
764
+ value: page.width - maxX,
765
+ full: lines.count >= 3 || maxX > textEnd,
766
+ edged: lines.atEdge >= 2 && maxX <= textEnd
767
+ });
376
768
  tops.push(page.height - maxY - topSize * ASCENDER);
377
769
  bottoms.push(minY - bottomSize * DESCENDER);
378
770
  });
@@ -382,34 +774,148 @@ function withMeasuredMargins(section, shown, pageRuns, pageMarks = []) {
382
774
  return s[Math.floor(s.length / 2)] ?? 0;
383
775
  };
384
776
  const clamp = (v, span, most = 1 / 3) => pt(Math.max(0, Math.min(v, span * most)));
777
+ const inked = foot.filter((r) => r.text.trim() !== "");
778
+ const footTop = Math.max(...inked.map((r) => r.y + r.fontSizePt * ASCENDER));
779
+ const footBottom = Math.min(...inked.map((r) => r.y - r.fontSizePt * DESCENDER));
780
+ const floor = inked.length > 0 ? footTop + textSize * FOOT_CLEAR_EM : Infinity;
781
+ const left = clamp(Math.min(...lefts), width);
782
+ const voters = rights.filter((r) => r.full);
783
+ const farthest = voters.length > 0 && voters.every((r) => r.edged) ? Math.max(width * GUESSED_MARGIN, width * (1 - NARROWEST_MEASURE) - left) : width * GUESSED_MARGIN;
385
784
  return {
386
785
  ...section,
387
786
  margins: {
388
- left: clamp(Math.min(...lefts), width),
389
- right: clamp(median(rights) - width * SLACK, width),
787
+ left,
788
+ right: pt(Math.max(0, Math.min(median((voters.length > 0 ? voters : rights).map((r) => r.value)) - width * SLACK, farthest))),
390
789
  top: clamp(Math.min(...tops), height, DEEPEST_MARGIN),
391
- bottom: clamp(Math.min(...bottoms) - height * SLACK, height, DEEPEST_MARGIN)
790
+ bottom: clamp(Math.min(...bottoms, floor) - height * SLACK, height, DEEPEST_MARGIN),
791
+ ...inked.length > 0 ? { footer: clamp(footBottom, height, DEEPEST_MARGIN) } : {}
392
792
  }
393
793
  };
394
794
  }
395
795
  /**
796
+ * The runs set in a face the page KERNED or LIGATED, marked so (see
797
+ * `pageSpacing`, `FaceOutlines.ligatures`): a word processor kerns nothing a
798
+ * run does not ask it to, and set unkerned, a line kerned on the page runs
799
+ * longer than it did there; a ligature the embedded face carries is formed
800
+ * where the run asks for its face's ligatures.
801
+ *
802
+ * @param blocks The blocks to mark, tables and text boxes within them too.
803
+ * @param typeset Run font name → what its runs ask for.
804
+ * @returns The blocks, their runs in those faces asking for it.
805
+ */
806
+ function typesetRuns(blocks, typeset) {
807
+ if (typeset.size === 0) return [...blocks];
808
+ const shape = (s) => ({
809
+ ...s,
810
+ ...s.text ? { text: {
811
+ ...s.text,
812
+ content: typesetRuns(s.text.content, typeset)
813
+ } } : {},
814
+ ...s.children ? { children: s.children.map((child) => ({
815
+ ...child,
816
+ shape: shape(child.shape)
817
+ })) } : {}
818
+ });
819
+ return blocks.map((el) => {
820
+ if (el.kind === "paragraph") {
821
+ const runs = el.paragraph.runs.map((run) => {
822
+ const face = run.properties.fontFamily?.ascii;
823
+ const asked = face !== void 0 ? typeset.get(face) : void 0;
824
+ return asked ? {
825
+ ...run,
826
+ properties: {
827
+ ...run.properties,
828
+ ...asked
829
+ }
830
+ } : run;
831
+ });
832
+ return {
833
+ ...el,
834
+ paragraph: {
835
+ ...el.paragraph,
836
+ runs
837
+ }
838
+ };
839
+ }
840
+ if (el.kind === "table") {
841
+ const rows = el.table.rows.map((row) => ({
842
+ ...row,
843
+ cells: row.cells.map((cell) => ({
844
+ ...cell,
845
+ content: typesetRuns(cell.content, typeset)
846
+ }))
847
+ }));
848
+ return {
849
+ ...el,
850
+ table: {
851
+ ...el.table,
852
+ rows
853
+ }
854
+ };
855
+ }
856
+ if (el.kind === "shape") return {
857
+ ...el,
858
+ shape: shape(el.shape)
859
+ };
860
+ return el;
861
+ });
862
+ }
863
+ /**
864
+ * What each face's runs ask for: kerning where the page kerned the face, and
865
+ * its standard ligatures where the page drew one of them (the ligatures the
866
+ * face's embedded font then forms are exactly the ones the page drew).
867
+ *
868
+ * @param kerned The run font names of the faces the page kerned.
869
+ * @param faceOutlines Run font name → the face's outlines, ligatures among them.
870
+ */
871
+ function typesetting(kerned, faceOutlines) {
872
+ const out = /* @__PURE__ */ new Map();
873
+ for (const face of kerned) out.set(face, { kerningMinPt: KERN_FROM_PT });
874
+ for (const [face, outlines] of faceOutlines ?? []) if (outlines.ligatures && outlines.ligatures.size > 0) out.set(face, {
875
+ ...out.get(face),
876
+ ligatures: "standard"
877
+ });
878
+ return out;
879
+ }
880
+ /** §17.3.2.19 — kerned from one point up: at every size a page is set in. */
881
+ var KERN_FROM_PT = pt(1);
882
+ /**
396
883
  * Assemble the final {@link FlowDoc} for a reconstruction: the body elements
397
884
  * with their styles resolved against the empty style sheet, the lifted-image
398
885
  * resource store, and the optional page {@link SectionProperties}. Shared by
399
886
  * both reconstruction paths (the tagged fast-path EP3 and the heuristic layout
400
887
  * path EP4).
401
888
  */
402
- function buildFlowDoc(body, resources = new ResourceStore(), section, embeddedFonts, sections = [], headersFooters) {
889
+ function buildFlowDoc(body, resources = new ResourceStore(), section, embeddedFonts, sections = [], headersFooters, faceFamilies, faceOutlines, kerned = /* @__PURE__ */ new Set()) {
890
+ const typeset = typesetting(kerned, faceOutlines);
891
+ const bands = headersFooters && typeset.size > 0 ? new Map([...headersFooters].map(([id, blocks]) => [id, typesetRuns(blocks, typeset)])) : headersFooters;
403
892
  return {
404
893
  kind: "flow",
405
- body: resolveBodyStyles([...body], EMPTY_STYLE_SHEET),
894
+ body: resolveBodyStyles(typesetRuns(body, typeset), RECONSTRUCTION_SHEET),
406
895
  sections,
407
- ...headersFooters && headersFooters.size > 0 ? { headersFooters } : {},
896
+ ...bands && bands.size > 0 ? { headersFooters: bands } : {},
408
897
  ...section ? { section } : {},
409
898
  ...embeddedFonts && embeddedFonts.size > 0 ? { embeddedFonts } : {},
899
+ ...faceFamilies && faceFamilies.size > 0 ? { faceFamilies } : {},
900
+ ...faceOutlines && faceOutlines.size > 0 ? { faceOutlines } : {},
901
+ compatibilityMode: CURRENT_WORD_LAYOUT,
410
902
  styles: EMPTY_STYLE_SHEET,
411
903
  resources
412
904
  };
413
905
  }
906
+ /** [MS-DOCX] `compatibilityMode` 15 — Word 2013's layout, a current Word's. */
907
+ var CURRENT_WORD_LAYOUT = 15;
908
+ /**
909
+ * §17.3.1.44 — the sheet a reconstruction's paragraphs resolve against: the
910
+ * empty one, with widow control off. Every source page opens a page of its
911
+ * own, and the page already broke its paragraphs where it did; where a
912
+ * reconstructed column runs a line long, widow control — on wherever nothing
913
+ * says otherwise — carries a second line over with it, and the column after
914
+ * runs longer still.
915
+ */
916
+ var RECONSTRUCTION_SHEET = {
917
+ ...EMPTY_STYLE_SHEET,
918
+ defaultParagraphProperties: { widowControl: false }
919
+ };
414
920
  //#endregion
415
- export { buildFlowDoc, dedupeLosses, imageBlock, paragraphBlock, paragraphFromRuns, positionedText, sectionFromPdfPages, shapeBlock, withMeasuredMargins };
921
+ export { ASCENDER, BASELINE_AT, CARRIER_LINE_PT, GUESSED_MARGIN, NATURAL_LINE_EM, buildFlowDoc, dedupeLosses, figureBlock, floatOntoSheet, imageBlock, imageMember, labelMember, membersBox, paragraphBlock, paragraphFromRuns, positionedText, sectionFromPdfPages, sectionOnSheet, shapeBlock, spaceAfter, textSizeOf, tooSmallToRead, vectorMember, withMeasuredMargins };