reamkit 1.16.0 → 1.18.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 (76) hide show
  1. package/dist/esm/core/bidi/segments.js +1 -2
  2. package/dist/esm/core/converter/project.js +5 -1
  3. package/dist/esm/core/document-model/index.d.ts +1 -1
  4. package/dist/esm/core/document-model/types.d.ts +526 -8
  5. package/dist/esm/core/drawingml/chart-geometry.d.ts +8 -12
  6. package/dist/esm/core/drawingml/chart-geometry.js +58 -9
  7. package/dist/esm/core/drawingml/chart-parser.js +44 -5
  8. package/dist/esm/core/drawingml/colors.d.ts +1 -1
  9. package/dist/esm/core/drawingml/colors.js +27 -1
  10. package/dist/esm/core/drawingml/preset-geometry.js +233 -0
  11. package/dist/esm/core/drawingml/shape-render.d.ts +12 -0
  12. package/dist/esm/core/drawingml/shape-render.js +116 -3
  13. package/dist/esm/core/drawingml/theme-parser.d.ts +10 -0
  14. package/dist/esm/core/drawingml/theme-parser.js +19 -1
  15. package/dist/esm/core/font/measure.d.ts +5 -0
  16. package/dist/esm/core/font/measure.js +21 -1
  17. package/dist/esm/core/images.d.ts +2 -2
  18. package/dist/esm/core/images.js +219 -0
  19. package/dist/esm/core/ir/flow.d.ts +23 -1
  20. package/dist/esm/core/lzw.d.ts +17 -0
  21. package/dist/esm/core/lzw.js +83 -0
  22. package/dist/esm/core/metafile/emf.d.ts +16 -0
  23. package/dist/esm/core/metafile/emf.js +635 -0
  24. package/dist/esm/core/metafile/picture.d.ts +106 -0
  25. package/dist/esm/core/metafile/picture.js +55 -0
  26. package/dist/esm/core/metafile/wmf.d.ts +9 -0
  27. package/dist/esm/core/metafile/wmf.js +371 -0
  28. package/dist/esm/core/numbering/apply.d.ts +5 -3
  29. package/dist/esm/core/numbering/apply.js +42 -11
  30. package/dist/esm/core/numbering/index.js +2 -0
  31. package/dist/esm/core/numbering/state.d.ts +13 -1
  32. package/dist/esm/core/numbering/state.js +256 -15
  33. package/dist/esm/core/po-helpers.js +16 -1
  34. package/dist/esm/core/style-cascade/resolver.js +66 -11
  35. package/dist/esm/core/style-cascade/table.js +35 -11
  36. package/dist/esm/core/style-cascade/types.d.ts +42 -1
  37. package/dist/esm/core/style-cascade/types.js +4 -0
  38. package/dist/esm/core/tiff.d.ts +20 -0
  39. package/dist/esm/core/tiff.js +339 -0
  40. package/dist/esm/core/vector.d.ts +22 -0
  41. package/dist/esm/excel/print-model.js +4 -2
  42. package/dist/esm/excel/sheet-to-flow.js +10 -8
  43. package/dist/esm/excel/worksheet-parser.js +1 -1
  44. package/dist/esm/html/html-writer.js +20 -5
  45. package/dist/esm/layout/page-doc.d.ts +107 -4
  46. package/dist/esm/layout/styled-layout.d.ts +44 -2
  47. package/dist/esm/layout/styled-layout.js +2330 -283
  48. package/dist/esm/pdf/shading.js +76 -6
  49. package/dist/esm/pdf/styled-page-emitter.js +294 -45
  50. package/dist/esm/pdf/vector-graphics.d.ts +29 -2
  51. package/dist/esm/pdf/vector-graphics.js +96 -12
  52. package/dist/esm/pdf-reader/image-decode.js +7 -75
  53. package/dist/esm/pptx/pptx-reader.js +1 -0
  54. package/dist/esm/pptx/slide-parser.d.ts +21 -4
  55. package/dist/esm/pptx/slide-parser.js +86 -23
  56. package/dist/esm/svg/svg-writer.js +2 -2
  57. package/dist/esm/word/document-parser.d.ts +96 -7
  58. package/dist/esm/word/document-parser.js +540 -116
  59. package/dist/esm/word/docx-reader.js +135 -43
  60. package/dist/esm/word/docx-writer.js +6 -1
  61. package/dist/esm/word/drawing-parser.d.ts +58 -5
  62. package/dist/esm/word/drawing-parser.js +1632 -63
  63. package/dist/esm/word/index.d.ts +4 -4
  64. package/dist/esm/word/numbering-parser.d.ts +2 -1
  65. package/dist/esm/word/numbering-parser.js +107 -21
  66. package/dist/esm/word/paragraph-properties.d.ts +11 -1
  67. package/dist/esm/word/paragraph-properties.js +219 -10
  68. package/dist/esm/word/run-properties.js +45 -1
  69. package/dist/esm/word/settings-parser.d.ts +24 -0
  70. package/dist/esm/word/settings-parser.js +31 -5
  71. package/dist/esm/word/shading.d.ts +11 -0
  72. package/dist/esm/word/shading.js +64 -0
  73. package/dist/esm/word/styles-parser.js +8 -2
  74. package/dist/esm/word/table-parser.d.ts +9 -1
  75. package/dist/esm/word/table-parser.js +182 -30
  76. package/package.json +1 -1
@@ -4,23 +4,26 @@ import { createFontMeasure } from "../core/font/measure.js";
4
4
  import { halfPtToPt, pt } from "../core/ir/units.js";
5
5
  import { ResourceStore } from "../core/ir/resources.js";
6
6
  import { prepareImage } from "../core/images.js";
7
+ import { PathBuilder, flipTransform } from "../core/vector.js";
8
+ import { isEmf, readEmf } from "../core/metafile/emf.js";
9
+ import { isWmf, readWmf } from "../core/metafile/wmf.js";
7
10
  import { segmentLevels } from "../core/bidi/segments.js";
8
11
  import { analyzeString, hasBidiCharacters } from "../core/bidi/index.js";
9
12
  import { FORCED_BREAK, breakLines } from "../core/line-breaker/knuth-plass.js";
10
13
  import { greedyBreakLines } from "../core/line-breaker/greedy.js";
11
14
  import { cjkBreakBetween, splitCjkSegment } from "../core/line-breaker/cjk.js";
12
15
  import "../core/line-breaker/index.js";
13
- import { applyNumbering, applyNumberingToHeadersFooters } from "../core/numbering/apply.js";
14
16
  import { DEFAULT_RESOLVED_PARAGRAPH, DEFAULT_RESOLVED_RUN } from "../core/style-cascade/types.js";
15
17
  import { resolveParagraphProperties, resolveRunProperties } from "../core/style-cascade/resolver.js";
16
18
  import "../core/style-cascade/index.js";
17
- import { PathBuilder, flipTransform } from "../core/vector.js";
19
+ import { applyNumbering, applyNumberingToHeadersFooters } from "../core/numbering/apply.js";
20
+ import "../core/numbering/index.js";
18
21
  import { buildSparkline } from "../core/drawingml/sparkline-geometry.js";
19
22
  import { arcPoint, arcToBeziers } from "../core/arc-to-bezier.js";
20
23
  import { buildChartScene, legendSeriesName } from "../core/drawingml/chart-geometry.js";
21
24
  import { layoutMath, mathGlyphSegments, variantStyle } from "./math-layout.js";
22
25
  import { rectPath } from "../core/drawingml/preset-geometry.js";
23
- import { buildShapePaths, buildShapeTransform, buildStroke, gradientToSolid } from "../core/drawingml/shape-render.js";
26
+ import { buildShapePaths, buildShapeTransform, buildStroke, gradientToSolid, lineEndPaths } from "../core/drawingml/shape-render.js";
24
27
  import { StructTreeBuilder } from "../pdf/struct-tree.js";
25
28
  //#region src/layout/styled-layout.ts
26
29
  function parsePdfAProfile(pdfA) {
@@ -33,6 +36,14 @@ function parsePdfAProfile(pdfA) {
33
36
  version: part === 1 ? "1.4" : "1.7"
34
37
  };
35
38
  }
39
+ /** §17.6.4 — how heavy Word draws the rule between columns. */
40
+ var COLUMN_SEPARATOR_PT = .5;
41
+ /**
42
+ * How far a space may be squeezed below its natural width — the shrink the
43
+ * line breaker offers when it weighs a line, and therefore the shrink the
44
+ * emitter owes it back when it draws one.
45
+ */
46
+ var GLUE_SHRINK_RATIO = .3;
36
47
  var TWIP_TO_PT = 1 / 20;
37
48
  var EIGHTH_PT = 1 / 8;
38
49
  var DEFAULT_CELL_PADDING_TWIPS = 108;
@@ -40,11 +51,41 @@ var DEFAULT_BORDER_SIZE_EIGHTH = 4;
40
51
  var FOOTNOTE_RULE_PT = .75;
41
52
  var FOOTNOTE_RULE_GAP_ABOVE = 4;
42
53
  var FOOTNOTE_SEPARATOR_HEIGHT = 10;
54
+ var LINE_NUMBER_GAP_PT = 18;
43
55
  var FLOAT_TEXT_GAP = 6;
44
56
  var MIN_WRAP_WIDTH = 36;
45
57
  function isOutOfFlowFloat(f) {
46
58
  return f !== void 0 && f.wrap !== "topAndBottom";
47
59
  }
60
+ /** The float's left edge, resolved from its horizontal anchor (§20.4.3.3). */
61
+ function floatXIn(frame, f, widthPt) {
62
+ const h = f.posH;
63
+ if (!h) return frame.columnLeft;
64
+ const textRight = frame.marginLeft + frame.contentWidth;
65
+ const pageRight = frame.pageLeft + frame.pageWidth;
66
+ const base = h.relativeFrom === "page" || h.relativeFrom === "leftMargin" ? frame.pageLeft : h.relativeFrom === "column" ? frame.columnLeft : h.relativeFrom === "rightMargin" ? textRight : frame.marginLeft;
67
+ const span = h.relativeFrom === "page" ? frame.pageWidth : h.relativeFrom === "column" ? frame.columnWidth : h.relativeFrom === "leftMargin" ? frame.marginLeft - frame.pageLeft : h.relativeFrom === "rightMargin" ? pageRight - textRight : frame.contentWidth;
68
+ if (h.align === "center") return base + (span - widthPt) / 2;
69
+ if (h.align === "right") return base + span - widthPt;
70
+ return base + (h.offsetPt ?? 0);
71
+ }
72
+ /** The float's TOP, y-up, resolved from its vertical anchor (§20.4.3.4). */
73
+ function floatTopYUpIn(frame, f, heightPt = 0) {
74
+ const v = f.posV;
75
+ if (!v) return frame.anchorY;
76
+ if (v.align !== void 0) {
77
+ const page = v.relativeFrom === "page";
78
+ const topYUp = page ? frame.pageTopYUp : frame.marginTopYUp;
79
+ const bottomYUp = page ? frame.pageBottomYUp : frame.marginBottomYUp;
80
+ if (v.align === "top") return topYUp;
81
+ if (v.align === "bottom") return bottomYUp + heightPt;
82
+ return bottomYUp + (topYUp - bottomYUp + heightPt) / 2;
83
+ }
84
+ if (v.relativeFrom === "page" || v.relativeFrom === "topMargin") return frame.pageTopYUp - (v.offsetPt ?? 0);
85
+ if (v.relativeFrom === "bottomMargin") return frame.marginBottomYUp - (v.offsetPt ?? 0);
86
+ if (v.relativeFrom === "margin") return frame.marginTopYUp - (v.offsetPt ?? 0);
87
+ return frame.anchorY - (v.offsetPt ?? 0);
88
+ }
48
89
  var FOOTNOTE_RULE_WIDTH = 144;
49
90
  function assignNoteNumbers(body, commentIds) {
50
91
  const footnotes = /* @__PURE__ */ new Map();
@@ -257,21 +298,49 @@ function commentTailBlocks(comment, n, opts) {
257
298
  */
258
299
  function layoutStyledDocument(body, options) {
259
300
  const sectionList = resolveSectionList(body, options);
260
- const noteAssigned = assignNoteNumbers(applyNumbering(body, options.numbering), options.comments ? new Set(options.comments.keys()) : void 0);
301
+ const noteAssigned = assignNoteNumbers(applyNumbering(body, options.numbering, options.styles), options.comments ? new Set(options.comments.keys()) : void 0);
261
302
  const numberedBody = noteAssigned.body;
262
- const numberedHeadersFooters = applyNumberingToHeadersFooters(options.headersFooters, options.numbering);
303
+ const numberedHeadersFooters = applyNumberingToHeadersFooters(options.headersFooters, options.numbering, options.styles);
263
304
  const pdfaProfile = options.pdfA ? parsePdfAProfile(options.pdfA) : void 0;
264
305
  const tagged = options.tagged === true || options.pdfUA === true || (pdfaProfile?.tagged ?? false);
265
306
  const structBuilder = tagged ? new StructTreeBuilder() : void 0;
266
- const fontResources = collectFontResources(numberedBody, numberedHeadersFooters, options);
267
307
  const imageResources = collectImageResources(numberedBody, numberedHeadersFooters, options);
268
- const sectionCtxs = sectionList.map((s) => buildSectionContext(s, options, numberedHeadersFooters, fontResources));
308
+ const fontResources = collectFontResources(numberedBody, numberedHeadersFooters, options, imageResources);
309
+ const sectionCtxs = sectionList.map((s) => buildSectionContext(s, options, numberedHeadersFooters, fontResources, imageResources));
269
310
  let sectionIdx = 0;
311
+ const bodyFrames = frameGroups(numberedBody);
270
312
  const blocks = numberedBody.map((el, idx) => {
271
313
  while (sectionIdx < sectionCtxs.length - 1 && idx >= sectionCtxs[sectionIdx].endIndex) sectionIdx++;
272
314
  const ctx = sectionCtxs[sectionIdx];
273
- return layoutBodyElement(el, options, fontResources, imageResources, ctx.columns ? ctx.columns[0].widthPt : ctx.contentWidth, ctx.pageContentHeight);
315
+ const width = ctx.columns ? ctx.columns[0].widthPt : ctx.contentWidth;
316
+ const box = {
317
+ pageWidthPt: ctx.pageWidth,
318
+ pageHeightPt: ctx.pageHeight,
319
+ marginLeftPt: ctx.marginLeft,
320
+ marginRightPt: ctx.pageWidth - ctx.marginLeft - ctx.contentWidth,
321
+ marginTopPt: ctx.marginTop,
322
+ marginBottomPt: ctx.marginBottom
323
+ };
324
+ const group = bodyFrames.starts.get(idx);
325
+ if (group) return layoutFrameBlock(group, ctx.options ?? options, fontResources, imageResources, width);
326
+ if (bodyFrames.continued.has(idx)) return emptyBlock(options);
327
+ return layoutBodyElement(el, ctx.options ?? options, fontResources, imageResources, width, ctx.pageContentHeight, box);
274
328
  });
329
+ for (let i = 0; i < blocks.length; i++) {
330
+ const here = blocks[i];
331
+ const next = blocks[i + 1];
332
+ if (here?.kind !== "paragraph" || next?.kind !== "paragraph") continue;
333
+ if (here.source?.properties.styleId !== next.source?.properties.styleId) continue;
334
+ if (here.resolved.contextualSpacing) blocks[i] = {
335
+ ...here,
336
+ spacingAfterPt: 0
337
+ };
338
+ if (next.resolved.contextualSpacing) blocks[i + 1] = {
339
+ ...next,
340
+ spacingBeforePt: 0
341
+ };
342
+ }
343
+ flowLinkedTextBoxes(blocks);
275
344
  const noteBlockCache = /* @__PURE__ */ new Map();
276
345
  const notePlan = options.footnotes && noteAssigned.footnotes.size > 0 ? {
277
346
  numbers: noteAssigned.footnotes,
@@ -286,7 +355,7 @@ function layoutStyledDocument(body, options) {
286
355
  const n = noteAssigned.footnotes.get(id);
287
356
  let laid = void 0;
288
357
  if (content && n !== void 0 && !noteAssigned.deferredFootnotes.includes(id)) {
289
- const noteBlocks = substituteNoteNumber(content, n).map((el) => layoutBodyElement(el, options, fontResources, imageResources, sectionCtx.contentWidth, sectionCtx.pageContentHeight));
358
+ const noteBlocks = substituteNoteNumber(content, n).map((el) => layoutBodyElement(el, sectionCtx.options ?? options, fontResources, imageResources, sectionCtx.contentWidth, sectionCtx.pageContentHeight));
290
359
  laid = {
291
360
  blocks: noteBlocks,
292
361
  heightPt: noteBlocks.reduce((sum, b) => sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : 0), 0)
@@ -314,7 +383,24 @@ function layoutStyledDocument(body, options) {
314
383
  n
315
384
  });
316
385
  }
317
- for (const note of tailNotes.sort((a, b) => a.n - b.n)) for (const el of substituteNoteNumber(note.content, note.n)) blocks.push(layoutBodyElement(el, options, fontResources, imageResources, lastCtx.contentWidth, lastCtx.pageContentHeight));
386
+ if (tailNotes.length > 0) blocks.push(layoutShapeBlock({
387
+ width: pt(FOOTNOTE_RULE_WIDTH),
388
+ height: pt(FOOTNOTE_RULE_PT),
389
+ geometry: {
390
+ kind: "preset",
391
+ preset: "rect",
392
+ adjust: /* @__PURE__ */ new Map()
393
+ },
394
+ fill: {
395
+ kind: "solid",
396
+ colorHex: "000000"
397
+ },
398
+ paragraphProperties: {
399
+ spacingBefore: pt(FOOTNOTE_SEPARATOR_HEIGHT),
400
+ spacingAfter: pt(FOOTNOTE_RULE_GAP_ABOVE)
401
+ }
402
+ }, options, fontResources, imageResources, lastCtx.contentWidth));
403
+ for (const note of tailNotes.sort((a, b) => a.n - b.n)) for (const el of substituteNoteNumber(note.content, note.n)) blocks.push(layoutBodyElement(el, lastCtx.options ?? options, fontResources, imageResources, lastCtx.contentWidth, lastCtx.pageContentHeight));
318
404
  const commentNums = noteAssigned.comments;
319
405
  const threadDepth = (id) => {
320
406
  let depth = 0;
@@ -346,7 +432,7 @@ function layoutStyledDocument(body, options) {
346
432
  const commentNotes = options.commentAnnotations && options.comments ? buildCommentNotes(noteAssigned.comments, options.comments) : void 0;
347
433
  const reflowParagraph = (paragraph, width, widths) => layoutParagraphBlock(paragraph, options, fontResources, imageResources, width, widths);
348
434
  return {
349
- pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph),
435
+ pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph, options.pageBackgroundFill?.imageResource !== void 0 ? imageResources.get(options.pageBackgroundFill.imageResource)?.resourceName : void 0),
350
436
  resources: options.resources ?? new ResourceStore(),
351
437
  fontResources,
352
438
  imageResources,
@@ -367,14 +453,16 @@ function resolvePageDimensions(options, section) {
367
453
  const sectionRight = section?.margins?.right !== void 0 ? section.margins.right : void 0;
368
454
  const sectionTop = section?.margins?.top !== void 0 ? section.margins.top : void 0;
369
455
  const sectionBottom = section?.margins?.bottom !== void 0 ? section.margins.bottom : void 0;
456
+ const gutter = section?.margins?.gutter ?? 0;
457
+ const atTop = options.gutterAtTop === true;
370
458
  const headerOffsetPt = section?.margins?.header ?? 720 * TWIP_TO_PT;
371
459
  const footerOffsetPt = section?.margins?.footer ?? 720 * TWIP_TO_PT;
372
460
  return {
373
461
  pageWidth: options.pageWidth ?? sectionPageWidth ?? 595,
374
462
  pageHeight: options.pageHeight ?? sectionPageHeight ?? 842,
375
- marginLeft: options.marginLeft ?? sectionLeft ?? 72,
463
+ marginLeft: (options.marginLeft ?? sectionLeft ?? 72) + (atTop ? 0 : gutter),
376
464
  marginRight: options.marginRight ?? sectionRight ?? 72,
377
- marginTop: options.marginTop ?? sectionTop ?? 72,
465
+ marginTop: (options.marginTop ?? sectionTop ?? 72) + (atTop ? gutter : 0),
378
466
  marginBottom: options.marginBottom ?? sectionBottom ?? 72,
379
467
  headerOffsetPt,
380
468
  footerOffsetPt
@@ -394,13 +482,32 @@ function resolveSectionList(body, options) {
394
482
  endIndex: body.length
395
483
  }];
396
484
  }
397
- function buildSectionContext(section, options, headersFooters, fontResources) {
485
+ function buildSectionContext(section, options, headersFooters, fontResources, imageResources) {
398
486
  const dims = resolvePageDimensions(options, section.properties);
399
487
  const contentWidth = dims.pageWidth - dims.marginLeft - dims.marginRight;
400
- const headerSet = layoutHeaderSet(section.properties, headersFooters, options, fontResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.headerOffsetPt);
401
- const footerSet = layoutFooterSet(section.properties, headersFooters, options, fontResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.footerOffsetPt);
488
+ const secOptions = section.properties.gridLinePitchPt !== void 0 ? {
489
+ ...options,
490
+ gridLinePitchPt: section.properties.gridLinePitchPt
491
+ } : options;
492
+ const relativeBox = {
493
+ pageWidthPt: dims.pageWidth,
494
+ pageHeightPt: dims.pageHeight,
495
+ marginLeftPt: dims.marginLeft,
496
+ marginRightPt: dims.marginRight,
497
+ marginTopPt: dims.marginTop,
498
+ marginBottomPt: dims.marginBottom
499
+ };
500
+ const headerSet = layoutHeaderSet(section.properties, headersFooters, secOptions, fontResources, imageResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.headerOffsetPt, {
501
+ top: dims.marginTop,
502
+ bottom: dims.marginBottom
503
+ }, relativeBox);
504
+ const footerSet = layoutFooterSet(section.properties, headersFooters, secOptions, fontResources, imageResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.footerOffsetPt, {
505
+ top: dims.marginTop,
506
+ bottom: dims.marginBottom
507
+ }, relativeBox);
402
508
  const columns = buildColumnGeometry(section.properties.columns, contentWidth);
403
- const headerBottom = dims.headerOffsetPt + Math.max(headerSet.default.heightPt ?? 0, headerSet.first.heightPt ?? 0, headerSet.even.heightPt ?? 0);
509
+ const headerHeight = Math.max(headerSet.default.heightPt ?? 0, section.properties.titlePg === true ? headerSet.first.heightPt ?? 0 : 0, section.properties.evenAndOddHeaders === true ? headerSet.even.heightPt ?? 0 : 0);
510
+ const headerBottom = headerHeight > 0 ? dims.headerOffsetPt + headerHeight : 0;
404
511
  const marginTop = Math.max(dims.marginTop, headerBottom);
405
512
  return {
406
513
  endIndex: section.endIndex,
@@ -416,7 +523,9 @@ function buildSectionContext(section, options, headersFooters, fontResources) {
416
523
  headerSet,
417
524
  footerSet,
418
525
  titlePg: section.properties.titlePg === true,
419
- evenAndOddHeaders: section.properties.evenAndOddHeaders === true
526
+ evenAndOddHeaders: section.properties.evenAndOddHeaders === true,
527
+ continuous: section.properties.sectionStart === "continuous",
528
+ options: secOptions
420
529
  };
421
530
  }
422
531
  function buildColumnGeometry(cols, contentWidth) {
@@ -451,7 +560,7 @@ function markPagination(cmds) {
451
560
  artifact: "pagination"
452
561
  }));
453
562
  }
454
- function layoutHeaderSet(section, headersFooters, options, fontResources, contentWidth, marginLeft, pageHeight, headerOffsetPt) {
563
+ function layoutHeaderSet(section, headersFooters, options, fontResources, imageResources, contentWidth, marginLeft, pageHeight, headerOffsetPt, bandMargins, relativeBox) {
455
564
  const band = (type) => {
456
565
  const ref = refByType(section.headers, type);
457
566
  if (!ref) return { commands: [] };
@@ -459,9 +568,9 @@ function layoutHeaderSet(section, headersFooters, options, fontResources, conten
459
568
  if (!content) return { commands: [] };
460
569
  let measured = 0;
461
570
  const render = (c) => {
462
- const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth);
571
+ const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth, imageResources, relativeBox);
463
572
  measured = blocksHeight(blocks);
464
- return markPagination(drawBlocksSequentially(blocks, marginLeft, pageHeight - headerOffsetPt, pageHeight));
573
+ return markPagination(drawBlocksSequentially(blocks, marginLeft, pageHeight - headerOffsetPt, pageHeight, contentWidth, void 0, bandMargins));
465
574
  };
466
575
  if (contentHasPageFields(content)) {
467
576
  render(substitutePageFields(content, 1, 1));
@@ -482,19 +591,37 @@ function layoutHeaderSet(section, headersFooters, options, fontResources, conten
482
591
  even: band("even")
483
592
  };
484
593
  }
594
+ /**
595
+ * §17.6.4 — the height each column of a BALANCED band is given: the band's
596
+ * share, but never more than the page has room for, and never so little that
597
+ * the band could not fit the columns at all.
598
+ *
599
+ * @param bandHeightPt The band's whole laid-out height.
600
+ * @param colCount How many columns it has to share.
601
+ * @param roomPt What the page has left below the band's top.
602
+ * @returns The per-column height, or 0 when the band should just fill.
603
+ */
604
+ function balancedColumnHeight(bandHeightPt, colCount, roomPt) {
605
+ if (bandHeightPt <= 0 || colCount < 2 || roomPt <= 0) return 0;
606
+ if (bandHeightPt > roomPt * colCount) return 0;
607
+ return Math.min(roomPt, bandHeightPt / colCount);
608
+ }
485
609
  /** Total laid-out height of a run of blocks, paragraph spacing included. */
486
610
  function blocksHeight(blocks) {
487
- return blocks.reduce((sum, b) => sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt), 0);
611
+ return blocks.reduce((sum, b) => {
612
+ if (b.kind !== "paragraph" && b.kind !== "table" && isOutOfFlowFloat(b.float)) return sum;
613
+ return sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt);
614
+ }, 0);
488
615
  }
489
- function layoutFooterSet(section, headersFooters, options, fontResources, contentWidth, marginLeft, pageHeight, footerOffsetPt) {
616
+ function layoutFooterSet(section, headersFooters, options, fontResources, imageResources, contentWidth, marginLeft, pageHeight, footerOffsetPt, bandMargins, relativeBox) {
490
617
  const band = (type) => {
491
618
  const ref = refByType(section.footers, type);
492
619
  if (!ref) return { commands: [] };
493
620
  const content = headersFooters.get(ref.relationshipId);
494
621
  if (!content) return { commands: [] };
495
622
  const render = (c) => {
496
- const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth);
497
- return markPagination(drawBlocksSequentially(blocks, marginLeft, footerOffsetPt + blocks.reduce((sum, b) => sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt), 0), pageHeight));
623
+ const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth, imageResources, relativeBox);
624
+ return markPagination(drawBlocksSequentially(blocks, marginLeft, footerOffsetPt + blocksHeight(blocks), pageHeight, contentWidth, void 0, bandMargins));
498
625
  };
499
626
  if (contentHasPageFields(content)) return {
500
627
  commands: [],
@@ -530,7 +657,7 @@ function substitutePageFields(content, pageNumber, totalPages) {
530
657
  }
531
658
  function pickBand(set, band) {
532
659
  const has = (e) => e.commands.length > 0 || e.renderDynamic !== void 0;
533
- if (band === "first") return has(set.first) ? set.first : set.default;
660
+ if (band === "first") return set.first;
534
661
  if (band === "even") return has(set.even) ? set.even : set.default;
535
662
  return set.default;
536
663
  }
@@ -539,24 +666,329 @@ function bandForPage(pageInSection, globalPageIdx, titlePg, evenAndOddHeaders) {
539
666
  if (evenAndOddHeaders && (globalPageIdx + 1) % 2 === 0) return "even";
540
667
  return "default";
541
668
  }
542
- function laidOutBlocksFor(elements, options, fontResources, contentWidth) {
543
- return elements.map((el) => layoutBodyElement(el, options, fontResources, void 0, contentWidth));
669
+ /**
670
+ * §17.3.1.11 — the runs of consecutive paragraphs that carry the SAME frame.
671
+ * Word treats them as one floating text frame, so they lay out together in one
672
+ * box; the entries after the first become empty blocks, which keeps every
673
+ * caller's element indexing (and its section bounds) intact.
674
+ *
675
+ * @param elements The body elements, in order.
676
+ * @returns `starts`: the index each frame begins at → its paragraphs;
677
+ * `continued`: the indices swallowed by a frame that began earlier.
678
+ */
679
+ function frameGroups(elements) {
680
+ const starts = /* @__PURE__ */ new Map();
681
+ const continued = /* @__PURE__ */ new Set();
682
+ const key = (p) => p.properties.frame ? JSON.stringify(p.properties.frame) : void 0;
683
+ let i = 0;
684
+ while (i < elements.length) {
685
+ const el = elements[i];
686
+ const k = el.kind === "paragraph" ? key(el.paragraph) : void 0;
687
+ if (k === void 0) {
688
+ i++;
689
+ continue;
690
+ }
691
+ const members = [el.paragraph];
692
+ let j = i + 1;
693
+ while (j < elements.length) {
694
+ const next = elements[j];
695
+ if (next.kind !== "paragraph" || key(next.paragraph) !== k) break;
696
+ members.push(next.paragraph);
697
+ continued.add(j);
698
+ j++;
699
+ }
700
+ starts.set(i, members);
701
+ i = j;
702
+ }
703
+ return {
704
+ starts,
705
+ continued
706
+ };
707
+ }
708
+ /** A block that occupies no space at all — the tail of a frame group. */
709
+ function emptyBlock(options) {
710
+ return {
711
+ kind: "paragraph",
712
+ resolved: resolveParagraphProperties({}, options.styles),
713
+ lines: [],
714
+ heightPt: 0,
715
+ spacingBeforePt: 0,
716
+ spacingAfterPt: 0
717
+ };
718
+ }
719
+ /**
720
+ * A text frame as the floating box it is: the paragraphs inside it lay out at
721
+ * the frame's width, the box takes the height the frame states or the text
722
+ * needs, and the body flows past it as its `w:wrap` says.
723
+ *
724
+ * @param paragraphs The frame's paragraphs, in order.
725
+ * @param options Render options.
726
+ * @param fontResources Fonts by variant.
727
+ * @param imageResources Images by resource id.
728
+ * @param contentWidth The width available where the frame is anchored.
729
+ * @returns The laid-out frame.
730
+ */
731
+ function layoutFrameBlock(paragraphs, options, fontResources, imageResources, contentWidth) {
732
+ const frame = paragraphs[0].properties.frame;
733
+ const exact = frame.heightRule === "exact" && frame.heightPt !== void 0;
734
+ const raw = (widthPt, pin) => layoutShapeBlock(frameShape(frame, paragraphs, widthPt, pin), options, fontResources, imageResources, contentWidth);
735
+ const atLeast = !exact && frame.heightPt !== void 0 && frame.heightRule !== "auto" ? frame.heightPt : 0;
736
+ const build = (widthPt) => {
737
+ const laid = raw(widthPt, exact);
738
+ return laid.kind === "shape" && atLeast > laid.heightPt ? raw(widthPt, true) : laid;
739
+ };
740
+ if (frame.widthPt !== void 0) return build(frame.widthPt);
741
+ const measured = build(contentWidth);
742
+ const natural = measured.kind === "shape" ? Math.max(1, ...measured.textLines.map((l) => l.contentWidthPt)) : contentWidth;
743
+ return build(Math.min(contentWidth, natural));
744
+ }
745
+ var MAX_FIT_PASSES = 4;
746
+ function scaleTextBody(text, k) {
747
+ return {
748
+ ...text,
749
+ content: text.content.map((el) => el.kind === "paragraph" ? {
750
+ ...el,
751
+ paragraph: {
752
+ ...el.paragraph,
753
+ runs: el.paragraph.runs.map((run) => ({
754
+ ...run,
755
+ properties: {
756
+ ...run.properties,
757
+ fontSizePt: pt((run.properties.fontSizePt ?? 11) * k)
758
+ }
759
+ }))
760
+ }
761
+ } : el)
762
+ };
763
+ }
764
+ function frameShape(frame, paragraphs, widthPt, exact) {
765
+ const lead = paragraphs[0]?.properties;
766
+ const edge = lead?.borders?.top ?? lead?.borders?.left;
767
+ const vertical = lead?.textDirection === "btLr" ? "vert270" : lead?.textDirection === "tbRl" ? "vert" : void 0;
768
+ const painted = lead?.shading !== void 0 || edge !== void 0 && edge.style !== "none";
769
+ return {
770
+ width: pt(widthPt),
771
+ height: pt(exact ? frame.heightPt ?? 0 : 0),
772
+ geometry: painted ? {
773
+ kind: "preset",
774
+ preset: "rect",
775
+ adjust: /* @__PURE__ */ new Map()
776
+ } : {
777
+ kind: "custom",
778
+ custom: {
779
+ pathWidth: 0,
780
+ pathHeight: 0,
781
+ commands: []
782
+ }
783
+ },
784
+ fill: lead?.shading ? {
785
+ kind: "solid",
786
+ colorHex: lead.shading.colorHex
787
+ } : { kind: "none" },
788
+ ...edge && edge.style !== "none" ? { line: {
789
+ fill: "solid",
790
+ colorHex: edge.colorHex ?? "000000",
791
+ width: edge.width ?? pt(.75)
792
+ } } : {},
793
+ float: frameFloat(frame),
794
+ paragraphProperties: {},
795
+ text: {
796
+ content: paragraphs.map((paragraph) => ({
797
+ kind: "paragraph",
798
+ paragraph
799
+ })),
800
+ insetLeft: pt(0),
801
+ insetRight: pt(0),
802
+ insetTop: pt(0),
803
+ insetBottom: pt(0),
804
+ ...vertical ? { vertical } : {},
805
+ ...exact ? {} : { autoFit: true }
806
+ }
807
+ };
808
+ }
809
+ /** §17.3.1.11 → §20.4.2.3: the frame's anchor, in the terms floats are placed in. */
810
+ /**
811
+ * `wps:linkedTxbx` — carry each chain's overflow from one box to the next.
812
+ * The words all live in the chain's FIRST box; a box shows as many of its lines
813
+ * as its height holds and hands the rest on, so LinkedTextBoxes.docx reads down
814
+ * one column and continues at the top of the other. A box that ends a chain
815
+ * keeps whatever is left, overflowing as it would have anyway.
816
+ *
817
+ * The lines are broken at the first box's width, so a chain of boxes of
818
+ * different widths sets its later columns to the wrong measure; re-breaking
819
+ * needs the source paragraphs split at the carry point, which the laid-out
820
+ * lines no longer know.
821
+ */
822
+ function flowLinkedTextBoxes(blocks) {
823
+ const chains = /* @__PURE__ */ new Map();
824
+ blocks.forEach((b, i) => {
825
+ if (b.kind !== "shape" || !b.textChain) return;
826
+ const at = chains.get(b.textChain.id) ?? [];
827
+ at.push(i);
828
+ chains.set(b.textChain.id, at);
829
+ });
830
+ for (const indices of chains.values()) {
831
+ if (indices.length < 2) continue;
832
+ indices.sort((a, b) => {
833
+ return (blocks[a].textChain?.seq ?? 0) - (blocks[b].textChain?.seq ?? 0);
834
+ });
835
+ let carried = [];
836
+ indices.forEach((idx, n) => {
837
+ const box = blocks[idx];
838
+ const all = n === 0 ? boxLines(box) : carried;
839
+ const last = n === indices.length - 1;
840
+ const room = box.heightPt - box.insetTopPt - box.insetBottomPt;
841
+ const kept = [];
842
+ let used = 0;
843
+ for (const l of all) {
844
+ const h = computeLineHeight(l.line, l.line.resolved) + l.gapPt;
845
+ if (!last && kept.length > 0 && used + h > room) break;
846
+ kept.push(l);
847
+ used += h;
848
+ }
849
+ carried = all.slice(kept.length);
850
+ blocks[idx] = rebuildBoxLines(box, kept);
851
+ });
852
+ }
853
+ }
854
+ function boxLines(box) {
855
+ return box.textLines.map((line, i) => ({
856
+ line,
857
+ gapPt: box.textLineGaps?.get(i) ?? 0,
858
+ ...box.textCharts?.get(i) ? { chart: box.textCharts.get(i) } : {},
859
+ ...box.textShapes?.get(i) ? { shape: box.textShapes.get(i) } : {},
860
+ ...box.textTables?.get(i) ? { table: box.textTables.get(i) } : {}
861
+ }));
862
+ }
863
+ function rebuildBoxLines(box, lines) {
864
+ const charts = /* @__PURE__ */ new Map();
865
+ const shapes = /* @__PURE__ */ new Map();
866
+ const tables = /* @__PURE__ */ new Map();
867
+ const gaps = /* @__PURE__ */ new Map();
868
+ let height = 0;
869
+ lines.forEach((l, i) => {
870
+ if (l.chart) charts.set(i, l.chart);
871
+ if (l.shape) shapes.set(i, l.shape);
872
+ if (l.table) tables.set(i, l.table);
873
+ if (l.gapPt > 0) gaps.set(i, l.gapPt);
874
+ height += computeLineHeight(l.line, l.line.resolved) + l.gapPt;
875
+ });
876
+ const { textCharts: _c, textShapes: _s, textTables: _t, textLineGaps: _g, ...rest } = box;
877
+ return {
878
+ ...rest,
879
+ textLines: lines.map((l) => l.line),
880
+ ...charts.size > 0 ? { textCharts: charts } : {},
881
+ ...shapes.size > 0 ? { textShapes: shapes } : {},
882
+ ...tables.size > 0 ? { textTables: tables } : {},
883
+ ...gaps.size > 0 ? { textLineGaps: gaps } : {},
884
+ textHeightPt: height
885
+ };
886
+ }
887
+ /**
888
+ * §17.3.1.11 — whether the frame is placed DOWN THE PAGE by itself, rather than
889
+ * where the flow has reached. `w:vAnchor="text"` (the default) measures `w:y`
890
+ * from the paragraph's own position, so such frames stack as paragraphs do —
891
+ * and a float takes no flow space, so lifting them out puts every one of them
892
+ * on the same line: tdf105035_framePrB.docx writes its title and its author
893
+ * line as text-anchored frames and both landed on the cursor, one over the
894
+ * other. Only a frame anchored to the page or the margin, or aligned by
895
+ * keyword, truly stands somewhere of its own.
896
+ */
897
+ function framePositioned(frame) {
898
+ return frame.yAlign !== void 0 || frame.vAnchor !== void 0 && frame.vAnchor !== "text";
899
+ }
900
+ function frameFloat(frame) {
901
+ const wrap = frame.wrap === "none" ? "none" : frame.wrap === "notBeside" ? framePositioned(frame) ? "notBeside" : "topAndBottom" : "square";
902
+ const hRel = frame.hAnchor === "page" ? "page" : frame.hAnchor === "margin" ? "margin" : "column";
903
+ const vRel = frame.vAnchor === "page" ? "page" : frame.vAnchor === "margin" ? "margin" : "paragraph";
904
+ const align = frame.xAlign === "left" || frame.xAlign === "center" || frame.xAlign === "right" ? frame.xAlign : void 0;
905
+ const vAlign = frame.yAlign === "top" || frame.yAlign === "center" || frame.yAlign === "bottom" ? frame.yAlign : void 0;
906
+ return {
907
+ wrap,
908
+ posH: {
909
+ relativeFrom: hRel,
910
+ ...align ? { align } : { offsetPt: frame.xPt ?? pt(0) }
911
+ },
912
+ posV: {
913
+ relativeFrom: vRel,
914
+ ...vAlign ? { align: vAlign } : { offsetPt: frame.yPt ?? pt(0) }
915
+ }
916
+ };
917
+ }
918
+ /**
919
+ * A drawing's width when it states one as a share of the page or the margins.
920
+ *
921
+ * @param rel The relative size, if any.
922
+ * @param content The margin box's width.
923
+ * @param box The page box, when the caller knows it.
924
+ * @returns The width in points, or `undefined` when none is stated.
925
+ */
926
+ function relativeWidth(rel, content, box) {
927
+ if (!rel?.widthPct) return void 0;
928
+ const from = rel.widthFrom;
929
+ return (box === void 0 || from === void 0 || from === "margin" ? content : from === "page" ? box.pageWidthPt : from === "leftMargin" ? box.marginLeftPt : box.marginRightPt) * rel.widthPct;
544
930
  }
545
- function layoutBodyElement(el, options, fontResources, imageResources, contentWidth, maxHeight) {
931
+ /** The height twin of {@link relativeWidth}; the margin box's height is the page's content height. */
932
+ function relativeHeight(rel, contentHeight, box) {
933
+ if (!rel?.heightPct) return void 0;
934
+ const from = rel.heightFrom;
935
+ const base = box === void 0 || from === void 0 || from === "margin" ? contentHeight : from === "page" ? box.pageHeightPt : from === "topMargin" ? box.marginTopPt : box.marginBottomPt;
936
+ return base === void 0 ? void 0 : base * rel.heightPct;
937
+ }
938
+ function laidOutBlocksFor(elements, options, fontResources, contentWidth, imageResources, box) {
939
+ const frames = frameGroups(elements);
940
+ return elements.map((el, idx) => {
941
+ const group = frames.starts.get(idx);
942
+ if (group) return layoutFrameBlock(group, options, fontResources, imageResources, contentWidth);
943
+ if (frames.continued.has(idx)) return emptyBlock(options);
944
+ return layoutBodyElement(el, options, fontResources, imageResources, contentWidth, void 0, box);
945
+ });
946
+ }
947
+ function layoutBodyElement(el, options, fontResources, imageResources, contentWidth, maxHeight, box) {
546
948
  if (el.kind === "paragraph") return layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, contentWidth);
547
949
  if (el.kind === "table") return layoutTableBlock(el.table, options, fontResources, imageResources, contentWidth);
548
- if (el.kind === "image") return layoutImageBlock(el.image, imageResources, contentWidth);
950
+ if (el.kind === "image") {
951
+ const meta = el.image.resource ? imageResources?.get(el.image.resource)?.metafile : void 0;
952
+ return meta ? layoutMetafileBlock(el.image, meta, options, fontResources, contentWidth, maxHeight, box) : layoutImageBlock(el.image, imageResources, contentWidth, maxHeight, box);
953
+ }
549
954
  if (el.kind === "chart") return layoutChartBlock(el.chart, options, fontResources, contentWidth, maxHeight);
550
- return layoutShapeBlock(el.shape, options, fontResources, imageResources, contentWidth, maxHeight);
955
+ return layoutShapeBlock(el.shape, options, fontResources, imageResources, contentWidth, maxHeight, box);
551
956
  }
552
- function layoutImageBlock(image, imageResources, contentWidth) {
553
- let widthPt = image.width;
554
- let heightPt = image.height;
555
- if (widthPt > contentWidth) {
957
+ function layoutImageBlock(image, imageResources, contentWidth, maxHeight, box) {
958
+ let widthPt = relativeWidth(image.relativeSize, contentWidth, box) ?? image.width;
959
+ let heightPt = relativeHeight(image.relativeSize, maxHeight, box) ?? (image.relativeSize?.widthPct && image.width > 0 ? image.height * (widthPt / image.width) : image.height);
960
+ if (widthPt > contentWidth && !image.float) {
556
961
  const scale = contentWidth / widthPt;
557
962
  widthPt = contentWidth;
558
963
  heightPt = heightPt * scale;
559
964
  }
965
+ let drawInset;
966
+ const eff = image.effectExtent;
967
+ if (eff) {
968
+ const s = image.width > 0 ? widthPt / image.width : 1;
969
+ let box = {
970
+ dxPt: eff.leftPt * s,
971
+ dyTopPt: eff.topPt * s,
972
+ widthPt,
973
+ heightPt
974
+ };
975
+ let reservedW = widthPt + (eff.leftPt + eff.rightPt) * s;
976
+ let reservedH = heightPt + (eff.topPt + eff.bottomPt) * s;
977
+ if (reservedW > contentWidth) {
978
+ const k = contentWidth / reservedW;
979
+ reservedW = contentWidth;
980
+ reservedH *= k;
981
+ box = {
982
+ dxPt: box.dxPt * k,
983
+ dyTopPt: box.dyTopPt * k,
984
+ widthPt: box.widthPt * k,
985
+ heightPt: box.heightPt * k
986
+ };
987
+ }
988
+ widthPt = reservedW;
989
+ heightPt = reservedH;
990
+ drawInset = box;
991
+ }
560
992
  const res = image.resource ? imageResources?.get(image.resource) : void 0;
561
993
  const resolvedAlignment = image.paragraphProperties.alignment ?? "left";
562
994
  return {
@@ -565,27 +997,34 @@ function layoutImageBlock(image, imageResources, contentWidth) {
565
997
  heightPt,
566
998
  resolvedAlignment,
567
999
  resourceName: res?.resourceName ?? "",
1000
+ ...image.outline ? { outline: image.outline } : {},
1001
+ ...image.wash ? { wash: image.wash } : {},
1002
+ ...image.crop ? { crop: image.crop } : {},
1003
+ ...image.rotation60k ? { rotationDeg: image.rotation60k / 6e4 } : {},
1004
+ ...image.flipH ? { flipH: true } : {},
1005
+ ...image.flipV ? { flipV: true } : {},
1006
+ ...drawInset ? { drawInset } : {},
568
1007
  spacingBeforePt: image.paragraphProperties.spacingBefore ?? 0,
569
1008
  spacingAfterPt: image.paragraphProperties.spacingAfter ?? 0,
570
1009
  ...image.altText ? { altText: image.altText } : {},
571
1010
  ...image.float ? { float: image.float } : {}
572
1011
  };
573
1012
  }
574
- function layoutShapeBlock(shape, options, fontResources, imageResources, contentWidth, maxHeight) {
575
- let widthPt = shape.width;
576
- let heightPt = shape.height;
577
- if (widthPt > contentWidth && widthPt > 0) {
1013
+ function layoutShapeBlock(shape, options, fontResources, imageResources, contentWidth, maxHeight, box, fitPass = 0) {
1014
+ let widthPt = relativeWidth(shape.relativeSize, contentWidth, box) ?? shape.width;
1015
+ let heightPt = relativeHeight(shape.relativeSize, maxHeight, box) ?? (shape.relativeSize?.widthPct && shape.width > 0 ? shape.height * (widthPt / shape.width) : shape.height);
1016
+ if (!shape.float && widthPt > contentWidth && widthPt > 0) {
578
1017
  const scale = contentWidth / widthPt;
579
1018
  widthPt = contentWidth;
580
1019
  heightPt *= scale;
581
1020
  }
582
- if (maxHeight !== void 0 && heightPt > maxHeight && heightPt > 0) {
1021
+ if (!shape.float && maxHeight !== void 0 && heightPt > maxHeight && heightPt > 0) {
583
1022
  const scale = maxHeight / heightPt;
584
1023
  widthPt *= scale;
585
1024
  heightPt = maxHeight;
586
1025
  }
587
- const paths = buildShapePaths(shape.geometry, widthPt, heightPt);
588
1026
  const fillGradient = shape.fill.kind === "gradient" ? shape.fill.gradient : void 0;
1027
+ const fillImageResourceName = shape.fill.kind === "picture" && shape.fill.imageResource !== void 0 ? imageResources?.get(shape.fill.imageResource)?.resourceName : void 0;
589
1028
  const fillColorHex = shape.fill.kind === "solid" ? shape.fill.colorHex : fillGradient ? gradientToSolid(fillGradient) : void 0;
590
1029
  const stroke = buildStroke(shape.line);
591
1030
  const t = shape.transform;
@@ -596,35 +1035,153 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
596
1035
  const insetTopPt = text?.insetTop ?? 3.6;
597
1036
  const insetBottomPt = text?.insetBottom ?? 3.6;
598
1037
  const textLines = [];
1038
+ const textCharts = /* @__PURE__ */ new Map();
1039
+ const textShapes = /* @__PURE__ */ new Map();
1040
+ const textTables = /* @__PURE__ */ new Map();
1041
+ const textLineGaps = /* @__PURE__ */ new Map();
599
1042
  let textHeightPt = 0;
1043
+ const vertical = text?.vertical;
600
1044
  if (text && text.content.length > 0) {
601
- const innerWidth = Math.max(1, widthPt - insetLeftPt - insetRightPt);
1045
+ const innerWidth = vertical ? Math.max(1, heightPt - insetTopPt - insetBottomPt) : Math.max(1, widthPt - insetLeftPt - insetRightPt);
602
1046
  for (const el of text.content) {
603
- if (el.kind !== "paragraph") continue;
1047
+ if (el.kind === "image") {
1048
+ const res = el.image.resource ? imageResources?.get(el.image.resource) : void 0;
1049
+ const scaled = Math.min(1, innerWidth / Math.max(1, el.image.width));
1050
+ const line = {
1051
+ tokens: [{
1052
+ kind: "image",
1053
+ imageResourceName: res?.resourceName ?? "",
1054
+ widthPt: el.image.width * scaled,
1055
+ heightPt: el.image.height * scaled,
1056
+ isSpace: false,
1057
+ bidiLevel: 0
1058
+ }],
1059
+ contentWidthPt: el.image.width * scaled,
1060
+ maxFontSizePt: el.image.height * scaled,
1061
+ availableWidthPt: innerWidth,
1062
+ firstLine: true,
1063
+ resolved: resolveParagraphProperties(el.image.paragraphProperties, options.styles),
1064
+ isLastInParagraph: true
1065
+ };
1066
+ textLines.push(line);
1067
+ textHeightPt += computeLineHeight(line, line.resolved);
1068
+ continue;
1069
+ }
1070
+ if (el.kind === "chart") {
1071
+ const laid = layoutChartBlock(el.chart, options, fontResources, innerWidth);
1072
+ const line = {
1073
+ tokens: [],
1074
+ contentWidthPt: laid.widthPt,
1075
+ maxFontSizePt: laid.heightPt,
1076
+ availableWidthPt: innerWidth,
1077
+ firstLine: true,
1078
+ resolved: resolveParagraphProperties(el.chart.paragraphProperties, options.styles),
1079
+ isLastInParagraph: true
1080
+ };
1081
+ textCharts.set(textLines.length, laid);
1082
+ textLines.push(line);
1083
+ textHeightPt += computeLineHeight(line, line.resolved);
1084
+ continue;
1085
+ }
1086
+ if (el.kind === "shape") {
1087
+ const laid = layoutShapeBlock(el.shape, options, fontResources, imageResources, innerWidth);
1088
+ const line = {
1089
+ tokens: [],
1090
+ contentWidthPt: laid.widthPt,
1091
+ maxFontSizePt: laid.heightPt,
1092
+ availableWidthPt: innerWidth,
1093
+ firstLine: true,
1094
+ resolved: resolveParagraphProperties(el.shape.paragraphProperties, options.styles),
1095
+ isLastInParagraph: true
1096
+ };
1097
+ textShapes.set(textLines.length, laid);
1098
+ textLines.push(line);
1099
+ textHeightPt += computeLineHeight(line, line.resolved);
1100
+ continue;
1101
+ }
1102
+ if (el.kind === "table") {
1103
+ const laid = layoutTableBlock(el.table, options, fontResources, imageResources, innerWidth);
1104
+ const line = {
1105
+ tokens: [],
1106
+ contentWidthPt: laid.totalWidthPt,
1107
+ maxFontSizePt: laid.heightPt,
1108
+ metricHeightPt: laid.heightPt,
1109
+ availableWidthPt: innerWidth,
1110
+ firstLine: true,
1111
+ resolved: resolveParagraphProperties({}, options.styles),
1112
+ isLastInParagraph: true
1113
+ };
1114
+ textTables.set(textLines.length, laid);
1115
+ textLines.push(line);
1116
+ textHeightPt += computeLineHeight(line, line.resolved);
1117
+ continue;
1118
+ }
604
1119
  const blk = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, innerWidth);
605
- const lines = blk.lines.length > 0 ? blk.lines : [{
606
- tokens: [],
607
- contentWidthPt: 0,
608
- maxFontSizePt: el.paragraph.runs[0]?.properties.fontSizePt ?? options.styles.defaultRunProperties.fontSizePt ?? pt(11),
609
- availableWidthPt: innerWidth,
610
- firstLine: true,
611
- resolved: blk.resolved,
612
- isLastInParagraph: true
613
- }];
614
- for (const line of lines) {
1120
+ const before = textLines.length > 0 ? blk.spacingBeforePt : 0;
1121
+ if (before > 0) {
1122
+ const last = textLines.length - 1;
1123
+ textLineGaps.set(last, (textLineGaps.get(last) ?? 0) + before);
1124
+ textHeightPt += before;
1125
+ }
1126
+ for (const line of blk.lines) {
615
1127
  textLines.push(line);
616
1128
  textHeightPt += computeLineHeight(line, blk.resolved);
617
1129
  }
1130
+ if (blk.spacingAfterPt > 0) {
1131
+ const last = textLines.length - 1;
1132
+ textLineGaps.set(last, (textLineGaps.get(last) ?? 0) + blk.spacingAfterPt);
1133
+ }
618
1134
  textHeightPt += blk.spacingAfterPt;
619
1135
  }
620
1136
  }
1137
+ if (text?.autoFit && !vertical && textLines.length > 0) heightPt = textHeightPt + insetTopPt + insetBottomPt;
1138
+ if (text?.fitToBox === true && textLines.length > 0 && fitPass < MAX_FIT_PASSES) {
1139
+ const innerW = Math.max(1, widthPt - insetLeftPt - insetRightPt);
1140
+ const innerH = Math.max(1, heightPt - insetTopPt - insetBottomPt);
1141
+ let widest = 0;
1142
+ let running = 0;
1143
+ let paragraphs = 0;
1144
+ for (const l of textLines) {
1145
+ running += l.contentWidthPt;
1146
+ if (l.isLastInParagraph) {
1147
+ widest = Math.max(widest, running);
1148
+ running = 0;
1149
+ paragraphs++;
1150
+ }
1151
+ }
1152
+ widest = Math.max(1, widest, running);
1153
+ const perLine = textHeightPt / Math.max(1, textLines.length);
1154
+ const naturalHeight = Math.max(1, perLine * Math.max(1, paragraphs || textLines.length));
1155
+ const k = Math.min(innerW / widest, innerH / naturalHeight);
1156
+ if (Number.isFinite(k) && k > .05 && k < 20 && Math.abs(k - 1) > .02) return layoutShapeBlock({
1157
+ ...shape,
1158
+ text: scaleTextBody(text, k)
1159
+ }, options, fontResources, imageResources, contentWidth, maxHeight, box, fitPass + 1);
1160
+ }
1161
+ const paths = buildShapePaths(shape.geometry, widthPt, heightPt);
1162
+ const markerPaths = lineEndPaths(paths, shape.line, stroke?.widthPt ?? 0);
1163
+ const scale = shape.width > 0 ? widthPt / shape.width : 1;
1164
+ const children = (shape.children ?? []).map((child) => ({
1165
+ laid: layoutShapeBlock({
1166
+ ...child.shape,
1167
+ width: pt(child.shape.width * scale),
1168
+ height: pt(child.shape.height * scale)
1169
+ }, options, fontResources, imageResources, Number.POSITIVE_INFINITY),
1170
+ xPt: child.xPt * scale,
1171
+ yPt: child.yPt * scale
1172
+ }));
621
1173
  return {
622
1174
  kind: "shape",
623
1175
  widthPt,
624
1176
  heightPt,
1177
+ ...children.length > 0 ? { children } : {},
625
1178
  paths,
1179
+ ...markerPaths.length > 0 ? { markerPaths } : {},
1180
+ ...fillImageResourceName ? { fillImageResourceName } : {},
1181
+ ...fillImageResourceName && shape.fill.imageCrop ? { fillImageCrop: shape.fill.imageCrop } : {},
626
1182
  ...fillColorHex ? { fillColorHex } : {},
627
1183
  ...fillGradient ? { fillGradient } : {},
1184
+ ...shape.fill.alpha !== void 0 && shape.fill.alpha < 1 ? { fillAlpha: shape.fill.alpha } : {},
628
1185
  ...stroke ? { stroke } : {},
629
1186
  ...shape.shadow ? { shadow: shape.shadow } : {},
630
1187
  rotation60k: t?.rotation60k ?? 0,
@@ -634,16 +1191,160 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
634
1191
  spacingBeforePt: pp.spacingBefore ?? 0,
635
1192
  spacingAfterPt: pp.spacingAfter ?? 0,
636
1193
  textLines,
1194
+ ...textCharts.size > 0 ? { textCharts } : {},
1195
+ ...textShapes.size > 0 ? { textShapes } : {},
1196
+ ...textTables.size > 0 ? { textTables } : {},
1197
+ ...textLineGaps.size > 0 ? { textLineGaps } : {},
637
1198
  textHeightPt,
1199
+ ...text?.chain ? { textChain: text.chain } : {},
638
1200
  insetLeftPt,
639
1201
  insetRightPt,
640
1202
  insetTopPt,
641
1203
  insetBottomPt,
642
1204
  anchor: text?.anchor ?? "t",
1205
+ ...vertical ? { vertical } : {},
643
1206
  ...shape.altText ? { altText: shape.altText } : {},
644
1207
  ...shape.float ? { float: shape.float } : {}
645
1208
  };
646
1209
  }
1210
+ /**
1211
+ * MS-EMF / MS-WMF — a metafile picture as the primitives a chart is made of,
1212
+ * scaled from its own logical box into the box the drawing reserves.
1213
+ *
1214
+ * A metafile IS a little drawing program, and what it draws is paths and words:
1215
+ * the same two things a chart's layout carries, placed in the same local y-up
1216
+ * frame. So one is turned into the other rather than growing a block kind, a
1217
+ * pagination arm and an emitter of its own.
1218
+ */
1219
+ function layoutMetafileBlock(image, pic, options, fontResources, contentWidth, maxHeight, box) {
1220
+ const placed = layoutImageBlock(image, void 0, contentWidth, maxHeight, box);
1221
+ const widthPt = placed.widthPt;
1222
+ const heightPt = placed.heightPt;
1223
+ const layout = rotateDrawing(metafileDrawing(pic, widthPt, heightPt, options, fontResources), (image.rotation60k ?? 0) / 6e4, widthPt / 2, heightPt / 2);
1224
+ const pp = image.paragraphProperties;
1225
+ return {
1226
+ kind: "chart",
1227
+ widthPt,
1228
+ heightPt,
1229
+ layout,
1230
+ resolvedAlignment: pp.alignment ?? "left",
1231
+ spacingBeforePt: pp.spacingBefore ?? 0,
1232
+ spacingAfterPt: pp.spacingAfter ?? 0,
1233
+ figureRole: "Image",
1234
+ ...image.altText ? { altText: image.altText } : {},
1235
+ ...image.float ? { float: image.float } : {}
1236
+ };
1237
+ }
1238
+ /** The same primitives, turned `deg` CLOCKWISE about a point of the local frame. */
1239
+ function rotateDrawing(layout, deg, cx, cy) {
1240
+ if (!deg) return layout;
1241
+ const a = -deg * Math.PI / 180;
1242
+ const cos = Math.cos(a);
1243
+ const sin = Math.sin(a);
1244
+ const at = (x, y) => ({
1245
+ x: cx + (x - cx) * cos - (y - cy) * sin,
1246
+ y: cy + (x - cx) * sin + (y - cy) * cos
1247
+ });
1248
+ return {
1249
+ shapes: layout.shapes.map((sh) => ({
1250
+ ...sh,
1251
+ paths: sh.paths.map((path) => ({
1252
+ ...path,
1253
+ segments: path.segments.map((seg) => {
1254
+ if (seg.op === "close") return seg;
1255
+ if (seg.op === "cubic") {
1256
+ const c1 = at(seg.x1, seg.y1);
1257
+ const c2 = at(seg.x2, seg.y2);
1258
+ const e = at(seg.x, seg.y);
1259
+ return {
1260
+ op: "cubic",
1261
+ x1: c1.x,
1262
+ y1: c1.y,
1263
+ x2: c2.x,
1264
+ y2: c2.y,
1265
+ x: e.x,
1266
+ y: e.y
1267
+ };
1268
+ }
1269
+ const p = at(seg.x, seg.y);
1270
+ return {
1271
+ op: seg.op,
1272
+ x: p.x,
1273
+ y: p.y
1274
+ };
1275
+ })
1276
+ }))
1277
+ })),
1278
+ texts: layout.texts.map((t) => {
1279
+ const p = at(t.x, t.y);
1280
+ return {
1281
+ ...t,
1282
+ x: p.x,
1283
+ y: p.y,
1284
+ rotationDeg: (t.rotationDeg ?? 0) - deg
1285
+ };
1286
+ })
1287
+ };
1288
+ }
1289
+ /**
1290
+ * A metafile's primitives, scaled from its own logical box into a `widthPt` ×
1291
+ * `heightPt` one and turned y-up about its bottom-left corner — the frame both
1292
+ * a chart block and an inline picture token are drawn in.
1293
+ */
1294
+ function metafileDrawing(pic, widthPt, heightPt, options, fontResources) {
1295
+ const sx = widthPt / pic.width;
1296
+ const sy = heightPt / pic.height;
1297
+ const mapX = (x) => (x - pic.left) * sx;
1298
+ const mapY = (y) => heightPt - (y - pic.top) * sy;
1299
+ const shapes = [];
1300
+ const texts = [];
1301
+ for (const prim of pic.prims) {
1302
+ if (prim.kind === "path") {
1303
+ shapes.push({
1304
+ paths: prim.paths.map((path) => ({
1305
+ ...path,
1306
+ segments: path.segments.map((seg) => seg.op === "close" ? seg : seg.op === "cubic" ? {
1307
+ op: "cubic",
1308
+ x1: mapX(seg.x1),
1309
+ y1: mapY(seg.y1),
1310
+ x2: mapX(seg.x2),
1311
+ y2: mapY(seg.y2),
1312
+ x: mapX(seg.x),
1313
+ y: mapY(seg.y)
1314
+ } : {
1315
+ op: seg.op,
1316
+ x: mapX(seg.x),
1317
+ y: mapY(seg.y)
1318
+ })
1319
+ })),
1320
+ ...prim.fillColorHex !== void 0 ? { fillColorHex: prim.fillColorHex } : {},
1321
+ ...prim.stroke ? { stroke: {
1322
+ ...prim.stroke,
1323
+ widthPt: Math.max(.25, prim.stroke.widthPt * sx)
1324
+ } } : {}
1325
+ });
1326
+ continue;
1327
+ }
1328
+ if (prim.kind !== "text") continue;
1329
+ const { variant } = options.registry.resolveByStyle(prim.bold === true, prim.italic === true);
1330
+ const font = fontResources.get(variant);
1331
+ if (!font) continue;
1332
+ const sizePt = Math.max(1, prim.sizeLu * sy);
1333
+ const line = makeChartLabelLine(prim.text, font, sizePt, prim.colorHex);
1334
+ const shift = prim.alignH === "center" ? -line.contentWidthPt / 2 : prim.alignH === "right" ? -line.contentWidthPt : 0;
1335
+ const baselineY = mapY(prim.y) - (prim.alignBaseline ? 0 : sizePt * .8);
1336
+ texts.push({
1337
+ line,
1338
+ x: mapX(prim.x) + shift,
1339
+ y: baselineY,
1340
+ ...prim.escapement ? { rotationDeg: prim.escapement / 10 } : {}
1341
+ });
1342
+ }
1343
+ return {
1344
+ shapes,
1345
+ texts
1346
+ };
1347
+ }
647
1348
  function layoutChartBlock(block, options, fontResources, contentWidth, maxHeight) {
648
1349
  let widthPt = block.width;
649
1350
  let heightPt = block.height;
@@ -693,6 +1394,7 @@ function buildChartLayout(chart, widthPt, heightPt, font) {
693
1394
  };
694
1395
  const shapes = [];
695
1396
  if (scene.background) shapes.push(rectPrim(scene.background));
1397
+ if (scene.plotBackground) shapes.push(rectPrim(scene.plotBackground));
696
1398
  for (const g of scene.gridlines ?? []) shapes.push(polylinePrim(g));
697
1399
  for (const pg of scene.polygons ?? []) shapes.push(polygonPrim(pg));
698
1400
  for (const r of scene.rects) shapes.push(rectPrim(r));
@@ -818,14 +1520,20 @@ function circlePath(cx, cy, r) {
818
1520
  const k = .5522847498307936 * r;
819
1521
  return new PathBuilder().moveTo(cx + r, cy).cubicTo(cx + r, cy + k, cx + k, cy + r, cx, cy + r).cubicTo(cx - k, cy + r, cx - r, cy + k, cx - r, cy).cubicTo(cx - r, cy - k, cx - k, cy - r, cx, cy - r).cubicTo(cx + k, cy - r, cx + r, cy - k, cx + r, cy).close().build();
820
1522
  }
1523
+ function strokeOfRect(r) {
1524
+ if (!r.strokeHex) return {};
1525
+ const stroke = buildStroke({
1526
+ colorHex: r.strokeHex,
1527
+ width: pt(r.strokeWidthPt ?? 1),
1528
+ ...r.strokeDash ? { dash: r.strokeDash } : {}
1529
+ });
1530
+ return stroke ? { stroke } : {};
1531
+ }
821
1532
  function rectPrim(r) {
822
1533
  return {
823
1534
  paths: [rectAtPath(r.x, r.y, r.w, r.h)],
824
1535
  ...r.fillHex ? { fillColorHex: r.fillHex } : {},
825
- ...r.strokeHex ? { stroke: {
826
- colorHex: r.strokeHex,
827
- widthPt: r.strokeWidthPt ?? 1
828
- } } : {}
1536
+ ...strokeOfRect(r)
829
1537
  };
830
1538
  }
831
1539
  function polylinePrim(p) {
@@ -915,10 +1623,16 @@ function collectImageResources(body, headersFooters, options) {
915
1623
  for (const run of el.paragraph.runs) if (run.inlineImage?.resource) seen.add(run.inlineImage.resource);
916
1624
  } else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) visit(cell.content);
917
1625
  else if (el.kind === "shape") {
918
- if (el.shape.text) visit(el.shape.text.content);
1626
+ const shapeImages = (sh) => {
1627
+ if (sh.fill.imageResource) seen.add(sh.fill.imageResource);
1628
+ if (sh.text) visit(sh.text.content);
1629
+ for (const child of sh.children ?? []) shapeImages(child.shape);
1630
+ };
1631
+ shapeImages(el.shape);
919
1632
  }
920
1633
  };
921
1634
  visit(body);
1635
+ if (options.pageBackgroundFill?.imageResource !== void 0) seen.add(options.pageBackgroundFill.imageResource);
922
1636
  for (const hf of headersFooters.values()) visit(hf);
923
1637
  for (const note of options.footnotes?.values() ?? []) visit(note);
924
1638
  for (const note of options.endnotes?.values() ?? []) visit(note);
@@ -928,6 +1642,15 @@ function collectImageResources(body, headersFooters, options) {
928
1642
  for (const resourceId of seen) {
929
1643
  const bytes = options.resources.get(resourceId);
930
1644
  if (!bytes) continue;
1645
+ if (isEmf(bytes) || isWmf(bytes)) {
1646
+ try {
1647
+ out.set(resourceId, {
1648
+ resourceName: "",
1649
+ metafile: isEmf(bytes) ? readEmf(bytes) : readWmf(bytes)
1650
+ });
1651
+ } catch {}
1652
+ continue;
1653
+ }
931
1654
  let prepared;
932
1655
  try {
933
1656
  prepared = prepareImage(bytes, { flattenAlpha });
@@ -942,12 +1665,296 @@ function collectImageResources(body, headersFooters, options) {
942
1665
  }
943
1666
  return out;
944
1667
  }
945
- function drawBlocksSequentially(blocks, startX, startY, pageHeight, structId) {
1668
+ function chartPageItems(laid, x, bottomYUp, pageHeight, structId) {
1669
+ const fig = structId !== void 0 ? { structId } : {};
1670
+ const out = [];
1671
+ for (const sh of laid.layout.shapes) out.push({
1672
+ type: "shape",
1673
+ shape: {
1674
+ paths: sh.paths,
1675
+ ...sh.fillColorHex ? { fillColorHex: sh.fillColorHex } : {},
1676
+ ...sh.stroke ? { stroke: sh.stroke } : {},
1677
+ transform: flipTransform([
1678
+ 1,
1679
+ 0,
1680
+ 0,
1681
+ 1,
1682
+ x,
1683
+ bottomYUp
1684
+ ], pageHeight)
1685
+ },
1686
+ ...fig
1687
+ });
1688
+ for (const t of laid.layout.texts) out.push({
1689
+ type: "line",
1690
+ line: t.line,
1691
+ originX: pt(x + t.x),
1692
+ baselineY: pt(pageHeight - (bottomYUp + t.y)),
1693
+ ...t.rotationDeg ? { rotationDeg: t.rotationDeg } : {},
1694
+ ...fig
1695
+ });
1696
+ return out;
1697
+ }
1698
+ function emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId) {
1699
+ if (sh.textLines.length === 0) return;
1700
+ if (sh.vertical) {
1701
+ const up = sh.vertical === "vert270";
1702
+ const innerHeight = Math.max(1, sh.heightPt - sh.insetTopPt - sh.insetBottomPt);
1703
+ let across = sh.anchor === "b" ? sh.widthPt - sh.insetRightPt - sh.textHeightPt : sh.anchor === "ctr" ? (sh.widthPt - sh.textHeightPt) / 2 : sh.insetLeftPt;
1704
+ for (const line of sh.textLines) {
1705
+ const h = computeLineHeight(line, line.resolved);
1706
+ const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerHeight);
1707
+ const off = lineBaselineOffset(line, line.resolved);
1708
+ const baselineAcross = up ? across + h - off : across + off;
1709
+ sink.push({
1710
+ type: "line",
1711
+ line,
1712
+ originX: pt(up ? x + baselineAcross : x + sh.widthPt - baselineAcross),
1713
+ baselineY: pt(pageHeight - (up ? bottomYUp + sh.insetBottomPt + lineOffset : bottomYUp + sh.heightPt - sh.insetTopPt - lineOffset)),
1714
+ rotationDeg: up ? 90 : -90,
1715
+ ...figId !== void 0 ? { structId: figId } : {}
1716
+ });
1717
+ across += h;
1718
+ }
1719
+ return;
1720
+ }
1721
+ const shapeTop = bottomYUp + sh.heightPt;
1722
+ const innerWidth = Math.max(1, sh.widthPt - sh.insetLeftPt - sh.insetRightPt);
1723
+ let textY;
1724
+ if (sh.anchor === "b") textY = bottomYUp + sh.insetBottomPt + sh.textHeightPt;
1725
+ else if (sh.anchor === "ctr") textY = bottomYUp + (sh.heightPt + sh.textHeightPt) / 2;
1726
+ else textY = shapeTop - sh.insetTopPt;
1727
+ sh.textLines.forEach((line, i) => {
1728
+ textY -= computeLineHeight(line, line.resolved);
1729
+ const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerWidth);
1730
+ const nested = sh.textCharts?.get(i);
1731
+ if (nested) {
1732
+ sink.push(...chartPageItems(nested, x + sh.insetLeftPt + lineOffset, textY, pageHeight, figId));
1733
+ return;
1734
+ }
1735
+ const drawing = sh.textShapes?.get(i);
1736
+ if (drawing) {
1737
+ emitShapeItems(drawing, x + sh.insetLeftPt + lineOffset, textY, sink, pageHeight, figId);
1738
+ return;
1739
+ }
1740
+ const table = sh.textTables?.get(i);
1741
+ if (table) {
1742
+ let rowY = textY + table.heightPt;
1743
+ for (const row of table.rows) {
1744
+ emitRowChunk(sink, row, x + sh.insetLeftPt + lineOffset + table.xOffsetPt, rowY, pageHeight, table.colCount);
1745
+ rowY -= row.heightPt;
1746
+ }
1747
+ return;
1748
+ }
1749
+ sink.push({
1750
+ type: "line",
1751
+ line,
1752
+ originX: pt(x + sh.insetLeftPt + lineOffset),
1753
+ baselineY: pt(pageHeight - (textY + lineBaselineOffset(line, line.resolved))),
1754
+ ...figId !== void 0 ? { structId: figId } : {}
1755
+ });
1756
+ textY -= sh.textLineGaps?.get(i) ?? 0;
1757
+ });
1758
+ }
1759
+ /** The page a table is being drawn onto, as a cell's floats need to see it. */
1760
+ function cellAnchorCtx(asm) {
1761
+ return {
1762
+ sink: (behind, zOrder) => PageAssembler.zSink(behind ? asm.floatsBehind : asm.floatsFront, zOrder),
1763
+ frame: asm.floatFrame(),
1764
+ exclude: (f, x, topYUp, widthPt, heightPt) => {
1765
+ asm.excludeFloat(f, x, topYUp, widthPt, heightPt);
1766
+ },
1767
+ pageHeight: asm.ctx.pageHeight
1768
+ };
1769
+ }
1770
+ function emitCellFloat(cf, ctx, frame) {
1771
+ const float = cf.block.float;
1772
+ if (!float) return;
1773
+ const x = floatXIn(frame, float, cf.block.widthPt);
1774
+ const topYUp = floatTopYUpIn(frame, float, cf.block.heightPt);
1775
+ const sink = ctx.sink(float.behind === true, float.zOrder);
1776
+ if (cf.block.kind === "shape") emitShapeItems(cf.block, x, topYUp - cf.block.heightPt, sink, ctx.pageHeight, void 0);
1777
+ else {
1778
+ const ins = cf.block.drawInset;
1779
+ sink.push({
1780
+ type: "image",
1781
+ x: pt(x + (ins?.dxPt ?? 0)),
1782
+ y: pt(ctx.pageHeight - (topYUp - (ins?.dyTopPt ?? 0))),
1783
+ width: pt(ins?.widthPt ?? cf.block.widthPt),
1784
+ height: pt(ins?.heightPt ?? cf.block.heightPt),
1785
+ imageResourceName: cf.block.resourceName,
1786
+ ...cf.block.crop ? { crop: cf.block.crop } : {},
1787
+ ...cf.block.wash ? { wash: cf.block.wash } : {},
1788
+ ...cf.block.rotationDeg ? { rotationDeg: cf.block.rotationDeg } : {},
1789
+ ...cf.block.flipH ? { flipH: true } : {},
1790
+ ...cf.block.flipV ? { flipV: true } : {}
1791
+ });
1792
+ if (cf.block.outline) sink.push(...pictureOutlineItems(cf.block.outline, x + (ins?.dxPt ?? 0), ctx.pageHeight - (topYUp - (ins?.dyTopPt ?? 0)), ins?.widthPt ?? cf.block.widthPt, ins?.heightPt ?? cf.block.heightPt, void 0));
1793
+ }
1794
+ if (float.wrap !== "none") ctx.exclude(float, x, topYUp, cf.block.widthPt, cf.block.heightPt);
1795
+ }
1796
+ function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
1797
+ if (sh.fillImageResourceName) sink.push({
1798
+ type: "image",
1799
+ x: pt(x),
1800
+ y: pt(pageHeight - (bottomYUp + sh.heightPt)),
1801
+ width: pt(sh.widthPt),
1802
+ height: pt(sh.heightPt),
1803
+ imageResourceName: sh.fillImageResourceName,
1804
+ ...sh.fillImageCrop ? { crop: sh.fillImageCrop } : {},
1805
+ clip: {
1806
+ paths: sh.paths,
1807
+ transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
1808
+ },
1809
+ ...figId !== void 0 ? { structId: figId } : {}
1810
+ });
1811
+ if (sh.markerPaths && sh.stroke) sink.push({
1812
+ type: "shape",
1813
+ shape: {
1814
+ paths: sh.markerPaths,
1815
+ fillColorHex: sh.stroke.colorHex,
1816
+ transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
1817
+ },
1818
+ ...figId !== void 0 ? { structId: figId } : {}
1819
+ });
1820
+ if (sh.paths.some((path) => path.segments.length > 0)) sink.push({
1821
+ type: "shape",
1822
+ shape: {
1823
+ paths: sh.paths,
1824
+ ...sh.fillColorHex ? { fillColorHex: sh.fillColorHex } : {},
1825
+ ...sh.fillGradient ? { fillGradient: sh.fillGradient } : {},
1826
+ ...sh.fillAlpha !== void 0 ? { fillAlpha: sh.fillAlpha } : {},
1827
+ ...sh.stroke ? { stroke: sh.stroke } : {},
1828
+ ...sh.shadow ? { shadow: sh.shadow } : {},
1829
+ transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
1830
+ },
1831
+ ...figId !== void 0 ? { structId: figId } : {}
1832
+ });
1833
+ for (const child of sh.children ?? []) emitShapeItems(child.laid, x + child.xPt, bottomYUp + sh.heightPt - child.yPt - child.laid.heightPt, sink, pageHeight, figId);
1834
+ emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId);
1835
+ }
1836
+ /**
1837
+ * §20.1.2.2.24 — the four rules of a picture's own frame, around the box the
1838
+ * picture is drawn in. Borders paint after images and before shapes, so the
1839
+ * frame lands on top of the picture the way Word draws it.
1840
+ *
1841
+ * @param outline The picture's frame.
1842
+ * @param x Left edge of the picture box.
1843
+ * @param topY Distance from the page TOP down to the box's top edge.
1844
+ * @param widthPt Box width.
1845
+ * @param heightPt Box height.
1846
+ * @param structId Tagged-PDF structure node the picture belongs to.
1847
+ * @returns One {@link BorderItem} per side.
1848
+ */
1849
+ function pictureOutlineItems(outline, x, topY, widthPt, heightPt, structId) {
1850
+ return [
1851
+ "top",
1852
+ "bottom",
1853
+ "left",
1854
+ "right"
1855
+ ].map((side) => ({
1856
+ type: "border",
1857
+ side,
1858
+ x: pt(x),
1859
+ y: pt(topY),
1860
+ width: pt(widthPt),
1861
+ height: pt(heightPt),
1862
+ borderSizePt: outline.widthPt,
1863
+ borderColorHex: outline.colorHex,
1864
+ ...structId !== void 0 ? { structId } : {}
1865
+ }));
1866
+ }
1867
+ function bandAnchor(float, widthPt, contentWidth, bandLeftPt = 0) {
1868
+ if (!float) return void 0;
1869
+ const align = float.posH?.align;
1870
+ if (align === "right") return contentWidth - widthPt;
1871
+ if (align === "center") return (contentWidth - widthPt) / 2;
1872
+ if (align === "left") return 0;
1873
+ const offset = float.posH?.offsetPt ?? 0;
1874
+ return float.posH?.relativeFrom === "page" ? offset - bandLeftPt : offset;
1875
+ }
1876
+ /**
1877
+ * §20.4.2.3 — how far DOWN the page a band's anchored drawing starts, in the
1878
+ * band emitter's y-UP frame. A `page`-relative anchor is measured from the
1879
+ * paper's own top edge, whatever the band's cursor has reached: n773061.docx
1880
+ * hangs its header text box 92pt down the page and, measured from the header's
1881
+ * first paragraph, it landed 60pt lower than Word and LibreOffice put it.
1882
+ *
1883
+ * @param float The anchor.
1884
+ * @param cursorY The band's cursor (y-up), where a paragraph-relative
1885
+ * anchor is measured from.
1886
+ * @param pageHeight The page height.
1887
+ * @returns The drawing's top edge, y-up.
1888
+ */
1889
+ function bandAnchorTop(float, cursorY, pageHeight, heightPt = 0, margins) {
1890
+ const align = float.posV?.align;
1891
+ if (align !== void 0) {
1892
+ const page = float.posV?.relativeFrom === "page" || !margins;
1893
+ const topYUp = page ? pageHeight : pageHeight - margins.top;
1894
+ const bottomYUp = page ? 0 : margins.bottom;
1895
+ if (align === "top") return topYUp;
1896
+ if (align === "bottom") return bottomYUp + heightPt;
1897
+ return bottomYUp + (topYUp - bottomYUp + heightPt) / 2;
1898
+ }
1899
+ const offset = float.posV?.offsetPt ?? 0;
1900
+ return float.posV?.relativeFrom === "page" ? pageHeight - offset : cursorY - offset;
1901
+ }
1902
+ function drawBlocksSequentially(blocks, startX, startY, pageHeight, contentWidth, structId, bandMargins) {
946
1903
  const out = [];
947
1904
  let cursorY = startY;
948
- for (const block of blocks) {
949
- if (block.kind !== "paragraph") continue;
1905
+ const isBehind = (b) => "float" in b && b.float?.behind === true;
1906
+ const ordered = blocks.some(isBehind) ? [...blocks.filter(isBehind), ...blocks.filter((b) => !isBehind(b))] : blocks;
1907
+ for (const block of ordered) {
1908
+ if (block.kind === "table") {
1909
+ const tableX = startX + block.xOffsetPt;
1910
+ for (const row of block.rows) {
1911
+ emitRowChunk(out, row, tableX, cursorY, pageHeight, block.colCount);
1912
+ cursorY -= row.heightPt;
1913
+ }
1914
+ continue;
1915
+ }
1916
+ if (block.kind === "chart") {
1917
+ cursorY -= block.spacingBeforePt + block.heightPt;
1918
+ const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
1919
+ out.push(...chartPageItems(block, startX + offset, cursorY, pageHeight, structId));
1920
+ cursorY -= block.spacingAfterPt;
1921
+ continue;
1922
+ }
1923
+ if (block.kind === "image") {
1924
+ cursorY -= block.spacingBeforePt;
1925
+ const anchored = bandAnchor(block.float, block.widthPt, contentWidth, startX);
1926
+ const offset = anchored ?? alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
1927
+ const top = anchored === void 0 || !block.float ? cursorY : bandAnchorTop(block.float, cursorY, pageHeight, block.heightPt, bandMargins);
1928
+ out.push({
1929
+ type: "image",
1930
+ x: pt(startX + offset),
1931
+ y: pt(pageHeight - top),
1932
+ width: pt(block.widthPt),
1933
+ height: pt(block.heightPt),
1934
+ imageResourceName: block.resourceName,
1935
+ ...block.crop ? { crop: block.crop } : {},
1936
+ ...block.wash ? { wash: block.wash } : {},
1937
+ ...block.rotationDeg ? { rotationDeg: block.rotationDeg } : {},
1938
+ ...block.flipH ? { flipH: true } : {},
1939
+ ...block.flipV ? { flipV: true } : {}
1940
+ });
1941
+ if (block.outline) out.push(...pictureOutlineItems(block.outline, startX + offset, pageHeight - top, block.widthPt, block.heightPt, structId));
1942
+ if (anchored === void 0) cursorY -= block.heightPt;
1943
+ cursorY -= block.spacingAfterPt;
1944
+ continue;
1945
+ }
1946
+ if (block.kind === "shape") {
1947
+ cursorY -= block.spacingBeforePt;
1948
+ const anchored = bandAnchor(block.float, block.widthPt, contentWidth, startX);
1949
+ const offset = anchored ?? alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
1950
+ const top = anchored === void 0 || !block.float ? cursorY : bandAnchorTop(block.float, cursorY, pageHeight, block.heightPt, bandMargins);
1951
+ emitShapeItems(block, startX + offset, top - block.heightPt, out, pageHeight, structId);
1952
+ if (anchored === void 0) cursorY -= block.heightPt;
1953
+ cursorY -= block.spacingAfterPt;
1954
+ continue;
1955
+ }
950
1956
  cursorY -= block.spacingBeforePt;
1957
+ const bandTopY = cursorY;
951
1958
  for (const line of block.lines) {
952
1959
  const h = computeLineHeight(line, block.resolved);
953
1960
  cursorY -= h;
@@ -957,14 +1964,60 @@ function drawBlocksSequentially(blocks, startX, startY, pageHeight, structId) {
957
1964
  type: "line",
958
1965
  line,
959
1966
  originX: pt(startX + indentLeft + offset),
960
- baselineY: pt(pageHeight - (cursorY + lineDescent(line))),
1967
+ baselineY: pt(pageHeight - (cursorY + lineBaselineOffset(line, block.resolved))),
961
1968
  ...structId !== void 0 ? { structId } : {}
962
1969
  });
963
1970
  }
1971
+ out.push(...paragraphDecoration(block.resolved, startX + block.resolved.indentLeft, startX + contentWidth - block.resolved.indentRight, bandTopY, cursorY, pageHeight));
964
1972
  cursorY -= block.spacingAfterPt;
965
1973
  }
966
1974
  return out;
967
1975
  }
1976
+ /**
1977
+ * §17.3.1.31 `w:shd` and §17.3.1.24 `w:pBdr` — the band behind a paragraph and
1978
+ * the rules around it, for a paragraph occupying `topY`…`bottomY` (both in the
1979
+ * y-up page frame) between `x0` and `x1`.
1980
+ *
1981
+ * @returns The fill and border items, in paint order.
1982
+ */
1983
+ function paragraphDecoration(resolved, x0, x1, topY, bottomY, pageHeight) {
1984
+ const out = [];
1985
+ const width = Math.max(0, x1 - x0);
1986
+ if (resolved.shading) out.push({
1987
+ type: "fill",
1988
+ x: pt(x0),
1989
+ y: pt(pageHeight - topY),
1990
+ width: pt(width),
1991
+ height: pt(Math.max(0, topY - bottomY)),
1992
+ fillColorHex: resolved.shading.colorHex
1993
+ });
1994
+ const borders = resolved.borders;
1995
+ if (!borders) return out;
1996
+ for (const side of [
1997
+ "top",
1998
+ "bottom",
1999
+ "left",
2000
+ "right"
2001
+ ]) {
2002
+ const edge = borders[side];
2003
+ if (!edge || edge.style === "none") continue;
2004
+ const gap = edge.spacePt ?? 0;
2005
+ const top = topY + gap;
2006
+ const bottom = bottomY - gap;
2007
+ out.push({
2008
+ type: "border",
2009
+ side,
2010
+ x: pt(x0),
2011
+ y: pt(pageHeight - top),
2012
+ width: pt(width),
2013
+ height: pt(Math.max(0, top - bottom)),
2014
+ borderSizePt: edge.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT,
2015
+ borderColorHex: edge.colorHex ?? "000000",
2016
+ ...edge.style !== "single" ? { borderStyle: edge.style } : {}
2017
+ });
2018
+ }
2019
+ return out;
2020
+ }
968
2021
  function runFontKeyAndParsed(options, ascii, bold, italic) {
969
2022
  if (ascii && options.embeddedFonts) {
970
2023
  const name = ascii.trim().toLowerCase();
@@ -1000,7 +2053,7 @@ function runFontKeyAndParsed(options, ascii, bold, italic) {
1000
2053
  function lookupFont(resources, fontKey) {
1001
2054
  return resources.get(fontKey) ?? resources.values().next().value;
1002
2055
  }
1003
- function collectFontResources(body, headersFooters, options) {
2056
+ function collectFontResources(body, headersFooters, options, imageResources) {
1004
2057
  const used = /* @__PURE__ */ new Map();
1005
2058
  const addRun = (run, para) => {
1006
2059
  const resolved = resolveRunProperties(run.properties, para.properties, options.styles);
@@ -1015,6 +2068,10 @@ function collectFontResources(body, headersFooters, options) {
1015
2068
  }
1016
2069
  const shaped = shapeText(run.text, parsed.glyphForCodepoint, parsed.advanceWidths, parsed.ligatures, parsed.kerning, parsed.joiningForms);
1017
2070
  for (const g of shaped.gids) bucket.gids.add(g);
2071
+ if (run.text.includes(" ")) for (const stop of resolveParagraphProperties(para.properties, options.styles).tabs) {
2072
+ const ch = stop.leader === void 0 ? void 0 : TAB_LEADER_CHARS.get(stop.leader);
2073
+ if (ch !== void 0) bucket.gids.add(parsed.glyphForCodepoint(ch.codePointAt(0)));
2074
+ }
1018
2075
  };
1019
2076
  const addMath = (node) => {
1020
2077
  for (const seg of mathGlyphSegments(node)) {
@@ -1031,30 +2088,48 @@ function collectFontResources(body, headersFooters, options) {
1031
2088
  for (const ch of seg.text) bucket.gids.add(parsed.glyphForCodepoint(ch.codePointAt(0)));
1032
2089
  }
1033
2090
  };
1034
- const visit = (elements) => {
1035
- for (const el of elements) if (el.kind === "paragraph") for (const run of el.paragraph.runs) {
1036
- if (run.math) {
1037
- addMath(run.math);
1038
- continue;
1039
- }
1040
- if (run.inlineImage && !run.text) continue;
1041
- if (run.field !== void 0) {
1042
- addRun({
1043
- text: `${run.text}0123456789`,
1044
- properties: run.properties
1045
- }, el.paragraph);
1046
- continue;
1047
- }
1048
- if (run.footnoteRef !== void 0 || run.endnoteRef !== void 0 || run.noteNumber) {
1049
- addRun({
1050
- text: `${run.text}0123456789 `,
1051
- properties: run.properties
1052
- }, el.paragraph);
1053
- continue;
2091
+ const addMetafile = (resource) => {
2092
+ const pic = resource ? imageResources.get(resource)?.metafile : void 0;
2093
+ for (const prim of pic?.prims ?? []) {
2094
+ if (prim.kind !== "text") continue;
2095
+ const reg = options.registry.resolveByStyle(prim.bold === true, prim.italic === true);
2096
+ let bucket = used.get(reg.variant);
2097
+ if (!bucket) {
2098
+ bucket = {
2099
+ parsed: reg.parsed,
2100
+ gids: /* @__PURE__ */ new Set()
2101
+ };
2102
+ used.set(reg.variant, bucket);
1054
2103
  }
1055
- addRun(run, el.paragraph);
2104
+ for (const ch of prim.text) bucket.gids.add(reg.parsed.glyphForCodepoint(ch.codePointAt(0)));
1056
2105
  }
1057
- else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) {
2106
+ };
2107
+ const visit = (elements) => {
2108
+ for (const el of elements) if (el.kind === "paragraph") {
2109
+ for (const run of el.paragraph.runs) addMetafile(run.inlineImage?.resource);
2110
+ for (const run of displayCapsRuns(el.paragraph, options)) {
2111
+ if (run.math) {
2112
+ addMath(run.math);
2113
+ continue;
2114
+ }
2115
+ if (run.inlineImage && !run.text) continue;
2116
+ if (run.field !== void 0) {
2117
+ addRun({
2118
+ text: `${run.text}0123456789`,
2119
+ properties: run.properties
2120
+ }, el.paragraph);
2121
+ continue;
2122
+ }
2123
+ if (run.footnoteRef !== void 0 || run.endnoteRef !== void 0 || run.noteNumber) {
2124
+ addRun({
2125
+ text: `${run.text}0123456789 `,
2126
+ properties: run.properties
2127
+ }, el.paragraph);
2128
+ continue;
2129
+ }
2130
+ addRun(run, el.paragraph);
2131
+ }
2132
+ } else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) {
1058
2133
  if (cell.properties.hashOnOverflow === true) for (const inner of cell.content) {
1059
2134
  if (inner.kind !== "paragraph") continue;
1060
2135
  for (const run of inner.paragraph.runs) addRun({
@@ -1065,8 +2140,13 @@ function collectFontResources(body, headersFooters, options) {
1065
2140
  visit(cell.content);
1066
2141
  }
1067
2142
  else if (el.kind === "shape") {
1068
- if (el.shape.text) visit(el.shape.text.content);
1069
- } else if (el.kind === "chart") {
2143
+ const shapeText = (sh) => {
2144
+ if (sh.text) visit(sh.text.content);
2145
+ for (const child of sh.children ?? []) shapeText(child.shape);
2146
+ };
2147
+ shapeText(el.shape);
2148
+ } else if (el.kind === "image") addMetafile(el.image.resource);
2149
+ else {
1070
2150
  const chart = options.charts?.get(el.chart.chartRelId);
1071
2151
  if (chart) {
1072
2152
  const reg = options.registry.resolveByStyle(false, false);
@@ -1093,6 +2173,16 @@ function collectFontResources(body, headersFooters, options) {
1093
2173
  }
1094
2174
  };
1095
2175
  visit(body);
2176
+ if ((options.sections ?? []).some((sec) => sec.properties.lineNumbering)) visit([{
2177
+ kind: "paragraph",
2178
+ paragraph: {
2179
+ properties: {},
2180
+ runs: [{
2181
+ text: "0123456789",
2182
+ properties: {}
2183
+ }]
2184
+ }
2185
+ }]);
1096
2186
  for (const hf of headersFooters.values()) visit(hf);
1097
2187
  for (const note of options.footnotes?.values() ?? []) visit(note);
1098
2188
  for (const note of options.endnotes?.values() ?? []) visit(note);
@@ -1135,17 +2225,83 @@ function collectFontResources(body, headersFooters, options) {
1135
2225
  }
1136
2226
  return out;
1137
2227
  }
2228
+ /**
2229
+ * The one line an empty paragraph still occupies — no tokens, but as tall as
2230
+ * the paragraph mark's own font (§17.3.1.31 `w:rPr` on the `w:pPr`, which is
2231
+ * exactly the run properties Word keeps for a paragraph that holds no runs).
2232
+ */
2233
+ function emptyLine(paragraph, options, resolved, availableWidthPt, fontResources) {
2234
+ const mark = resolveRunProperties(paragraph.properties.runProperties ?? {}, paragraph.properties, options.styles);
2235
+ const fontSizePt = mark.fontSizePt;
2236
+ const profile = options.layoutProfile ?? "ream";
2237
+ const { variant } = options.registry.resolveByStyle(mark.bold, mark.italic);
2238
+ const parsed = fontResources.get(variant)?.parsed;
2239
+ const metric = profile !== "ream" && parsed ? fontLeadingPt(parsed, fontSizePt, profile) : void 0;
2240
+ return {
2241
+ tokens: [],
2242
+ contentWidthPt: 0,
2243
+ maxFontSizePt: fontSizePt,
2244
+ availableWidthPt,
2245
+ firstLine: true,
2246
+ resolved,
2247
+ isLastInParagraph: true,
2248
+ ...metric && metric.ascentPt + metric.descentPt > 0 ? {
2249
+ metricHeightPt: metric.ascentPt + metric.descentPt + metric.lineGapPt,
2250
+ metricDescentPt: metric.descentPt
2251
+ } : {}
2252
+ };
2253
+ }
1138
2254
  function layoutParagraphBlock(paragraph, options, fontResources, imageResources, contentWidth, lineWidths) {
1139
- const baseResolved = resolveParagraphProperties(paragraph.properties, options.styles);
2255
+ const cascaded = resolveParagraphProperties(paragraph.properties, options.styles);
2256
+ const baseResolved = options.gridLinePitchPt !== void 0 && cascaded.snapToGrid !== false ? {
2257
+ ...cascaded,
2258
+ gridLinePitchPt: options.gridLinePitchPt
2259
+ } : cascaded;
2260
+ if (paragraph.properties.sectionBreak === true && paragraph.runs.length === 0) return {
2261
+ kind: "paragraph",
2262
+ resolved: baseResolved,
2263
+ lines: [],
2264
+ heightPt: 0,
2265
+ spacingBeforePt: 0,
2266
+ spacingAfterPt: 0
2267
+ };
1140
2268
  const baseDir = paragraphBaseDirection(paragraph, baseResolved);
1141
- const resolved = baseDir === "rtl" && baseResolved.alignment === "left" ? {
2269
+ const swapAlign = (a) => a === "left" ? "right" : a === "right" ? "left" : a;
2270
+ const resolved = baseResolved.bidi === true ? {
2271
+ ...baseResolved,
2272
+ alignment: swapAlign(baseResolved.alignment),
2273
+ indentLeft: baseResolved.indentRight,
2274
+ indentRight: baseResolved.indentLeft
2275
+ } : baseDir === "rtl" && baseResolved.alignment === "left" ? {
1142
2276
  ...baseResolved,
1143
2277
  alignment: "right"
1144
2278
  } : baseResolved;
1145
- const lines = wrap(tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir), paragraphMaxWidth(resolved, contentWidth, true), paragraphMaxWidth(resolved, contentWidth, false), resolved, options.hyphenator, options.layoutProfile ?? "ream", lineWidths);
2279
+ const tokens = tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir);
2280
+ const firstLineWidth = paragraphMaxWidth(resolved, contentWidth, true);
2281
+ const otherWidth = paragraphMaxWidth(resolved, contentWidth, false);
2282
+ const runWrap = (part, firstWidth, widths) => wrap(part, firstWidth, otherWidth, resolved, options.hyphenator, options.layoutProfile ?? "ream", options.doNotExpandShiftReturn === true, widths);
2283
+ const parts = splitAtColumnBreaks(tokens);
2284
+ const columnBreakLines = /* @__PURE__ */ new Set();
2285
+ let wrapped;
2286
+ if (parts.length === 1) wrapped = runWrap(tokens, firstLineWidth, lineWidths);
2287
+ else {
2288
+ wrapped = [];
2289
+ for (const [i, part] of parts.entries()) {
2290
+ const lines = runWrap(part, i === 0 ? firstLineWidth : otherWidth, lineWidths?.slice(wrapped.length));
2291
+ if (i > 0 && lines.length > 0) columnBreakLines.add(wrapped.length);
2292
+ wrapped.push(...i === 0 ? lines : lines.map((l) => ({
2293
+ ...l,
2294
+ firstLine: false
2295
+ })));
2296
+ }
2297
+ }
2298
+ const lines = wrapped.length > 0 ? wrapped : [emptyLine(paragraph, options, resolved, Math.max(firstLineWidth, 0), fontResources)];
1146
2299
  let heightPt = 0;
1147
2300
  for (const line of lines) heightPt += computeLineHeight(line, resolved);
1148
2301
  const numbering = paragraph.properties.numbering;
2302
+ const breakIdx = paragraph.runs.findIndex((r) => r.pageBreak === true);
2303
+ const leadingPageBreak = breakIdx >= 0 && paragraph.runs.slice(0, breakIdx + 1).every((r) => r.text === "" && r.inlineImage === void 0);
2304
+ const trailingPageBreak = paragraph.runs.some((r, i) => r.pageBreak === true && (!leadingPageBreak || i > breakIdx));
1149
2305
  return {
1150
2306
  kind: "paragraph",
1151
2307
  resolved,
@@ -1157,7 +2313,9 @@ function layoutParagraphBlock(paragraph, options, fontResources, imageResources,
1157
2313
  numId: numbering.numId,
1158
2314
  level: numbering.ilvl
1159
2315
  } } : {},
1160
- ...paragraph.runs.some((r) => r.pageBreak) ? { pageBreakAfter: true } : {},
2316
+ ...leadingPageBreak ? { pageBreakBefore: true } : {},
2317
+ ...trailingPageBreak ? { pageBreakAfter: true } : {},
2318
+ ...columnBreakLines.size > 0 ? { columnBreakLines } : {},
1161
2319
  ...paragraph.bookmarks && paragraph.bookmarks.length > 0 ? { bookmarks: paragraph.bookmarks } : {},
1162
2320
  source: paragraph
1163
2321
  };
@@ -1197,13 +2355,76 @@ function resolveMathItems(items, fontFor) {
1197
2355
  };
1198
2356
  });
1199
2357
  }
2358
+ /**
2359
+ * §17.3.2.5 `w:caps` / §17.3.2.33 `w:smallCaps` — the paragraph's runs as they
2360
+ * are DISPLAYED. A capitals run is upper-cased; a small-capitals run is too,
2361
+ * and the letters that were lower case are split out to be set smaller.
2362
+ *
2363
+ * @param paragraph The paragraph.
2364
+ * @param options The render options (for the style cascade).
2365
+ * @returns The runs to lay out — the same array when none asks for capitals.
2366
+ */
2367
+ function displayCapsRuns(paragraph, options) {
2368
+ let any = false;
2369
+ const out = [];
2370
+ for (const raw of paragraph.runs) {
2371
+ const run = raw.text.includes(SOFT_HYPHEN) ? (any = true, {
2372
+ ...raw,
2373
+ text: raw.text.replaceAll(SOFT_HYPHEN, "")
2374
+ }) : raw;
2375
+ const resolved = run.text.length > 0 ? resolveRunProperties(run.properties, paragraph.properties, options.styles) : void 0;
2376
+ if (!resolved || !resolved.caps && !resolved.smallCaps) {
2377
+ out.push(run);
2378
+ continue;
2379
+ }
2380
+ any = true;
2381
+ if (resolved.caps) {
2382
+ out.push({
2383
+ ...run,
2384
+ text: run.text.toUpperCase()
2385
+ });
2386
+ continue;
2387
+ }
2388
+ let start = 0;
2389
+ const wasLower = (i) => {
2390
+ const ch = run.text[i];
2391
+ return ch.toLowerCase() === ch && ch.toUpperCase() !== ch;
2392
+ };
2393
+ for (let i = 1; i <= run.text.length; i++) {
2394
+ if (i < run.text.length && wasLower(i) === wasLower(start)) continue;
2395
+ const segment = run.text.slice(start, i).toUpperCase();
2396
+ out.push(wasLower(start) ? {
2397
+ ...run,
2398
+ text: segment,
2399
+ properties: {
2400
+ ...run.properties,
2401
+ fontSizePt: pt(resolved.fontSizePt * .8)
2402
+ }
2403
+ } : {
2404
+ ...run,
2405
+ text: segment
2406
+ });
2407
+ start = i;
2408
+ }
2409
+ }
2410
+ return any ? out : paragraph.runs;
2411
+ }
1200
2412
  function tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir) {
1201
- const plans = paragraph.runs.map((run) => {
2413
+ const plans = displayCapsRuns(paragraph, options).map((run) => {
1202
2414
  if (run.inlineImage) {
1203
- const naturalW = run.inlineImage.width;
2415
+ const eff = run.inlineImage.effectExtent;
2416
+ const padW = (eff?.leftPt ?? 0) + (eff?.rightPt ?? 0);
2417
+ const padH = (eff?.topPt ?? 0) + (eff?.bottomPt ?? 0);
2418
+ const naturalW = run.inlineImage.width + padW;
1204
2419
  const widthPt = Math.min(naturalW, contentWidth);
1205
2420
  const scale = naturalW > 0 ? widthPt / naturalW : 1;
1206
- const heightPt = run.inlineImage.height * scale;
2421
+ const heightPt = (run.inlineImage.height + padH) * scale;
2422
+ const drawBox = eff ? {
2423
+ dxPt: eff.leftPt * scale,
2424
+ dyPt: eff.bottomPt * scale,
2425
+ widthPt: run.inlineImage.width * scale,
2426
+ heightPt: run.inlineImage.height * scale
2427
+ } : void 0;
1207
2428
  const res = run.inlineImage.resource ? imageResources?.get(run.inlineImage.resource) : void 0;
1208
2429
  const resolvedRun = resolveRunProperties(run.properties, paragraph.properties, options.styles);
1209
2430
  const { fontKey } = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
@@ -1215,7 +2436,16 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
1215
2436
  isImage: true,
1216
2437
  imageWidthPt: widthPt,
1217
2438
  imageHeightPt: heightPt,
1218
- imageResourceName: res?.resourceName ?? ""
2439
+ imageResourceName: res?.resourceName ?? "",
2440
+ ...res?.metafile ? { imageMetafile: metafileDrawing(res.metafile, drawBox?.widthPt ?? widthPt, drawBox?.heightPt ?? heightPt, options, fontResources) } : {},
2441
+ ...run.inlineImage.outline ? { imageOutline: run.inlineImage.outline } : {},
2442
+ ...run.inlineImage.shadow ? { imageShadow: run.inlineImage.shadow } : {},
2443
+ ...run.inlineImage.wash ? { imageWash: run.inlineImage.wash } : {},
2444
+ ...run.inlineImage.crop ? { imageCrop: run.inlineImage.crop } : {},
2445
+ ...run.inlineImage.rotation60k ? { imageRotationDeg: run.inlineImage.rotation60k / 6e4 } : {},
2446
+ ...run.inlineImage.flipH ? { imageFlipH: true } : {},
2447
+ ...run.inlineImage.flipV ? { imageFlipV: true } : {},
2448
+ ...drawBox ? { imageDrawBox: drawBox } : {}
1219
2449
  };
1220
2450
  }
1221
2451
  if (run.math) {
@@ -1270,6 +2500,19 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
1270
2500
  ...plan.resolvedRun.rtl ? { rtl: true } : {}
1271
2501
  })), baseDir));
1272
2502
  }
2503
+ /**
2504
+ * Split a paragraph's tokens at every §17.3.3.1 column break, dropping the
2505
+ * break token itself (its line ends there anyway). One part ⇒ no break.
2506
+ *
2507
+ * @param tokens The paragraph's tokens in order.
2508
+ * @returns The parts, in order; always at least one.
2509
+ */
2510
+ function splitAtColumnBreaks(tokens) {
2511
+ const parts = [[]];
2512
+ for (const t of tokens) if (t.kind === "text" && t.columnBreak) parts.push([]);
2513
+ else parts[parts.length - 1].push(t);
2514
+ return parts.filter((p, i) => i === 0 || p.length > 0);
2515
+ }
1273
2516
  function tokenizePlansLtr(plans) {
1274
2517
  const tokens = [];
1275
2518
  for (const plan of plans) {
@@ -1279,6 +2522,15 @@ function tokenizePlansLtr(plans) {
1279
2522
  imageResourceName: plan.imageResourceName,
1280
2523
  widthPt: plan.imageWidthPt,
1281
2524
  heightPt: plan.imageHeightPt,
2525
+ ...plan.imageOutline ? { outline: plan.imageOutline } : {},
2526
+ ...plan.imageShadow ? { shadow: plan.imageShadow } : {},
2527
+ ...plan.imageWash ? { wash: plan.imageWash } : {},
2528
+ ...plan.imageMetafile ? { metafile: plan.imageMetafile } : {},
2529
+ ...plan.imageCrop ? { crop: plan.imageCrop } : {},
2530
+ ...plan.imageRotationDeg ? { rotationDeg: plan.imageRotationDeg } : {},
2531
+ ...plan.imageFlipH ? { flipH: true } : {},
2532
+ ...plan.imageFlipV ? { flipV: true } : {},
2533
+ ...plan.imageDrawBox ? { drawBox: plan.imageDrawBox } : {},
1282
2534
  isSpace: false,
1283
2535
  bidiLevel: 0
1284
2536
  });
@@ -1301,21 +2553,37 @@ function tokenizePlansLtr(plans) {
1301
2553
  kind: "text",
1302
2554
  text: t.text,
1303
2555
  isSpace: t.isSpace,
2556
+ ...t.tab ? { tab: true } : {},
1304
2557
  ...plan.run.href !== void 0 ? { href: plan.run.href } : {},
1305
2558
  ...plan.run.footnoteRef !== void 0 ? { footnoteRef: plan.run.footnoteRef } : {},
1306
2559
  ...plan.run.anchor !== void 0 ? { anchor: plan.run.anchor } : {},
1307
2560
  ...plan.run.listMarker ? { listMarker: true } : {},
2561
+ ...plan.run.columnBreak ? { columnBreak: true } : {},
1308
2562
  ...highlight ? { highlight: true } : {},
1309
2563
  resolvedRun: plan.resolvedRun,
1310
2564
  font: plan.font,
1311
2565
  fontSizePt: plan.fontSizePt,
1312
2566
  ...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
1313
- widthPt: plan.font.measure.textWidthPt(t.text, plan.fontSizePt),
2567
+ widthPt: measureRunText(plan, t.text),
1314
2568
  bidiLevel: 0
1315
2569
  });
1316
2570
  }
1317
2571
  return tokens;
1318
2572
  }
2573
+ /**
2574
+ * §17.3.2.35 — a run's text measured with the space it puts between its
2575
+ * characters. PDF adds the same amount per glyph through `Tc`, so the width the
2576
+ * line is broken at and the width the page draws agree.
2577
+ *
2578
+ * @param plan The run being tokenized.
2579
+ * @param text The token's text.
2580
+ * @returns The advance width in points.
2581
+ */
2582
+ function measureRunText(plan, text) {
2583
+ const base = plan.font.measure.textWidthPt(text, plan.fontSizePt);
2584
+ const extra = plan.resolvedRun.letterSpacingPt;
2585
+ return extra === void 0 || extra === 0 ? base : base + extra * [...text].length;
2586
+ }
1319
2587
  function tokenizePlansBidi(plans, realLevels) {
1320
2588
  const tokens = [];
1321
2589
  let realIdx = 0;
@@ -1327,6 +2595,15 @@ function tokenizePlansBidi(plans, realLevels) {
1327
2595
  imageResourceName: plan.imageResourceName,
1328
2596
  widthPt: plan.imageWidthPt,
1329
2597
  heightPt: plan.imageHeightPt,
2598
+ ...plan.imageOutline ? { outline: plan.imageOutline } : {},
2599
+ ...plan.imageShadow ? { shadow: plan.imageShadow } : {},
2600
+ ...plan.imageWash ? { wash: plan.imageWash } : {},
2601
+ ...plan.imageMetafile ? { metafile: plan.imageMetafile } : {},
2602
+ ...plan.imageCrop ? { crop: plan.imageCrop } : {},
2603
+ ...plan.imageRotationDeg ? { rotationDeg: plan.imageRotationDeg } : {},
2604
+ ...plan.imageFlipH ? { flipH: true } : {},
2605
+ ...plan.imageFlipV ? { flipV: true } : {},
2606
+ ...plan.imageDrawBox ? { drawBox: plan.imageDrawBox } : {},
1330
2607
  isSpace: false,
1331
2608
  bidiLevel: realLevels[realIdx] ?? 0
1332
2609
  });
@@ -1361,12 +2638,13 @@ function tokenizePlansBidi(plans, realLevels) {
1361
2638
  ...plan.run.footnoteRef !== void 0 ? { footnoteRef: plan.run.footnoteRef } : {},
1362
2639
  ...plan.run.anchor !== void 0 ? { anchor: plan.run.anchor } : {},
1363
2640
  ...plan.run.listMarker ? { listMarker: true } : {},
2641
+ ...plan.run.columnBreak ? { columnBreak: true } : {},
1364
2642
  ...(plan.run.commentRangeRefs?.length ?? 0) > 0 ? { highlight: true } : {},
1365
2643
  resolvedRun: plan.resolvedRun,
1366
2644
  font: plan.font,
1367
2645
  fontSizePt: plan.fontSizePt,
1368
2646
  ...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
1369
- widthPt: plan.font.measure.textWidthPt(text, plan.fontSizePt),
2647
+ widthPt: measureRunText(plan, text),
1370
2648
  bidiLevel: curLevel
1371
2649
  });
1372
2650
  };
@@ -1393,6 +2671,7 @@ function paragraphBaseDirection(paragraph, resolved) {
1393
2671
  if (resolved.bidi) return "rtl";
1394
2672
  let text = "";
1395
2673
  for (const run of paragraph.runs) {
2674
+ if (run.listMarker === true) continue;
1396
2675
  if (!run.inlineImage) text += run.text;
1397
2676
  if (text.length > 64) break;
1398
2677
  }
@@ -1405,16 +2684,29 @@ function paragraphMaxWidth(p, contentWidth, firstLine) {
1405
2684
  const firstLineExtra = firstLine ? p.indentFirstLine : 0;
1406
2685
  return Math.max(1, contentWidth - indentLeft - indentRight - firstLineExtra);
1407
2686
  }
2687
+ var HYPHENS = new Set([
2688
+ 45,
2689
+ 8208,
2690
+ 8211,
2691
+ 8212
2692
+ ]);
2693
+ /** §17.3.3.29 — the optional hyphen, printed only where a line breaks on it. */
2694
+ var SOFT_HYPHEN = "­";
1408
2695
  function tokenizeText(text) {
1409
2696
  const out = [];
1410
2697
  if (text.length === 0) return out;
1411
- const re = /(\s+)|(\S+)/g;
2698
+ const re = /(\t)|(\s+)|(\S+)/g;
1412
2699
  let m;
1413
2700
  while ((m = re.exec(text)) !== null) if (m[1] !== void 0) out.push({
1414
- text: m[1],
2701
+ text: "",
2702
+ isSpace: true,
2703
+ tab: true
2704
+ });
2705
+ else if (m[2] !== void 0) out.push({
2706
+ text: m[2],
1415
2707
  isSpace: true
1416
2708
  });
1417
- else if (m[2] !== void 0) for (const piece of splitCjkSegment(m[2])) out.push({
2709
+ else if (m[3] !== void 0) for (const piece of splitCjkSegment(m[3])) out.push({
1418
2710
  text: piece,
1419
2711
  isSpace: false
1420
2712
  });
@@ -1428,14 +2720,51 @@ function lastCodePoint(s) {
1428
2720
  function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1429
2721
  const entries = [];
1430
2722
  let prevBoxEndCp = null;
2723
+ let prevWasImage = false;
1431
2724
  for (const tok of tokens) {
2725
+ const breaks = tok.isSpace ? tok.text.match(/\r\n|[\r\n]/gu)?.length ?? 0 : 0;
2726
+ if (breaks > 0) {
2727
+ for (let i = 0; i < breaks; i++) {
2728
+ entries.push({
2729
+ item: {
2730
+ type: "glue",
2731
+ width: 0,
2732
+ stretch: 1e6,
2733
+ shrink: 0
2734
+ },
2735
+ token: null
2736
+ });
2737
+ entries.push({
2738
+ item: {
2739
+ type: "penalty",
2740
+ width: 0,
2741
+ penalty: FORCED_BREAK,
2742
+ flagged: false
2743
+ },
2744
+ token: null,
2745
+ softBreak: true
2746
+ });
2747
+ }
2748
+ prevBoxEndCp = null;
2749
+ continue;
2750
+ }
1432
2751
  if (tok.isSpace || tok.kind === "image" || tok.kind === "math") {
2752
+ if (tok.kind === "image" && (prevWasImage || prevBoxEndCp !== null)) entries.push({
2753
+ item: {
2754
+ type: "penalty",
2755
+ width: 0,
2756
+ penalty: 0,
2757
+ flagged: false
2758
+ },
2759
+ token: null
2760
+ });
2761
+ prevWasImage = tok.kind === "image";
1433
2762
  entries.push({
1434
2763
  item: tok.isSpace ? {
1435
2764
  type: "glue",
1436
2765
  width: tok.widthPt,
1437
2766
  stretch: tok.widthPt * .6,
1438
- shrink: tok.widthPt * .3
2767
+ shrink: tok.widthPt * GLUE_SHRINK_RATIO
1439
2768
  } : {
1440
2769
  type: "box",
1441
2770
  width: tok.widthPt
@@ -1446,7 +2775,7 @@ function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1446
2775
  continue;
1447
2776
  }
1448
2777
  const startCp = tok.text.codePointAt(0) ?? 0;
1449
- if (prevBoxEndCp !== null && cjkBreakBetween(prevBoxEndCp, startCp)) entries.push({
2778
+ if (prevWasImage || prevBoxEndCp !== null && cjkBreakBetween(prevBoxEndCp, startCp)) entries.push({
1450
2779
  item: {
1451
2780
  type: "penalty",
1452
2781
  width: 0,
@@ -1455,10 +2784,12 @@ function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1455
2784
  },
1456
2785
  token: null
1457
2786
  });
2787
+ prevWasImage = false;
1458
2788
  prevBoxEndCp = lastCodePoint(tok.text);
1459
2789
  const positions = hyphenator ? hyphenator.hyphenate(tok.text) : [];
1460
2790
  if (positions.length === 0) {
1461
- const chunks = maxLineWidthPt !== void 0 && maxLineWidthPt > 0 && tok.widthPt > maxLineWidthPt * 1.1 ? splitToWidth(tok, maxLineWidthPt) : void 0;
2791
+ const tooWide = maxLineWidthPt !== void 0 && maxLineWidthPt > 0;
2792
+ const chunks = (tooWide && tok.widthPt > maxLineWidthPt ? splitAtHyphens(tok) : void 0) ?? (tooWide && tok.widthPt > maxLineWidthPt * 1.1 ? splitToWidth(tok, maxLineWidthPt) : void 0);
1462
2793
  if (chunks) {
1463
2794
  chunks.forEach((chunk, ci) => {
1464
2795
  if (ci > 0) entries.push({
@@ -1545,6 +2876,94 @@ function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1545
2876
  });
1546
2877
  return entries;
1547
2878
  }
2879
+ /** Word's own default when a document names none (§17.15.1.25): half an inch. */
2880
+ var DEFAULT_TAB_STOP_PT = 36;
2881
+ var TAB_LEADER_CHARS = new Map([
2882
+ ["dot", "."],
2883
+ ["hyphen", "-"],
2884
+ ["underscore", "_"],
2885
+ ["middleDot", "·"]
2886
+ ]);
2887
+ /**
2888
+ * Give each tab on a line the width that carries it to its stop, and the
2889
+ * leader that fills the way there.
2890
+ *
2891
+ * §17.3.1.37 — a tab advances to a POSITION, not by a fixed amount, and where
2892
+ * that position is depends on how far the line has already run. Measured as
2893
+ * ordinary whitespace it collapsed to a space: the page numbers of
2894
+ * FDO77715.docx's index sat against their titles with no dot leader between,
2895
+ * and its header ran its left and right halves together on one line.
2896
+ *
2897
+ * A `right`, `center` or `decimal` stop positions the text AFTER the tab, so
2898
+ * each is resolved against the segment that follows it.
2899
+ *
2900
+ * @param tokens The line's tokens, replaced in place.
2901
+ * @param resolved The paragraph's resolved properties: the stops and indents.
2902
+ * @param isFirst Whether this is the paragraph's first line.
2903
+ */
2904
+ function resolveTabs(tokens, resolved, isFirst, availableWidthPt) {
2905
+ const stops = resolved.tabs;
2906
+ let x = resolved.indentLeft + (isFirst ? resolved.indentFirstLine : 0);
2907
+ for (let i = 0; i < tokens.length; i++) {
2908
+ const tok = tokens[i];
2909
+ if (!tok) continue;
2910
+ if (!(tok.kind === "text" && tok.tab === true)) {
2911
+ x += tok.widthPt;
2912
+ continue;
2913
+ }
2914
+ let segment = 0;
2915
+ let decimalAt = 0;
2916
+ let seenDecimal = false;
2917
+ for (let j = i + 1; j < tokens.length; j++) {
2918
+ const next = tokens[j];
2919
+ if (next.kind === "text" && next.tab === true) break;
2920
+ if (!seenDecimal && next.kind === "text") {
2921
+ const dot = next.text.indexOf(".");
2922
+ if (dot >= 0) {
2923
+ decimalAt = segment + next.font.measure.textWidthPt(next.text.slice(0, dot), next.fontSizePt);
2924
+ seenDecimal = true;
2925
+ }
2926
+ }
2927
+ segment += next.widthPt;
2928
+ }
2929
+ const stopAt = (sp) => sp.relativeTo === "center" ? availableWidthPt / 2 : sp.relativeTo === "right" ? availableWidthPt : sp.positionPt;
2930
+ const stop = stops.find((sp) => stopAt(sp) > x + .01);
2931
+ const hangingStop = isFirst && resolved.indentFirstLine < 0 && resolved.indentLeft > x + .01 ? resolved.indentLeft : void 0;
2932
+ const explicitAt = stop ? stopAt(stop) : void 0;
2933
+ const position = explicitAt !== void 0 && (hangingStop === void 0 || explicitAt <= hangingStop) ? explicitAt : hangingStop ?? (Math.floor(x / DEFAULT_TAB_STOP_PT) + 1) * DEFAULT_TAB_STOP_PT;
2934
+ const alignment = stop?.alignment ?? "left";
2935
+ if (segment === 0 && alignment !== "left") {
2936
+ const after = tokens[i + 1];
2937
+ if (after?.kind === "text" && after.tab === true) {
2938
+ tokens[i] = {
2939
+ ...tok,
2940
+ widthPt: 0,
2941
+ text: ""
2942
+ };
2943
+ continue;
2944
+ }
2945
+ }
2946
+ let target = position;
2947
+ if (alignment === "right") target = position - segment;
2948
+ else if (alignment === "center") target = position - segment / 2;
2949
+ else if (alignment === "decimal") target = position - (seenDecimal ? decimalAt : segment);
2950
+ const width = Math.max(0, target - x);
2951
+ const leaderChar = stop?.leader === void 0 ? void 0 : TAB_LEADER_CHARS.get(stop.leader);
2952
+ const text = leaderChar === void 0 ? "" : fillLeader(tok, leaderChar, width);
2953
+ tokens[i] = {
2954
+ ...tok,
2955
+ widthPt: width,
2956
+ text
2957
+ };
2958
+ x += width;
2959
+ }
2960
+ }
2961
+ /** As many leader characters as fit the gap, measured in the tab's own font. */
2962
+ function fillLeader(tab, leader, widthPt) {
2963
+ const unit = tab.font.measure.textWidthPt(leader, tab.fontSizePt);
2964
+ if (!(unit > 0)) return "";
2965
+ return leader.repeat(Math.max(0, Math.floor(widthPt / unit)));
2966
+ }
1548
2967
  /**
1549
2968
  * Cut an unbreakable word into the widest pieces that fit `widthPt`, measured
1550
2969
  * with the token's own font. Each piece keeps the run's formatting, so the line
@@ -1555,6 +2974,24 @@ function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1555
2974
  * @returns The pieces in order, or undefined when even one character overruns
1556
2975
  * (nothing can be gained by splitting then).
1557
2976
  */
2977
+ function splitAtHyphens(tok) {
2978
+ if (tok.kind !== "text") return void 0;
2979
+ const parts = [];
2980
+ let start = 0;
2981
+ for (let i = 0; i < tok.text.length; i++) {
2982
+ if (!HYPHENS.has(tok.text.codePointAt(i) ?? 0)) continue;
2983
+ if (i === start || i === tok.text.length - 1) continue;
2984
+ parts.push(tok.text.slice(start, i + 1));
2985
+ start = i + 1;
2986
+ }
2987
+ if (parts.length === 0) return void 0;
2988
+ parts.push(tok.text.slice(start));
2989
+ return parts.map((text) => ({
2990
+ ...tok,
2991
+ text,
2992
+ widthPt: tok.font.measure.textWidthPt(text, tok.fontSizePt)
2993
+ }));
2994
+ }
1558
2995
  function splitToWidth(tok, widthPt) {
1559
2996
  if (tok.kind !== "text") return void 0;
1560
2997
  const chars = [...tok.text];
@@ -1585,18 +3022,19 @@ function splitToWidth(tok, widthPt) {
1585
3022
  }
1586
3023
  /** The fewest characters a mid-word break may leave on a line. */
1587
3024
  var MIN_SPLIT_CHARS = 6;
1588
- function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved, profile) {
3025
+ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved, profile, allowEmpty = false) {
1589
3026
  let st = start;
1590
3027
  let et = breakIdx;
1591
3028
  while (st < et && (entries[st].token === null || !isFirst && entries[st].token?.isSpace === true)) st++;
1592
3029
  const paints = (t) => t !== null && t !== void 0 && t.kind === "text" && (t.resolvedRun.underline !== "none" || t.resolvedRun.strike);
1593
3030
  while (et > st && (entries[et - 1].token === null || entries[et - 1].token?.isSpace === true && !paints(entries[et - 1].token))) et--;
1594
- if (st >= et) return null;
3031
+ if (st >= et && !allowEmpty) return null;
1595
3032
  const lineTokens = [];
1596
3033
  for (let i = st; i < et; i++) {
1597
3034
  const ft = entries[i].token;
1598
3035
  if (ft) lineTokens.push(ft);
1599
3036
  }
3037
+ resolveTabs(lineTokens, resolved, isFirst, availableWidthPt);
1600
3038
  const hyphenWidth = entries[breakIdx]?.hyphenWidthPt;
1601
3039
  if (hyphenWidth !== void 0) {
1602
3040
  const lastIdx = lineTokens.length - 1;
@@ -1616,8 +3054,10 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
1616
3054
  let metricAscent = 0;
1617
3055
  let metricDescent = 0;
1618
3056
  let metricLineGap = 0;
3057
+ const prints = lineTokens.some((t) => !t.isSpace);
1619
3058
  for (const t of lineTokens) {
1620
3059
  contentWidth += t.widthPt;
3060
+ if (prints && t.isSpace) continue;
1621
3061
  const sz = tokenLineSize(t);
1622
3062
  if (sz > maxSize) maxSize = sz;
1623
3063
  if (t.kind === "math") {
@@ -1647,10 +3087,26 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
1647
3087
  } : {}
1648
3088
  };
1649
3089
  }
1650
- function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile, lineWidths) {
3090
+ function withLeadingTabWidths(tokens, resolved, firstLineWidth) {
3091
+ const first = tokens[0];
3092
+ if (!(first?.kind === "text" && first.tab === true)) return tokens;
3093
+ const probe = tokens.map((t) => ({ ...t }));
3094
+ resolveTabs(probe, resolved, true, firstLineWidth);
3095
+ const out = tokens.slice();
3096
+ for (let i = 0; i < out.length; i++) {
3097
+ const tok = out[i];
3098
+ if (!(tok.kind === "text" && tok.tab === true)) break;
3099
+ out[i] = {
3100
+ ...tok,
3101
+ widthPt: probe[i].widthPt
3102
+ };
3103
+ }
3104
+ return out;
3105
+ }
3106
+ function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile, noExpandShiftReturn, lineWidths) {
1651
3107
  if (tokens.length === 0) return [];
1652
3108
  const widths = lineWidths ?? [firstLineWidth, otherWidth];
1653
- const entries = paragraphItemStream(tokens, hyphenator, Math.max(0, ...widths));
3109
+ const entries = paragraphItemStream(withLeadingTabWidths(tokens, resolved, widths[0] ?? firstLineWidth), hyphenator, Math.max(0, ...widths));
1654
3110
  const items = entries.map((e) => e.item);
1655
3111
  const breaks = profile === "ream" ? breakLines(items, widths).breaks : greedyBreakLines(items, widths);
1656
3112
  const buildLines = (at) => {
@@ -1658,10 +3114,13 @@ function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile,
1658
3114
  let start = 0;
1659
3115
  let isFirst = true;
1660
3116
  let lineIdx = 0;
1661
- for (const breakIdx of at) {
3117
+ for (const [i, breakIdx] of at.entries()) {
1662
3118
  const width = lineIdx < widths.length ? widths[lineIdx] : widths[widths.length - 1];
1663
- const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved, profile);
1664
- if (line) out.push(line);
3119
+ const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved, profile, i < at.length - 1);
3120
+ if (line) out.push(noExpandShiftReturn && entries[breakIdx]?.softBreak ? {
3121
+ ...line,
3122
+ noJustify: true
3123
+ } : line);
1665
3124
  start = breakIdx + 1;
1666
3125
  isFirst = false;
1667
3126
  lineIdx++;
@@ -1695,11 +3154,17 @@ function fontLeadingPt(parsed, sizePt, profile) {
1695
3154
  };
1696
3155
  }
1697
3156
  function computeLineHeight(line, p) {
3157
+ const natural = naturalLineHeight(line, p);
3158
+ const pitch = p.gridLinePitchPt;
3159
+ if (pitch !== void 0 && pitch > 0) return Math.ceil(natural / pitch - 1e-6) * pitch;
3160
+ return natural;
3161
+ }
3162
+ function naturalLineHeight(line, p) {
1698
3163
  const fontSize = line.maxFontSizePt || 12;
1699
3164
  const natural = line.metricHeightPt ?? fontSize * 1.2;
1700
3165
  const mathH = (line.mathAscentPt ?? 0) + (line.mathDescentPt ?? 0);
1701
3166
  const mathNeed = mathH > 0 ? mathH * 1.05 : 0;
1702
- if (p.spacingLineRule === "exact" && p.spacingLine > 0) return Math.max(p.spacingLine, mathNeed);
3167
+ if (p.spacingLineRule === "exact" && p.spacingLine >= 0) return Math.max(p.spacingLine, mathNeed);
1703
3168
  if (p.spacingLineRule === "atLeast" && p.spacingLine > 0) return Math.max(natural, p.spacingLine, mathNeed);
1704
3169
  const lineTwips = Math.round(p.spacingLine * 20);
1705
3170
  const multiple = lineTwips > 0 ? lineTwips / 240 : 1;
@@ -1710,6 +3175,37 @@ function lineDescent(line) {
1710
3175
  const base = line.metricDescentPt ?? fs * .2;
1711
3176
  return Math.max(base, line.mathDescentPt ?? 0);
1712
3177
  }
3178
+ /**
3179
+ * §17.3.1.33 — how far the baseline stands above the BOTTOM of the line box,
3180
+ * which is what says where the extra height of a spaced-out line goes.
3181
+ *
3182
+ * A line given a fixed height (`exact`, `atLeast`) hangs from the bottom of its
3183
+ * box: the space a taller `w:line` asks for opens ABOVE the text, which is what
3184
+ * makes "at least 24pt" push a heading down its own line.
3185
+ *
3186
+ * `auto` — Word's "Multiple", the 1.15 every modern template sets — scales the
3187
+ * line instead, and the space it gains falls BELOW: the first line of a spaced
3188
+ * paragraph starts exactly where a single-spaced one does. Read the other way
3189
+ * round, fdo80800.docx's 1.5-line cells sat 6.6pt under the plain cell beside
3190
+ * them, where Word and LibreOffice write all three on one baseline.
3191
+ *
3192
+ * §17.6.5 — the extra a document GRID adds is not the paragraph's at all, and
3193
+ * is split evenly: the line is centred on its grid row.
3194
+ *
3195
+ * @param line The laid-out line.
3196
+ * @param p The owning paragraph's resolved properties.
3197
+ * @returns The distance from the line box's bottom edge up to the baseline.
3198
+ */
3199
+ function lineBaselineOffset(line, p) {
3200
+ const descent = lineDescent(line);
3201
+ const natural = naturalLineHeight(line, p);
3202
+ const singled = naturalLineHeight(line, {
3203
+ ...p,
3204
+ spacingLineRule: "auto",
3205
+ spacingLine: pt(0)
3206
+ });
3207
+ return descent + (p.spacingLineRule === "auto" ? Math.max(0, natural - singled) : 0) + (computeLineHeight(line, p) - natural) / 2;
3208
+ }
1713
3209
  function alignmentOffset(alignment, lineWidth, available) {
1714
3210
  if (alignment === "right") return Math.max(0, available - lineWidth);
1715
3211
  if (alignment === "center") return Math.max(0, (available - lineWidth) / 2);
@@ -1724,7 +3220,7 @@ function layoutTableBlock(table, options, fontResources, imageResources, content
1724
3220
  for (let r = 0; r < table.rows.length; r++) {
1725
3221
  const rl = layoutTableRow(table.rows[r], r, table.rows.length, colCount, table.properties, columnWidthsPt, options, fontResources, imageResources, mergeRoles[r] ?? [], aboveBordersByCol);
1726
3222
  const nextAbove = [];
1727
- let ci = 0;
3223
+ let ci = table.rows[r].properties.gridBefore ?? 0;
1728
3224
  for (const cell of table.rows[r].cells) {
1729
3225
  const span = Math.max(1, cell.properties.colSpan ?? 1);
1730
3226
  for (let k = 0; k < span; k++) nextAbove[ci + k] = cell.properties.borders;
@@ -1738,17 +3234,46 @@ function layoutTableBlock(table, options, fontResources, imageResources, content
1738
3234
  ...rp.pageBreakBefore ? { breakBefore: true } : {}
1739
3235
  } : rl);
1740
3236
  }
3237
+ growRowsForMerges(rows);
1741
3238
  const heightPt = rows.reduce((s, r) => s + r.heightPt, 0);
1742
3239
  const totalWidthPt = columnWidthsPt.reduce((s, w) => s + w, 0);
3240
+ const xOffsetPt = (table.properties.indentPt ?? 0) + tableXOffset(table.properties.alignment, contentWidth, totalWidthPt);
1743
3241
  return {
1744
3242
  kind: "table",
3243
+ ...table.properties.float ? { float: table.properties.float } : {},
1745
3244
  rows,
1746
3245
  heightPt,
1747
3246
  totalWidthPt,
1748
3247
  colCount,
1749
- xOffsetPt: tableXOffset(table.properties.alignment, contentWidth, totalWidthPt)
3248
+ xOffsetPt
1750
3249
  };
1751
3250
  }
3251
+ /**
3252
+ * §17.4.85 `w:vMerge` — make every vertical merge fit. Each row is already as
3253
+ * tall as its own (unmerged) cells; a merge that needs more than its rows add
3254
+ * up to grows the LAST row of the run, which is where Word puts the slack.
3255
+ *
3256
+ * @param rows The table's laid-out rows, adjusted in place.
3257
+ */
3258
+ function growRowsForMerges(rows) {
3259
+ for (let r = 0; r < rows.length; r++) for (const cell of rows[r].cells) {
3260
+ if (cell.mergeRole !== "start") continue;
3261
+ let last = r;
3262
+ for (let k = r + 1; k < rows.length; k++) {
3263
+ const below = rows[k].cells.find((c) => c.colStart === cell.colStart);
3264
+ if (!below || below.mergeRole !== "middle" && below.mergeRole !== "end") break;
3265
+ last = k;
3266
+ if (below.mergeRole === "end") break;
3267
+ }
3268
+ let have = 0;
3269
+ for (let k = r; k <= last; k++) have += rows[k].heightPt;
3270
+ const deficit = cell.totalHeightPt - have;
3271
+ if (deficit > .01) rows[last] = {
3272
+ ...rows[last],
3273
+ heightPt: rows[last].heightPt + deficit
3274
+ };
3275
+ }
3276
+ }
1752
3277
  function tableXOffset(alignment, contentWidth, totalWidthPt) {
1753
3278
  const slack = contentWidth - totalWidthPt;
1754
3279
  if (slack <= 0 || !alignment || alignment === "left") return 0;
@@ -1757,15 +3282,18 @@ function tableXOffset(alignment, contentWidth, totalWidthPt) {
1757
3282
  function computeColumnWidths(table, options, fontResources, contentWidth) {
1758
3283
  let colCount = table.grid.length;
1759
3284
  for (const row of table.rows) {
1760
- let rowCols = 0;
3285
+ let rowCols = row.properties.gridBefore ?? 0;
1761
3286
  for (const cell of row.cells) rowCols += Math.max(1, cell.properties.colSpan ?? 1);
1762
3287
  if (rowCols > colCount) colCount = rowCols;
1763
3288
  }
1764
3289
  if (colCount === 0) return [];
1765
- if (table.properties.layout === "fixed") return gridWidthsScaled(table, contentWidth, colCount);
3290
+ const gridShort = gridColTwips(table).length < colCount;
3291
+ const declared = declaredColumnWidths(table, colCount, contentWidth, gridShort);
3292
+ if (declared) return declared;
3293
+ if (gridColTwips(table).some((w) => w > 0)) return gridWidthsScaled(table, contentWidth, colCount);
1766
3294
  const colNaturalWidths = new Array(colCount).fill(0);
1767
3295
  for (const row of table.rows) {
1768
- let colIdx = 0;
3296
+ let colIdx = row.properties.gridBefore ?? 0;
1769
3297
  for (let i = 0; i < row.cells.length; i++) {
1770
3298
  const cell = row.cells[i];
1771
3299
  const span = Math.max(1, cell.properties.colSpan ?? 1);
@@ -1798,6 +3326,81 @@ function computeColumnWidths(table, options, fontResources, contentWidth) {
1798
3326
  function gridColTwips(table) {
1799
3327
  return table.grid.map((w) => Math.round(w * 20));
1800
3328
  }
3329
+ /**
3330
+ * §17.4.53 — column widths taken from the cells' own `w:tcW` for a table laid
3331
+ * out `fixed`. Each cell shares its preferred width over the columns it spans
3332
+ * and the widest claim wins; a column nobody declares keeps its grid share.
3333
+ * Returns `undefined` (⇒ the grid decides) unless the table says `fixed` and
3334
+ * enough of it is declared to trust.
3335
+ */
3336
+ function declaredColumnWidths(table, colCount, contentWidth, gridShort = false) {
3337
+ if (table.properties.layout !== "fixed" && !gridShort) return void 0;
3338
+ const target = totalTableTarget(table, contentWidth);
3339
+ const declaredWidth = (cell) => {
3340
+ const p = cell.properties;
3341
+ if (p.widthFraction !== void 0) return p.widthFraction * target;
3342
+ if (p.width !== void 0 && p.widthType !== "auto" && p.widthType !== "nil") return p.width;
3343
+ };
3344
+ const claimed = new Array(colCount).fill(0);
3345
+ const spanning = [];
3346
+ let anyDeclared = false;
3347
+ let reach = 0;
3348
+ for (const row of table.rows) {
3349
+ if (row.cells.length < 2) continue;
3350
+ let span = row.properties.gridBefore ?? 0;
3351
+ for (const cell of row.cells) span += Math.max(1, cell.properties.colSpan ?? 1);
3352
+ if (span > reach) reach = span;
3353
+ }
3354
+ const phantomFrom = reach > 0 && reach < colCount ? reach : colCount;
3355
+ for (const row of table.rows) {
3356
+ let colIdx = row.properties.gridBefore ?? 0;
3357
+ for (const cell of row.cells) {
3358
+ const span = Math.max(1, cell.properties.colSpan ?? 1);
3359
+ const widthPt = declaredWidth(cell);
3360
+ if (widthPt !== void 0 && widthPt > 0) {
3361
+ anyDeclared = true;
3362
+ if (span === 1) {
3363
+ if (widthPt > claimed[colIdx]) claimed[colIdx] = widthPt;
3364
+ } else spanning.push({
3365
+ start: colIdx,
3366
+ span,
3367
+ widthPt
3368
+ });
3369
+ }
3370
+ colIdx += span;
3371
+ }
3372
+ }
3373
+ if (!anyDeclared) return void 0;
3374
+ for (const s of spanning) {
3375
+ const end = Math.min(s.start + s.span, phantomFrom);
3376
+ let have = 0;
3377
+ const blank = [];
3378
+ for (let i = s.start; i < end; i++) {
3379
+ have += claimed[i];
3380
+ if (claimed[i] === 0) blank.push(i);
3381
+ }
3382
+ const deficit = s.widthPt - have;
3383
+ if (deficit <= 0) continue;
3384
+ if (blank.length > 0) {
3385
+ const each = deficit / blank.length;
3386
+ for (const i of blank) claimed[i] = each;
3387
+ } else if (have > 0) for (let i = s.start; i < end; i++) claimed[i] = claimed[i] * (s.widthPt / have);
3388
+ }
3389
+ const gridTwips = gridColTwips(table);
3390
+ const rest = [];
3391
+ let claimedTotal = 0;
3392
+ for (let i = 0; i < phantomFrom; i++) if (claimed[i] > 0) claimedTotal += claimed[i];
3393
+ else rest.push(i);
3394
+ if (rest.length > 0) {
3395
+ const leftover = Math.max(0, target - claimedTotal);
3396
+ const restGrid = rest.reduce((s, i) => s + (gridTwips[i] ?? 0), 0);
3397
+ for (const i of rest) claimed[i] = restGrid > 0 ? (gridTwips[i] ?? 0) / restGrid * leftover : leftover / rest.length;
3398
+ }
3399
+ const total = claimed.reduce((s, w) => s + w, 0);
3400
+ if (total <= 0) return void 0;
3401
+ const scale = total > target ? target / total : 1;
3402
+ return claimed.map((w) => w * scale);
3403
+ }
1801
3404
  function gridWidthsScaled(table, contentWidth, colCount) {
1802
3405
  const gridTwips = gridColTwips(table);
1803
3406
  const totalGridTwips = gridTwips.reduce((s, w) => s + w, 0);
@@ -1860,8 +3463,9 @@ function aboveOfSpan(aboveBordersByCol, colIdx, span) {
1860
3463
  function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidthsPt, options, fontResources, imageResources, rowMergeRoles, aboveBordersByCol) {
1861
3464
  const cells = [];
1862
3465
  const columnXOffsets = [];
1863
- let cursorX = 0;
1864
- let colIdx = 0;
3466
+ const skip = Math.min(Math.max(0, row.properties.gridBefore ?? 0), columnWidthsPt.length);
3467
+ let cursorX = columnWidthsPt.slice(0, skip).reduce((sum, w) => sum + w, 0);
3468
+ let colIdx = skip;
1865
3469
  for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {
1866
3470
  const cell = row.cells[cellIdx];
1867
3471
  const span = Math.max(1, cell.properties.colSpan ?? 1);
@@ -1882,7 +3486,10 @@ function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidth
1882
3486
  colIdx += span;
1883
3487
  }
1884
3488
  let heightPt = 0;
1885
- for (const c of cells) if (c.totalHeightPt > heightPt) heightPt = c.totalHeightPt;
3489
+ for (const c of cells) {
3490
+ if (c.mergeRole === "start") continue;
3491
+ if (c.totalHeightPt > heightPt) heightPt = c.totalHeightPt;
3492
+ }
1886
3493
  if (row.properties.height && row.properties.heightRule === "exact") {
1887
3494
  heightPt = row.properties.height;
1888
3495
  return {
@@ -1890,7 +3497,8 @@ function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidth
1890
3497
  cells: cells.map((c) => clipCellToHeight(c, heightPt)),
1891
3498
  columnXOffsets,
1892
3499
  rowIdx,
1893
- rowCount
3500
+ rowCount,
3501
+ ...row.properties.cantSplit ? { cantSplit: true } : {}
1894
3502
  };
1895
3503
  } else if (row.properties.height && row.properties.heightRule === "atLeast") heightPt = Math.max(heightPt, row.properties.height);
1896
3504
  return {
@@ -1898,7 +3506,8 @@ function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidth
1898
3506
  cells,
1899
3507
  columnXOffsets,
1900
3508
  rowIdx,
1901
- rowCount
3509
+ rowCount,
3510
+ ...row.properties.cantSplit ? { cantSplit: true } : {}
1902
3511
  };
1903
3512
  }
1904
3513
  /**
@@ -1930,6 +3539,31 @@ function clipCellToHeight(cell, heightPt) {
1930
3539
  contentHeightPt: used
1931
3540
  };
1932
3541
  }
3542
+ /**
3543
+ * The one run a picture BLOCK is made of, so a context that lays out paragraphs
3544
+ * (a table cell) can draw it without a block path of its own.
3545
+ *
3546
+ * @param image The image block.
3547
+ * @returns A run carrying it as an inline picture.
3548
+ */
3549
+ function imageRun(image) {
3550
+ return {
3551
+ text: "",
3552
+ properties: {},
3553
+ inlineImage: {
3554
+ ...image.resource ? { resource: image.resource } : {},
3555
+ ...image.outline ? { outline: image.outline } : {},
3556
+ ...image.wash ? { wash: image.wash } : {},
3557
+ width: image.width,
3558
+ height: image.height,
3559
+ ...image.crop ? { crop: image.crop } : {},
3560
+ ...image.rotation60k ? { rotation60k: image.rotation60k } : {},
3561
+ ...image.flipH ? { flipH: true } : {},
3562
+ ...image.flipV ? { flipV: true } : {},
3563
+ ...image.effectExtent ? { effectExtent: image.effectExtent } : {}
3564
+ }
3565
+ };
3566
+ }
1933
3567
  function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imageResources, rowIdx, colStart, colSpan, rowCount, colCount, mergeRole, leftNeighborBorders, aboveNeighborBorders) {
1934
3568
  const cellMar = cell.properties.margins;
1935
3569
  const tableMar = tableProps.defaultCellMargins;
@@ -1942,8 +3576,19 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
1942
3576
  const innerWidth = Math.max(1, widthPt - padLeftPt - padRightPt);
1943
3577
  const lines = [];
1944
3578
  const nestedTables = [];
3579
+ const cellShapes = [];
3580
+ const cellFloats = [];
3581
+ const lineGaps = /* @__PURE__ */ new Map();
1945
3582
  let contentHeightPt = 0;
1946
3583
  let clipped = false;
3584
+ let pendingGapPt = 0;
3585
+ const openParagraph = (spacingBeforePt) => {
3586
+ const gap = pendingGapPt + spacingBeforePt;
3587
+ pendingGapPt = 0;
3588
+ if (gap <= 0) return;
3589
+ lineGaps.set(lines.length, (lineGaps.get(lines.length) ?? 0) + gap);
3590
+ contentHeightPt += gap;
3591
+ };
1947
3592
  if (mergeRole !== "middle" && mergeRole !== "end") {
1948
3593
  for (const el of cell.content) if (el.kind === "paragraph") {
1949
3594
  const noWrap = cell.properties.noWrap === true;
@@ -1951,16 +3596,50 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
1951
3596
  const cutAt = widthPt - padLeftPt;
1952
3597
  if (noWrap && (block.lines[0]?.contentWidthPt ?? 0) > cutAt) clipped = true;
1953
3598
  const keep = noWrap ? clipLineToWidth(block.lines, cutAt, cell.properties.hashOnOverflow === true) : block.lines;
3599
+ const markOnly = cell.properties.hideMark === true && el.paragraph.runs.every((r) => r.text === "");
3600
+ if (!markOnly) openParagraph(block.spacingBeforePt);
1954
3601
  for (const line of keep) {
1955
3602
  lines.push(line);
1956
- contentHeightPt += computeLineHeight(line, block.resolved);
3603
+ if (!markOnly) contentHeightPt += computeLineHeight(line, block.resolved);
1957
3604
  }
1958
- contentHeightPt += block.spacingAfterPt;
3605
+ if (!markOnly) pendingGapPt = block.spacingAfterPt;
1959
3606
  } else if (el.kind === "table") {
1960
3607
  const nested = layoutTableBlock(el.table, options, fontResources, imageResources, innerWidth);
1961
- nestedTables.push(nested);
3608
+ nestedTables.push({
3609
+ block: nested,
3610
+ afterLine: lines.length
3611
+ });
1962
3612
  contentHeightPt += nested.heightPt;
1963
- }
3613
+ } else if (el.kind === "image" && !isOutOfFlowFloat(el.image.float)) {
3614
+ const block = layoutParagraphBlock({
3615
+ properties: el.image.paragraphProperties,
3616
+ runs: [imageRun(el.image)]
3617
+ }, options, fontResources, imageResources, innerWidth);
3618
+ openParagraph(block.spacingBeforePt);
3619
+ for (const line of block.lines) {
3620
+ lines.push(line);
3621
+ contentHeightPt += computeLineHeight(line, block.resolved);
3622
+ }
3623
+ contentHeightPt += block.spacingAfterPt;
3624
+ pendingGapPt = block.spacingAfterPt;
3625
+ } else if (el.kind === "shape" && !isOutOfFlowFloat(el.shape.float)) {
3626
+ const block = layoutShapeBlock(el.shape, options, fontResources, imageResources, innerWidth);
3627
+ openParagraph(block.spacingBeforePt);
3628
+ cellShapes.push({
3629
+ block,
3630
+ afterLine: lines.length
3631
+ });
3632
+ contentHeightPt += block.heightPt;
3633
+ pendingGapPt = block.spacingAfterPt;
3634
+ } else if (el.kind === "image" && isOutOfFlowFloat(el.image.float)) cellFloats.push({
3635
+ block: layoutImageBlock(el.image, imageResources, innerWidth),
3636
+ afterLine: lines.length
3637
+ });
3638
+ else if (el.kind === "shape" && isOutOfFlowFloat(el.shape.float)) cellFloats.push({
3639
+ block: layoutShapeBlock(el.shape, options, fontResources, imageResources, innerWidth),
3640
+ afterLine: lines.length
3641
+ });
3642
+ contentHeightPt += pendingGapPt;
1964
3643
  }
1965
3644
  const colEnd = colStart + colSpan - 1;
1966
3645
  const borders = resolveCellBorders(cell.properties.borders, tableProps.borders, rowIdx, colStart, colEnd, rowCount, colCount, mergeRole, leftNeighborBorders, aboveNeighborBorders);
@@ -1977,7 +3656,10 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
1977
3656
  ...cell.properties.icon ? { icon: cell.properties.icon } : {},
1978
3657
  ...cell.properties.sparkline ? { sparkline: cell.properties.sparkline } : {},
1979
3658
  lines,
3659
+ ...lineGaps.size > 0 ? { lineGaps } : {},
1980
3660
  ...nestedTables.length > 0 ? { nestedTables } : {},
3661
+ ...cellShapes.length > 0 ? { shapes: cellShapes } : {},
3662
+ ...cellFloats.length > 0 ? { floats: cellFloats } : {},
1981
3663
  contentHeightPt,
1982
3664
  totalHeightPt,
1983
3665
  ...cell.properties.verticalAlign ? { verticalAlign: cell.properties.verticalAlign } : {},
@@ -1992,6 +3674,7 @@ var BORDER_STYLE_RANK = {
1992
3674
  thick: 4,
1993
3675
  single: 3,
1994
3676
  dashed: 2,
3677
+ dashSmallGap: 2,
1995
3678
  dashDot: 2,
1996
3679
  dashDotDot: 2,
1997
3680
  dotted: 1,
@@ -2099,7 +3782,7 @@ function listItemParagraphNode(builder, stack, list, wantLbl) {
2099
3782
  * Push order into `current`/`pages` IS the byte order of the emitted PDF, so the
2100
3783
  * bodies move unchanged.
2101
3784
  */
2102
- var PageAssembler = class {
3785
+ var PageAssembler = class PageAssembler {
2103
3786
  /**
2104
3787
  * @param sectionCtxs The per-section render contexts, in order; assembly
2105
3788
  * starts on the first section's first page.
@@ -2109,20 +3792,29 @@ var PageAssembler = class {
2109
3792
  * `undefined` when there are none.
2110
3793
  * @param bookmarkPositions Out-param: bookmark name → its GoTo destination.
2111
3794
  */
2112
- constructor(sectionCtxs, builder, notes, bookmarkPositions) {
3795
+ constructor(sectionCtxs, builder, notes, bookmarkPositions, backgroundImageName) {
2113
3796
  this.sectionCtxs = sectionCtxs;
2114
3797
  this.builder = builder;
2115
3798
  this.notes = notes;
2116
3799
  this.bookmarkPositions = bookmarkPositions;
3800
+ this.backgroundImageName = backgroundImageName;
2117
3801
  this.ctx = sectionCtxs[0];
3802
+ this.pageStartCtx = this.ctx;
3803
+ this.pageNumber = this.ctx.properties.pageNumberStart ?? 1;
2118
3804
  this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
2119
3805
  this.colStartY = this.cursorY;
3806
+ this.bandTopY = this.cursorY;
2120
3807
  }
2121
3808
  pages = [];
2122
3809
  ctx;
2123
3810
  secIdx = 0;
2124
3811
  pageInSection = 0;
2125
3812
  globalPageIdx = 0;
3813
+ /**
3814
+ * §17.6.12 — the number the NEXT page prints for a PAGE field. Counts up with
3815
+ * the pages and restarts wherever a section names a `w:pgNumType w:start`.
3816
+ */
3817
+ pageNumber = 1;
2126
3818
  current = [];
2127
3819
  pendingPageBreak = false;
2128
3820
  cursorY;
@@ -2136,11 +3828,23 @@ var PageAssembler = class {
2136
3828
  colStartLen = 0;
2137
3829
  /** The cursor's y at the top of the current column — see {@link colHasContent}. */
2138
3830
  colStartY;
3831
+ /**
3832
+ * Where this page's column band begins. The top margin on an ordinary page;
3833
+ * further down when a §17.6.22 `continuous` section started mid-page, whose
3834
+ * columns run beside each other from there, not from the paper's top.
3835
+ */
3836
+ bandTopY;
2139
3837
  /** The current column's left edge (`marginLeft` plus the column x-offset). */
2140
3838
  colLeft = () => this.ctx.marginLeft + (this.ctx.columns?.[this.colIdx]?.xOffsetPt ?? 0);
2141
3839
  /** The current column's width (the section content width when single-column). */
2142
3840
  colWidth = () => this.ctx.columns?.[this.colIdx]?.widthPt ?? this.ctx.contentWidth;
2143
3841
  /**
3842
+ * The measure the section's blocks were BROKEN at — the first column's, since
3843
+ * that is all the block pass can know. A column of another width has to
3844
+ * re-break what lands in it.
3845
+ */
3846
+ builtWidth = () => this.ctx.columns?.[0]?.widthPt ?? this.ctx.contentWidth;
3847
+ /**
2144
3848
  * Whether the current column is already in use.
2145
3849
  *
2146
3850
  * This gates every overflow break, so that a block too tall for an empty page
@@ -2167,15 +3871,118 @@ var PageAssembler = class {
2167
3871
  if (this.ctx.columns && this.colIdx + 1 < this.ctx.columns.length) {
2168
3872
  this.colIdx++;
2169
3873
  this.colStartLen = this.current.length;
2170
- this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
3874
+ this.cursorY = this.bandTopY;
2171
3875
  this.colStartY = this.cursorY;
3876
+ const isLast = this.colIdx + 1 >= this.ctx.columns.length;
3877
+ this.balanceBottomY = this.balanceHeightPt > 0 && !isLast ? this.cursorY - this.balanceHeightPt : void 0;
2172
3878
  } else this.flushPage();
2173
3879
  };
2174
3880
  /**
3881
+ * §17.6.8 — the running line count for the margin numbers. Reset by the
3882
+ * section's own `w:restart` rule.
3883
+ */
3884
+ lineNumber = 0;
3885
+ /**
3886
+ * §17.6.8 — the margin number for the body line just placed, if the section
3887
+ * asks for one at this count. Pushed at the line's own baseline, right-
3888
+ * aligned a quarter inch off the text.
3889
+ *
3890
+ * @param baselineY The line's baseline, in page (y-down) coordinates.
3891
+ * @param line The line being numbered — its font draws the digits.
3892
+ */
3893
+ emitLineNumber = (baselineY, line) => {
3894
+ const ln = this.ctx.properties.lineNumbering;
3895
+ if (!ln) return;
3896
+ this.lineNumber++;
3897
+ const n = ln.start + this.lineNumber - 1;
3898
+ if ((n - ln.start + 1) % ln.countBy !== 0) return;
3899
+ const token = line.tokens.find((t) => t.kind === "text");
3900
+ if (!token) return;
3901
+ const text = String(n);
3902
+ const sizePt = token.fontSizePt;
3903
+ const widthPt = token.font.measure.textWidthPt(text, sizePt);
3904
+ const digits = {
3905
+ kind: "text",
3906
+ text,
3907
+ isSpace: false,
3908
+ resolvedRun: token.resolvedRun,
3909
+ font: token.font,
3910
+ fontSizePt: sizePt,
3911
+ widthPt,
3912
+ bidiLevel: 0
3913
+ };
3914
+ const numbered = {
3915
+ ...line,
3916
+ tokens: [digits],
3917
+ contentWidthPt: widthPt
3918
+ };
3919
+ this.current.push({
3920
+ type: "line",
3921
+ line: numbered,
3922
+ originX: pt(this.colLeft() - (ln.distancePt ?? LINE_NUMBER_GAP_PT) - widthPt),
3923
+ baselineY: pt(baselineY)
3924
+ });
3925
+ };
3926
+ /**
3927
+ * §17.6.12 — a section that names its own first page number restarts the
3928
+ * count there; one that names none carries on from the section before.
3929
+ *
3930
+ * @param next The section being entered.
3931
+ */
3932
+ restartPageNumbers = (next) => {
3933
+ const start = next.properties.pageNumberStart;
3934
+ if (start !== void 0) this.pageNumber = start;
3935
+ if ((next.properties.lineNumbering?.restart ?? "newPage") !== "continuous") this.lineNumber = 0;
3936
+ };
3937
+ /**
3938
+ * §17.6.22 — start a `continuous` section on the page in hand: its columns
3939
+ * begin where the section does, not at the top of the paper.
3940
+ *
3941
+ * @param next The section to continue into.
3942
+ */
3943
+ startBandSection = (next, bandHeightPt) => {
3944
+ this.secIdx++;
3945
+ this.ctx = next;
3946
+ this.restartPageNumbers(next);
3947
+ this.colIdx = 0;
3948
+ this.colStartLen = this.current.length;
3949
+ this.cursorY = Math.min(this.cursorY, next.pageHeight - next.marginTop);
3950
+ this.bandTopY = this.cursorY;
3951
+ this.colStartY = this.cursorY;
3952
+ this.beginBalancedBand(bandHeightPt);
3953
+ };
3954
+ /**
3955
+ * §17.6.4 — even the columns of the band that starts at the cursor: each gets
3956
+ * the same share of `bandHeightPt`. A band too tall for the page fills column
3957
+ * after column as before; there is nothing to even out when it will spill
3958
+ * anyway.
3959
+ *
3960
+ * @param bandHeightPt The height of everything the band holds.
3961
+ */
3962
+ beginBalancedBand = (bandHeightPt) => {
3963
+ const colCount = this.ctx.columns?.length ?? 1;
3964
+ const room = this.cursorY - (this.ctx.marginBottom + this.noteReserve);
3965
+ const share = colCount > 1 ? balancedColumnHeight(bandHeightPt, colCount, room) : 0;
3966
+ this.balanceHeightPt = share;
3967
+ this.balanceBottomY = share > 0 ? this.cursorY - share : void 0;
3968
+ };
3969
+ /**
2175
3970
  * §20.4.2.3 out-of-flow drawings (wrap `'none'`): they render at their anchored
2176
3971
  * position without moving the cursor. `behindDoc` sinks below the body text,
2177
3972
  * everything else above it; both flush with the page.
2178
3973
  */
3974
+ /**
3975
+ * §20.4.2.3 — a float layer, drawn in `relativeHeight` order. A stable sort
3976
+ * by z leaves the ones that state none where the document put them:
3977
+ * dml-rectangle-relsize.docx writes its blue bar first and gives it the
3978
+ * higher z, and drawn in document order the white rectangle covered it.
3979
+ */
3980
+ /** A float layer that stamps each item it takes with the float's z-order. */
3981
+ static zSink = (target, z) => z === void 0 ? target : { push: (...items) => target.push(...items.map((i) => ({
3982
+ ...i,
3983
+ z
3984
+ }))) };
3985
+ static byZ = (items) => items.every((i) => i.z === void 0) ? [...items] : [...items].sort((a, b) => (a.z ?? 0) - (b.z ?? 0));
2179
3986
  floatsBehind = [];
2180
3987
  floatsFront = [];
2181
3988
  /**
@@ -2183,27 +3990,42 @@ var PageAssembler = class {
2183
3990
  * must flow around. Page-scoped, like the float graphics.
2184
3991
  */
2185
3992
  exclusions = [];
2186
- /** The float's left edge on the page, resolved from its horizontal anchor. */
2187
- floatX = (f, widthPt) => {
2188
- const h = f.posH;
2189
- if (!h) return this.colLeft();
2190
- const base = h.relativeFrom === "page" ? 0 : h.relativeFrom === "column" ? this.colLeft() : this.ctx.marginLeft;
2191
- const span = h.relativeFrom === "page" ? this.ctx.pageWidth : h.relativeFrom === "column" ? this.colWidth() : this.ctx.contentWidth;
2192
- if (h.align === "center") return base + (span - widthPt) / 2;
2193
- if (h.align === "right") return base + span - widthPt;
2194
- return base + (h.offsetPt ?? 0);
3993
+ /**
3994
+ * Claim the area a side-wrapping float keeps to itself: its own box grown by
3995
+ * the §20.4.2.3 `distT/B/L/R` stand-off the anchor asks for.
3996
+ */
3997
+ excludeFloat = (f, x, topYUp, w, h) => {
3998
+ const d = f.wrapDist;
3999
+ const wide = f.wrap === "notBeside";
4000
+ this.exclusions.push({
4001
+ x0: wide ? this.colLeft() : x - (d?.leftPt ?? 0),
4002
+ x1: wide ? this.colLeft() + this.colWidth() : x + w + (d?.rightPt ?? 0),
4003
+ topYUp: topYUp + (d?.topPt ?? 0),
4004
+ bottomYUp: topYUp - h - (d?.bottomPt ?? 0),
4005
+ ...f.wrapSide === "bothSides" ? { wrapBothSides: true } : {}
4006
+ });
2195
4007
  };
4008
+ /** The frame a float anchored in the body flow is resolved against. */
4009
+ floatFrame = () => ({
4010
+ pageLeft: 0,
4011
+ pageWidth: this.ctx.pageWidth,
4012
+ pageTopYUp: this.ctx.pageHeight,
4013
+ pageBottomYUp: 0,
4014
+ marginLeft: this.ctx.marginLeft,
4015
+ contentWidth: this.ctx.contentWidth,
4016
+ marginTopYUp: this.ctx.pageHeight - this.ctx.marginTop,
4017
+ marginBottomYUp: this.ctx.marginBottom,
4018
+ columnLeft: this.colLeft(),
4019
+ columnWidth: this.colWidth(),
4020
+ anchorY: this.cursorY
4021
+ });
4022
+ /** The float's left edge on the page, resolved from its horizontal anchor. */
4023
+ floatX = (f, widthPt) => floatXIn(this.floatFrame(), f, widthPt);
2196
4024
  /**
2197
4025
  * The drawing's TOP in the y-up cursor frame. Paragraph/line-relative offsets
2198
4026
  * hang off the anchoring paragraph's current position.
2199
4027
  */
2200
- floatTopYUp = (f) => {
2201
- const v = f.posV;
2202
- if (!v) return this.cursorY;
2203
- if (v.relativeFrom === "page") return this.ctx.pageHeight - (v.offsetPt ?? 0);
2204
- if (v.relativeFrom === "margin") return this.ctx.pageHeight - this.ctx.marginTop - (v.offsetPt ?? 0);
2205
- return this.cursorY - (v.offsetPt ?? 0);
2206
- };
4028
+ floatTopYUp = (f, heightPt = 0) => floatTopYUpIn(this.floatFrame(), f, heightPt);
2207
4029
  /**
2208
4030
  * Tagged PDF: the stack of open list levels (for L/LI/LBody nesting). Cleared
2209
4031
  * whenever a non-list-item block interrupts the run of list paragraphs.
@@ -2219,7 +4041,21 @@ var PageAssembler = class {
2219
4041
  noteReserve = 0;
2220
4042
  placedNotes = /* @__PURE__ */ new Set();
2221
4043
  /** The page's usable bottom: the margin plus whatever the notes band has claimed so far. */
2222
- bottomLimit = () => this.ctx.marginBottom + this.noteReserve;
4044
+ /**
4045
+ * §17.6.4 — where the current column must stop. Normally the page's bottom
4046
+ * margin (plus any footnote reserve); inside a BALANCED band it is the line
4047
+ * the band's share of the height reaches, so the columns come out even.
4048
+ */
4049
+ bottomLimit = () => Math.max(this.ctx.marginBottom + this.noteReserve, this.balanceBottomY ?? Number.NEGATIVE_INFINITY);
4050
+ /**
4051
+ * §17.6.4 — a `continuous` section of several columns whose content fits the
4052
+ * page is BALANCED: Word evens the columns out rather than filling the first
4053
+ * to the bottom. Set while such a band is being laid out; the height each
4054
+ * column is given is `balanceHeightPt`. IndexFieldFlagF.docx sets its index
4055
+ * that way and we filled column one and left the rest empty.
4056
+ */
4057
+ balanceBottomY;
4058
+ balanceHeightPt = 0;
2223
4059
  /**
2224
4060
  * The first exclusion a line spanning `[yTop-h, yTop]` collides with in the
2225
4061
  * current column (x-overlap with the column required).
@@ -2247,15 +4083,25 @@ var PageAssembler = class {
2247
4083
  const cl = this.colLeft();
2248
4084
  const leftRoom = e.x0 - FLOAT_TEXT_GAP - cl;
2249
4085
  const rightRoom = cl + full - (e.x1 + FLOAT_TEXT_GAP);
2250
- if (rightRoom >= leftRoom) {
2251
- const w = Math.max(MIN_WRAP_WIDTH, rightRoom);
2252
- return {
2253
- width: w,
2254
- xOffset: full - w
2255
- };
2256
- }
4086
+ if (Math.max(leftRoom, rightRoom) < MIN_WRAP_WIDTH) return {
4087
+ width: full,
4088
+ xOffset: 0,
4089
+ blockedUntilYUp: e.bottomYUp
4090
+ };
4091
+ if (leftRoom >= MIN_WRAP_WIDTH && rightRoom >= MIN_WRAP_WIDTH && e.wrapBothSides) return {
4092
+ width: leftRoom,
4093
+ xOffset: 0,
4094
+ pair: {
4095
+ width: rightRoom,
4096
+ xOffset: full - rightRoom
4097
+ }
4098
+ };
4099
+ if (rightRoom >= leftRoom) return {
4100
+ width: rightRoom,
4101
+ xOffset: full - rightRoom
4102
+ };
2257
4103
  return {
2258
- width: Math.max(MIN_WRAP_WIDTH, leftRoom),
4104
+ width: leftRoom,
2259
4105
  xOffset: 0
2260
4106
  };
2261
4107
  };
@@ -2274,9 +4120,16 @@ var PageAssembler = class {
2274
4120
  let y = startY;
2275
4121
  for (let i = 0; i < 200; i++) {
2276
4122
  const g = this.lineGeometryAt(y, h0);
4123
+ if (g.blockedUntilYUp !== void 0) {
4124
+ widths.push(this.colWidth());
4125
+ y = g.blockedUntilYUp - h0;
4126
+ if (y < this.bottomLimit()) break;
4127
+ continue;
4128
+ }
2277
4129
  widths.push(g.width);
4130
+ if (g.pair) widths.push(g.pair.width);
2278
4131
  if (g.width < this.colWidth()) narrowed = true;
2279
- else if (i > 0) break;
4132
+ else if (narrowed) break;
2280
4133
  y -= h0;
2281
4134
  if (y < this.bottomLimit()) break;
2282
4135
  }
@@ -2328,7 +4181,7 @@ var PageAssembler = class {
2328
4181
  const noteNode = this.builder.create("Note", this.builder.root);
2329
4182
  structId = this.builder.create("P", noteNode).id;
2330
4183
  }
2331
- out.push(...drawBlocksSequentially(note.blocks, this.ctx.marginLeft, cursor, this.ctx.pageHeight, structId));
4184
+ out.push(...drawBlocksSequentially(note.blocks, this.ctx.marginLeft, cursor, this.ctx.pageHeight, this.ctx.contentWidth, structId));
2332
4185
  cursor -= note.heightPt;
2333
4186
  }
2334
4187
  return out;
@@ -2353,22 +4206,25 @@ var PageAssembler = class {
2353
4206
  const footer = pickBand(this.ctx.footerSet, band);
2354
4207
  if (header.renderDynamic) this.dynBands.push({
2355
4208
  pageIdx: this.pages.length,
2356
- pageNumber: this.globalPageIdx + 1,
4209
+ pageNumber: this.pageNumber,
2357
4210
  position: "header",
2358
4211
  render: header.renderDynamic
2359
4212
  });
2360
4213
  if (footer.renderDynamic) this.dynBands.push({
2361
4214
  pageIdx: this.pages.length,
2362
- pageNumber: this.globalPageIdx + 1,
4215
+ pageNumber: this.pageNumber,
2363
4216
  position: "footer",
2364
4217
  render: footer.renderDynamic
2365
4218
  });
2366
4219
  this.pages.push({
2367
4220
  commands: [
4221
+ ...this.pageBackgroundItems(),
4222
+ ...this.pageBorderItems(),
4223
+ ...this.columnSeparatorItems(),
2368
4224
  ...header.commands,
2369
- ...this.floatsBehind,
4225
+ ...PageAssembler.byZ(this.floatsBehind),
2370
4226
  ...this.current,
2371
- ...this.floatsFront,
4227
+ ...PageAssembler.byZ(this.floatsFront),
2372
4228
  ...this.renderNotesBand(),
2373
4229
  ...footer.commands
2374
4230
  ],
@@ -2385,35 +4241,214 @@ var PageAssembler = class {
2385
4241
  this.colStartLen = 0;
2386
4242
  this.pageInSection++;
2387
4243
  this.globalPageIdx++;
4244
+ this.pageNumber++;
4245
+ if ((this.ctx.properties.lineNumbering?.restart ?? "newPage") === "newPage") this.lineNumber = 0;
4246
+ this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
4247
+ this.colStartY = this.cursorY;
4248
+ this.bandTopY = this.cursorY;
4249
+ this.balanceBottomY = void 0;
4250
+ this.balanceHeightPt = 0;
4251
+ this.pageStartCtx = this.ctx;
4252
+ };
4253
+ /**
4254
+ * §17.6.4 `w:cols w:sep` — the rule down the middle of every gutter, run the
4255
+ * height of the text area. multi-column-separator-with-line.docx asks for one
4256
+ * and we drew its two columns with nothing between them.
4257
+ *
4258
+ * @returns One border item per gutter, or none when the section asks for no rule.
4259
+ */
4260
+ columnSeparatorItems = () => {
4261
+ const ctx = this.pageStartCtx;
4262
+ const cols = ctx.columns;
4263
+ if (!cols || cols.length < 2 || ctx.properties.columns?.separator !== true) return [];
4264
+ let deepest = ctx.marginTop;
4265
+ for (const item of this.current) {
4266
+ const bottom = item.type === "line" ? item.baselineY : item.type === "shape" ? 0 : item.y + item.height;
4267
+ if (bottom > deepest) deepest = bottom;
4268
+ }
4269
+ const height = Math.min(deepest, ctx.pageHeight - ctx.marginBottom) - ctx.marginTop;
4270
+ if (height <= 0) return [];
4271
+ const out = [];
4272
+ for (let i = 1; i < cols.length; i++) {
4273
+ const left = cols[i - 1];
4274
+ const right = cols[i];
4275
+ const x = ctx.marginLeft + (left.xOffsetPt + left.widthPt + right.xOffsetPt) / 2;
4276
+ out.push({
4277
+ type: "border",
4278
+ side: "left",
4279
+ x: pt(x),
4280
+ y: pt(ctx.marginTop),
4281
+ width: pt(0),
4282
+ height: pt(height),
4283
+ borderSizePt: COLUMN_SEPARATOR_PT,
4284
+ borderColorHex: "000000"
4285
+ });
4286
+ }
4287
+ return out;
4288
+ };
4289
+ /** The section context the CURRENT page began in (see {@link columnSeparatorItems}). */
4290
+ pageStartCtx;
4291
+ /**
4292
+ * §17.6.10 `w:pgBorders` — the rules around the page. `@w:offsetFrom` says
4293
+ * what each edge's space is measured from: the paper's own edge, or the text
4294
+ * margin the rule stands outside of. a4andborders.docx frames every page and
4295
+ * we drew nothing.
4296
+ *
4297
+ * @returns The border items for this page, in paint order.
4298
+ */
4299
+ /**
4300
+ * §17.2.1 — what the page is painted before anything is drawn on it: the
4301
+ * `v:background`'s PICTURE or GRADIENT when it has one, else the flat
4302
+ * `w:background @w:color`. Read as the colour alone, fill.docx's five-stop
4303
+ * sweep came out the navy of its fallback and tdf126533_pageBitmap.docx's
4304
+ * paper came out plain.
4305
+ *
4306
+ * @returns The background items for this page (at most one).
4307
+ */
4308
+ pageBackgroundItems = () => {
4309
+ const options = this.ctx.options;
4310
+ const w = this.ctx.pageWidth;
4311
+ const h = this.ctx.pageHeight;
4312
+ const fill = options?.pageBackgroundFill;
4313
+ if (fill?.kind === "picture" && this.backgroundImageName !== void 0) return [{
4314
+ type: "image",
4315
+ x: pt(0),
4316
+ y: pt(0),
4317
+ width: pt(w),
4318
+ height: pt(h),
4319
+ imageResourceName: this.backgroundImageName
4320
+ }];
4321
+ if (fill?.kind === "gradient" && fill.gradient) return [{
4322
+ type: "shape",
4323
+ shape: {
4324
+ paths: [rectAtPath(0, 0, w, h)],
4325
+ fillColorHex: gradientToSolid(fill.gradient),
4326
+ fillGradient: fill.gradient,
4327
+ transform: flipTransform([
4328
+ 1,
4329
+ 0,
4330
+ 0,
4331
+ 1,
4332
+ 0,
4333
+ 0
4334
+ ], h)
4335
+ }
4336
+ }];
4337
+ if (options?.pageBackgroundColorHex !== void 0) return [{
4338
+ type: "fill",
4339
+ x: pt(0),
4340
+ y: pt(0),
4341
+ width: pt(w),
4342
+ height: pt(h),
4343
+ fillColorHex: options.pageBackgroundColorHex
4344
+ }];
4345
+ return [];
4346
+ };
4347
+ pageBorderItems = () => {
4348
+ const pg = this.ctx.properties.pageBorders;
4349
+ if (!pg) return [];
4350
+ const H = this.ctx.pageHeight;
4351
+ const W = this.ctx.pageWidth;
4352
+ const out = [];
4353
+ for (const side of [
4354
+ "top",
4355
+ "bottom",
4356
+ "left",
4357
+ "right"
4358
+ ]) {
4359
+ const edge = pg.borders[side];
4360
+ if (!edge || edge.style === "none") continue;
4361
+ const gap = edge.spacePt ?? 0;
4362
+ const x0 = pg.offsetFrom === "page" ? gap : this.ctx.marginLeft - gap;
4363
+ const x1 = pg.offsetFrom === "page" ? W - gap : W - this.ctx.marginLeft + gap;
4364
+ const yTop = pg.offsetFrom === "page" ? gap : this.ctx.marginTop - gap;
4365
+ const yBottom = pg.offsetFrom === "page" ? H - gap : H - this.ctx.marginBottom + gap;
4366
+ out.push({
4367
+ type: "border",
4368
+ side,
4369
+ x: pt(Math.max(0, x0)),
4370
+ y: pt(Math.max(0, yTop)),
4371
+ width: pt(Math.max(0, x1 - x0)),
4372
+ height: pt(Math.max(0, yBottom - yTop)),
4373
+ borderSizePt: edge.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT,
4374
+ borderColorHex: edge.colorHex ?? "000000",
4375
+ ...edge.style !== "single" ? { borderStyle: edge.style } : {}
4376
+ });
4377
+ }
4378
+ return out;
4379
+ };
4380
+ /**
4381
+ * Throw the in-progress page away and reset for the next one.
4382
+ *
4383
+ * A section boundary reached with a page that draws nothing is the one place
4384
+ * this is wanted: {@link flushPage} keeps such a page whenever the cursor
4385
+ * moved at all, which is right in the middle of a flow and wrong at a
4386
+ * boundary, where the space consumed was the section break itself.
4387
+ */
4388
+ dropPage = () => {
4389
+ this.current = [];
4390
+ this.floatsBehind = [];
4391
+ this.floatsFront = [];
4392
+ this.exclusions = [];
4393
+ this.pageNotes = [];
4394
+ this.noteReserve = 0;
4395
+ this.colIdx = 0;
4396
+ this.colStartLen = 0;
2388
4397
  this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
2389
4398
  this.colStartY = this.cursorY;
4399
+ this.bandTopY = this.cursorY;
4400
+ this.balanceBottomY = void 0;
4401
+ this.balanceHeightPt = 0;
2390
4402
  };
2391
4403
  };
2392
- function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph) {
4404
+ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph, backgroundImageName) {
2393
4405
  if (sectionCtxs.length === 0) return [];
2394
- const asm = new PageAssembler(sectionCtxs, builder, notes, bookmarkPositions);
4406
+ const asm = new PageAssembler(sectionCtxs, builder, notes, bookmarkPositions, backgroundImageName);
4407
+ const balanceIfEndsContinuous = (fromBlock) => {
4408
+ const here = asm.ctx;
4409
+ const next = sectionCtxs[asm.secIdx + 1];
4410
+ if ((here.columns?.length ?? 1) < 2) return;
4411
+ if (!next?.continuous) return;
4412
+ if (next.pageWidth !== here.pageWidth || next.pageHeight !== here.pageHeight) return;
4413
+ const end = Math.min(here.endIndex, blocks.length);
4414
+ asm.beginBalancedBand(blocksHeight(blocks.slice(fromBlock, end)));
4415
+ };
4416
+ balanceIfEndsContinuous(0);
2395
4417
  for (let blockIdx = 0; blockIdx < blocks.length; blockIdx++) {
2396
4418
  while (asm.secIdx < sectionCtxs.length - 1 && blockIdx >= asm.ctx.endIndex) {
2397
- asm.flushPage();
4419
+ const next = sectionCtxs[asm.secIdx + 1];
4420
+ if (next.continuous && next.pageWidth === asm.ctx.pageWidth && next.pageHeight === asm.ctx.pageHeight) {
4421
+ const end = Math.min(next.endIndex, blocks.length);
4422
+ asm.startBandSection(next, blocksHeight(blocks.slice(blockIdx, end)));
4423
+ continue;
4424
+ }
4425
+ if (asm.pageHasContent() || asm.pageInSection === 0) asm.flushPage(true);
4426
+ else asm.dropPage();
2398
4427
  asm.secIdx++;
2399
4428
  asm.ctx = sectionCtxs[asm.secIdx];
4429
+ asm.restartPageNumbers(asm.ctx);
2400
4430
  asm.pageInSection = 0;
2401
4431
  asm.cursorY = asm.ctx.pageHeight - asm.ctx.marginTop;
4432
+ balanceIfEndsContinuous(blockIdx);
2402
4433
  }
2403
4434
  const block = blocks[blockIdx];
2404
4435
  if (asm.pendingPageBreak) {
2405
4436
  asm.pendingPageBreak = false;
2406
- if (asm.current.length > 0) asm.flushPage();
4437
+ asm.flushPage(true);
2407
4438
  }
2408
4439
  if (builder && !(block.kind === "paragraph" && block.list)) asm.listStack.length = 0;
2409
4440
  if (block.kind === "paragraph") {
2410
- if (block.resolved.pageBreakBefore && asm.pageHasContent()) asm.flushPage();
4441
+ if ((block.resolved.pageBreakBefore || block.pageBreakBefore) && asm.pageHasContent()) asm.flushPage();
2411
4442
  asm.cursorY -= block.spacingBeforePt;
2412
4443
  let pb = block;
4444
+ const colW = asm.colWidth();
4445
+ if (reflowParagraph && pb.source && Math.abs(colW - asm.builtWidth()) > .5) pb = reflowParagraph(pb.source, colW);
2413
4446
  if (reflowParagraph && pb.source && asm.exclusions.length > 0) {
2414
- const widths = asm.lineWidthsFor(block, asm.cursorY);
2415
- if (widths) pb = reflowParagraph(pb.source, asm.colWidth(), widths);
4447
+ const widths = asm.lineWidthsFor(pb, asm.cursorY);
4448
+ if (widths) pb = reflowParagraph(pb.source, colW, widths);
2416
4449
  }
4450
+ const borderTopY = asm.cursorY;
4451
+ const borderPage = asm.pages.length;
2417
4452
  let structId;
2418
4453
  let markerLblId;
2419
4454
  const firstLine = pb.lines[0];
@@ -2428,7 +4463,9 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2428
4463
  if (lang && lang !== defaultLang) builder.node(structId).lang = lang;
2429
4464
  }
2430
4465
  let firstLineOfBlock = true;
2431
- for (const line of pb.lines) {
4466
+ let pairPending;
4467
+ for (const [lineIdx, line] of pb.lines.entries()) {
4468
+ if (pb.columnBreakLines?.has(lineIdx) && asm.colHasContent()) asm.advanceColumn();
2432
4469
  const h = computeLineHeight(line, pb.resolved);
2433
4470
  let newNotes = asm.lineFootnotes(line);
2434
4471
  const addedReserve = (sub) => sub.reduce((sum, x) => sum + x.heightPt, 0) + (asm.pageNotes.length === 0 && sub.length > 0 ? FOOTNOTE_SEPARATOR_HEIGHT : 0);
@@ -2456,11 +4493,22 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2456
4493
  });
2457
4494
  }
2458
4495
  }
2459
- asm.cursorY -= h;
4496
+ const pairedNow = pairPending;
4497
+ pairPending = void 0;
4498
+ if (pairedNow === void 0) {
4499
+ for (let guard = 0; guard < 8 && asm.exclusions.length > 0; guard++) {
4500
+ const blocked = asm.lineGeometryAt(asm.cursorY, h).blockedUntilYUp;
4501
+ if (blocked === void 0 || blocked >= asm.cursorY) break;
4502
+ asm.cursorY = blocked;
4503
+ }
4504
+ asm.cursorY -= h;
4505
+ }
2460
4506
  const indentLeft = pb.resolved.indentLeft + (line.firstLine ? pb.resolved.indentFirstLine : 0);
2461
4507
  const offset = alignmentOffset(pb.resolved.alignment, line.contentWidthPt, line.availableWidthPt);
2462
- const baselineY = pt(asm.ctx.pageHeight - (asm.cursorY + lineDescent(line)));
2463
- const exclusionShift = asm.exclusions.length > 0 ? asm.lineGeometryAt(asm.cursorY + h, h).xOffset : 0;
4508
+ const baselineY = pt(asm.ctx.pageHeight - (asm.cursorY + lineBaselineOffset(line, pb.resolved)));
4509
+ const geometry = asm.exclusions.length > 0 ? asm.lineGeometryAt(asm.cursorY + h, h) : void 0;
4510
+ const exclusionShift = pairedNow ?? geometry?.xOffset ?? 0;
4511
+ if (pairedNow === void 0 && geometry?.pair && Math.abs(line.availableWidthPt - geometry.width) < 1) pairPending = geometry.pair.xOffset;
2464
4512
  const originX = asm.colLeft() + indentLeft + offset + exclusionShift;
2465
4513
  if (markerLblId !== void 0 && line === firstLine) {
2466
4514
  let k = 0;
@@ -2499,87 +4547,54 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2499
4547
  baselineY,
2500
4548
  ...structId !== void 0 ? { structId } : {}
2501
4549
  });
4550
+ asm.emitLineNumber(baselineY, line);
2502
4551
  }
4552
+ if (asm.pages.length === borderPage) asm.current.push(...paragraphDecoration(pb.resolved, asm.colLeft() + pb.resolved.indentLeft, asm.colLeft() + asm.colWidth() - pb.resolved.indentRight, borderTopY, asm.cursorY, asm.ctx.pageHeight));
2503
4553
  asm.cursorY -= pb.spacingAfterPt;
2504
4554
  if (pb.pageBreakAfter) asm.pendingPageBreak = true;
2505
4555
  } else if (block.kind === "image") {
2506
4556
  const figId = builder ? createFigure(builder, block.altText, "Image") : void 0;
2507
4557
  const emitImageAt = (x, topYUp, sink) => {
4558
+ const ins = block.drawInset;
2508
4559
  sink.push({
2509
4560
  type: "image",
2510
- x: pt(x),
2511
- y: pt(asm.ctx.pageHeight - topYUp),
2512
- width: pt(block.widthPt),
2513
- height: pt(block.heightPt),
4561
+ x: pt(x + (ins?.dxPt ?? 0)),
4562
+ y: pt(asm.ctx.pageHeight - (topYUp - (ins?.dyTopPt ?? 0))),
4563
+ width: pt(ins?.widthPt ?? block.widthPt),
4564
+ height: pt(ins?.heightPt ?? block.heightPt),
2514
4565
  imageResourceName: block.resourceName,
4566
+ ...block.crop ? { crop: block.crop } : {},
4567
+ ...block.wash ? { wash: block.wash } : {},
4568
+ ...block.rotationDeg ? { rotationDeg: block.rotationDeg } : {},
4569
+ ...block.flipH ? { flipH: true } : {},
4570
+ ...block.flipV ? { flipV: true } : {},
4571
+ ...block.flipH ? { flipH: true } : {},
4572
+ ...block.flipV ? { flipV: true } : {},
2515
4573
  ...figId !== void 0 ? { structId: figId } : {}
2516
4574
  });
4575
+ if (block.outline) sink.push(...pictureOutlineItems(block.outline, x + (ins?.dxPt ?? 0), asm.ctx.pageHeight - (topYUp - (ins?.dyTopPt ?? 0)), ins?.widthPt ?? block.widthPt, ins?.heightPt ?? block.heightPt, figId));
2517
4576
  };
2518
4577
  if (isOutOfFlowFloat(block.float)) {
2519
4578
  const fx = asm.floatX(block.float, block.widthPt);
2520
- const fy = asm.floatTopYUp(block.float);
2521
- emitImageAt(fx, fy, block.float.behind ? asm.floatsBehind : asm.floatsFront);
2522
- if (block.float.wrap !== "none") asm.exclusions.push({
2523
- x0: fx,
2524
- x1: fx + block.widthPt,
2525
- topYUp: fy,
2526
- bottomYUp: fy - block.heightPt
2527
- });
4579
+ const fy = asm.floatTopYUp(block.float, block.heightPt);
4580
+ emitImageAt(fx, fy, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
4581
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
2528
4582
  } else {
2529
4583
  asm.cursorY -= block.spacingBeforePt;
2530
4584
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
2531
4585
  asm.cursorY -= block.heightPt;
2532
- emitImageAt(asm.colLeft(), asm.cursorY + block.heightPt, asm.current);
4586
+ const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, asm.colWidth());
4587
+ emitImageAt(asm.colLeft() + offset, asm.cursorY + block.heightPt, asm.current);
2533
4588
  asm.cursorY -= block.spacingAfterPt;
2534
4589
  }
2535
4590
  } else if (block.kind === "shape") {
2536
4591
  const figId = builder ? createFigure(builder, block.altText, "Shape") : void 0;
2537
- const emitShapeAt = (x, bottomYUp, sink) => {
2538
- const transform = flipTransform(buildShapeTransform(x, bottomYUp, block.widthPt, block.heightPt, block.rotation60k, block.flipH, block.flipV), asm.ctx.pageHeight);
2539
- sink.push({
2540
- type: "shape",
2541
- shape: {
2542
- paths: block.paths,
2543
- ...block.fillColorHex ? { fillColorHex: block.fillColorHex } : {},
2544
- ...block.fillGradient ? { fillGradient: block.fillGradient } : {},
2545
- ...block.stroke ? { stroke: block.stroke } : {},
2546
- ...block.shadow ? { shadow: block.shadow } : {},
2547
- transform
2548
- },
2549
- ...figId !== void 0 ? { structId: figId } : {}
2550
- });
2551
- if (block.textLines.length > 0) {
2552
- const shapeBottom = bottomYUp;
2553
- const shapeTop = bottomYUp + block.heightPt;
2554
- const innerWidth = Math.max(1, block.widthPt - block.insetLeftPt - block.insetRightPt);
2555
- let textY;
2556
- if (block.anchor === "b") textY = shapeBottom + block.insetBottomPt + block.textHeightPt;
2557
- else if (block.anchor === "ctr") textY = shapeBottom + (block.heightPt + block.textHeightPt) / 2;
2558
- else textY = shapeTop - block.insetTopPt;
2559
- for (const line of block.textLines) {
2560
- const h = computeLineHeight(line, line.resolved);
2561
- textY -= h;
2562
- const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerWidth);
2563
- sink.push({
2564
- type: "line",
2565
- line,
2566
- originX: pt(x + block.insetLeftPt + lineOffset),
2567
- baselineY: pt(asm.ctx.pageHeight - (textY + lineDescent(line))),
2568
- ...figId !== void 0 ? { structId: figId } : {}
2569
- });
2570
- }
2571
- }
2572
- };
4592
+ const emitShapeAt = (x, bottomYUp, sink) => emitShapeItems(block, x, bottomYUp, sink, asm.ctx.pageHeight, figId);
2573
4593
  if (isOutOfFlowFloat(block.float)) {
2574
4594
  const fx = asm.floatX(block.float, block.widthPt);
2575
- const fy = asm.floatTopYUp(block.float);
2576
- emitShapeAt(fx, fy - block.heightPt, block.float.behind ? asm.floatsBehind : asm.floatsFront);
2577
- if (block.float.wrap !== "none") asm.exclusions.push({
2578
- x0: fx,
2579
- x1: fx + block.widthPt,
2580
- topYUp: fy,
2581
- bottomYUp: fy - block.heightPt
2582
- });
4595
+ const fy = asm.floatTopYUp(block.float, block.heightPt);
4596
+ emitShapeAt(fx, fy - block.heightPt, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
4597
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
2583
4598
  } else {
2584
4599
  asm.cursorY -= block.spacingBeforePt;
2585
4600
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
@@ -2589,45 +4604,15 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2589
4604
  asm.cursorY -= block.spacingAfterPt;
2590
4605
  }
2591
4606
  } else if (block.kind === "chart") {
2592
- const figId = builder ? createFigure(builder, block.altText, "Chart") : void 0;
2593
- const fig = figId !== void 0 ? { structId: figId } : {};
4607
+ const figId = builder ? createFigure(builder, block.altText, block.figureRole ?? "Chart") : void 0;
2594
4608
  const emitChartAt = (x, bottomYUp, sink) => {
2595
- for (const s of block.layout.shapes) sink.push({
2596
- type: "shape",
2597
- shape: {
2598
- paths: s.paths,
2599
- ...s.fillColorHex ? { fillColorHex: s.fillColorHex } : {},
2600
- ...s.stroke ? { stroke: s.stroke } : {},
2601
- transform: flipTransform([
2602
- 1,
2603
- 0,
2604
- 0,
2605
- 1,
2606
- x,
2607
- bottomYUp
2608
- ], asm.ctx.pageHeight)
2609
- },
2610
- ...fig
2611
- });
2612
- for (const t of block.layout.texts) sink.push({
2613
- type: "line",
2614
- line: t.line,
2615
- originX: pt(x + t.x),
2616
- baselineY: pt(asm.ctx.pageHeight - (bottomYUp + t.y)),
2617
- ...t.rotationDeg ? { rotationDeg: t.rotationDeg } : {},
2618
- ...fig
2619
- });
4609
+ sink.push(...chartPageItems(block, x, bottomYUp, asm.ctx.pageHeight, figId));
2620
4610
  };
2621
4611
  if (isOutOfFlowFloat(block.float)) {
2622
4612
  const fx = asm.floatX(block.float, block.widthPt);
2623
- const fy = asm.floatTopYUp(block.float);
2624
- emitChartAt(fx, fy - block.heightPt, block.float.behind ? asm.floatsBehind : asm.floatsFront);
2625
- if (block.float.wrap !== "none") asm.exclusions.push({
2626
- x0: fx,
2627
- x1: fx + block.widthPt,
2628
- topYUp: fy,
2629
- bottomYUp: fy - block.heightPt
2630
- });
4613
+ const fy = asm.floatTopYUp(block.float, block.heightPt);
4614
+ emitChartAt(fx, fy - block.heightPt, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
4615
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
2631
4616
  } else {
2632
4617
  asm.cursorY -= block.spacingBeforePt;
2633
4618
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
@@ -2638,6 +4623,19 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2638
4623
  }
2639
4624
  } else {
2640
4625
  const colCount = block.rows.reduce((max, r) => Math.max(max, r.cells.reduce((s, c) => Math.max(s, c.colStart + c.colSpan), 0)), 0);
4626
+ const anchorY = isOutOfFlowFloat(block.float) ? asm.floatTopYUp(block.float, block.heightPt) : 0;
4627
+ if (isOutOfFlowFloat(block.float) && anchorY - block.heightPt >= asm.ctx.marginBottom) {
4628
+ const fx = asm.floatX(block.float, block.totalWidthPt);
4629
+ const fy = anchorY;
4630
+ const sink = PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder);
4631
+ let rowY = fy;
4632
+ for (const row of block.rows) {
4633
+ emitRowChunk(sink, row, fx, rowY, asm.ctx.pageHeight, colCount, void 0, void 0, cellAnchorCtx(asm));
4634
+ rowY -= row.heightPt;
4635
+ }
4636
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.totalWidthPt, block.heightPt);
4637
+ continue;
4638
+ }
2641
4639
  const headerRows = [];
2642
4640
  for (const r of block.rows) if (r.isHeader) headerRows.push(r);
2643
4641
  else break;
@@ -2661,7 +4659,9 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2661
4659
  return b.create("P", cellNode).id;
2662
4660
  });
2663
4661
  }
2664
- const chunks = row.heightPt > asm.ctx.pageContentHeight ? splitRowIntoChunks(row, asm.ctx.pageContentHeight) : [row];
4662
+ const room = asm.cursorY - asm.bottomLimit();
4663
+ const page = asm.ctx.pageContentHeight;
4664
+ const chunks = !row.cantSplit && row.heightPt > room && asm.colHasContent() && room > 0 ? splitRowIntoChunks(row, room, page) : row.heightPt > page ? splitRowIntoChunks(row, page) : [row];
2665
4665
  for (let ci = 0; ci < chunks.length; ci++) {
2666
4666
  const chunk = chunks[ci];
2667
4667
  const forcedBreak = ci === 0 && row.breakBefore && !isLeadingHeader && asm.current.length > 0;
@@ -2675,7 +4675,7 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2675
4675
  }
2676
4676
  }
2677
4677
  const mergedHeights = chunks.length === 1 && row.cells.some((c) => c.mergeRole === "start") ? row.cells.map((c) => c.mergeRole === "start" ? mergedBoxHeightPt(block.rows, ri, c.colStart, asm.cursorY - asm.bottomLimit()) : void 0) : void 0;
2678
- emitRowChunk(asm.current, chunk, tableX, asm.cursorY, asm.ctx.pageHeight, colCount, cellStructIds, mergedHeights);
4678
+ emitRowChunk(asm.current, chunk, tableX, asm.cursorY, asm.ctx.pageHeight, colCount, cellStructIds, mergedHeights, cellAnchorCtx(asm));
2679
4679
  asm.cursorY -= chunk.heightPt;
2680
4680
  if (ci === chunks.length - 1 && ri < block.rows.length - 1) {
2681
4681
  const next = block.rows[ri + 1];
@@ -2744,7 +4744,7 @@ function tableCellRowSpan(rows, startRowIdx, colStart) {
2744
4744
  }
2745
4745
  return span;
2746
4746
  }
2747
- function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellStructIds, mergedHeights) {
4747
+ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellStructIds, mergedHeights, anchored) {
2748
4748
  const rowTop = cursorY;
2749
4749
  const rowBottom = cursorY - row.heightPt;
2750
4750
  {
@@ -2917,7 +4917,49 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
2917
4917
  const slack = cell.verticalAlign !== void 0 ? boxHeightPt - cell.padTopPt - cell.contentHeightPt - cell.padBottomPt : 0;
2918
4918
  const vOffset = slack <= 0 ? 0 : cell.verticalAlign === "bottom" ? slack : cell.verticalAlign === "center" ? slack / 2 : 0;
2919
4919
  let textY = rowTop - cell.padTopPt - vOffset;
2920
- for (const line of cell.lines) {
4920
+ const nestedX = cellX + cell.padLeftPt;
4921
+ const drawNestedBefore = (n) => {
4922
+ for (const nt of cell.nestedTables ?? []) {
4923
+ if (nt.afterLine !== n) continue;
4924
+ for (const nrow of nt.block.rows) {
4925
+ const nestedIds = structId !== void 0 ? nrow.cells.map(() => structId) : void 0;
4926
+ emitRowChunk(out, nrow, nestedX, textY, pageHeight, nt.block.colCount, nestedIds, void 0, anchored);
4927
+ textY -= nrow.heightPt;
4928
+ }
4929
+ }
4930
+ for (const cs of cell.shapes ?? []) {
4931
+ if (cs.afterLine !== n) continue;
4932
+ const offset = alignmentOffset(cs.block.resolvedAlignment, cs.block.widthPt, cell.widthPt - cell.padLeftPt - cell.padRightPt);
4933
+ textY -= cs.block.heightPt;
4934
+ emitShapeItems(cs.block, nestedX + offset, textY, out, pageHeight, structId);
4935
+ }
4936
+ for (const cf of cell.floats ?? []) {
4937
+ if (cf.afterLine !== n || !anchored) continue;
4938
+ const inner = cell.widthPt - cell.padLeftPt - cell.padRightPt;
4939
+ const cellTop = rowTop - cell.padTopPt;
4940
+ const cellBottom = rowTop - (mergedHeights?.[i] ?? row.heightPt) + cell.padBottomPt;
4941
+ const own = cf.block.float?.inCell !== false;
4942
+ emitCellFloat(cf, anchored, {
4943
+ ...anchored.frame,
4944
+ ...own ? {
4945
+ pageLeft: nestedX,
4946
+ pageWidth: inner,
4947
+ pageTopYUp: cellTop,
4948
+ pageBottomYUp: cellBottom,
4949
+ marginLeft: nestedX,
4950
+ contentWidth: inner,
4951
+ marginTopYUp: cellTop,
4952
+ marginBottomYUp: cellBottom
4953
+ } : {},
4954
+ columnLeft: nestedX,
4955
+ columnWidth: inner,
4956
+ anchorY: textY
4957
+ });
4958
+ }
4959
+ };
4960
+ for (const [lineIdx, line] of cell.lines.entries()) {
4961
+ drawNestedBefore(lineIdx);
4962
+ textY -= cell.lineGaps?.get(lineIdx) ?? 0;
2921
4963
  const h = computeLineHeight(line, line.resolved);
2922
4964
  textY -= h;
2923
4965
  const offset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, cell.widthPt - cell.padLeftPt - cell.padRightPt);
@@ -2926,7 +4968,7 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
2926
4968
  type: "line",
2927
4969
  line,
2928
4970
  originX: pt(cellX + cell.padLeftPt + indentLeft + offset),
2929
- baselineY: pt(pageHeight - (textY + lineDescent(line))),
4971
+ baselineY: pt(pageHeight - (textY + lineBaselineOffset(line, line.resolved))),
2930
4972
  ...cell.clipped ? { clip: {
2931
4973
  x: pt(cellX),
2932
4974
  y: pt(pageHeight - rowTop),
@@ -2936,20 +4978,16 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
2936
4978
  ...structId !== void 0 ? { structId } : {}
2937
4979
  });
2938
4980
  }
2939
- if (cell.nestedTables) {
2940
- const nestedX = cellX + cell.padLeftPt;
2941
- for (const nt of cell.nestedTables) for (const nrow of nt.rows) {
2942
- const nestedIds = structId !== void 0 ? nrow.cells.map(() => structId) : void 0;
2943
- emitRowChunk(out, nrow, nestedX, textY, pageHeight, nt.colCount, nestedIds);
2944
- textY -= nrow.heightPt;
2945
- }
2946
- }
4981
+ drawNestedBefore(cell.lines.length);
2947
4982
  }
2948
4983
  }
2949
- function splitRowIntoChunks(row, capacity) {
4984
+ function splitRowIntoChunks(row, capacity, restCapacity = capacity) {
2950
4985
  if (row.cells.some((c) => c.nestedTables && c.nestedTables.length > 0)) return [row];
2951
4986
  const queues = row.cells.map((c) => ({
2952
- remaining: [...c.lines],
4987
+ remaining: c.lines.map((line, i) => ({
4988
+ line,
4989
+ gapPt: c.lineGaps?.get(i) ?? 0
4990
+ })),
2953
4991
  template: c
2954
4992
  }));
2955
4993
  const anyHasLines = () => queues.some((q) => q.template.mergeRole === "middle" || q.template.mergeRole === "end" ? false : q.remaining.length > 0);
@@ -2967,23 +5005,31 @@ function splitRowIntoChunks(row, capacity) {
2967
5005
  continue;
2968
5006
  }
2969
5007
  const padTop = isFirst ? tpl.padTopPt : 0;
2970
- const capacityForLines = Math.max(0, capacity - padTop);
5008
+ const capacityForLines = Math.max(0, (isFirst ? capacity : restCapacity) - padTop);
2971
5009
  const taken = [];
2972
5010
  let takenHeight = 0;
2973
5011
  while (q.remaining.length > 0) {
2974
5012
  const next = q.remaining[0];
2975
- const lh = computeLineHeight(next, next.resolved);
5013
+ const lh = computeLineHeight(next.line, next.line.resolved) + next.gapPt;
2976
5014
  if (taken.length > 0 && takenHeight + lh > capacityForLines) break;
2977
5015
  taken.push(next);
2978
5016
  takenHeight += lh;
2979
5017
  q.remaining.shift();
2980
5018
  }
2981
5019
  const cellHeight = padTop + takenHeight;
5020
+ const gaps = /* @__PURE__ */ new Map();
5021
+ taken.forEach((t, li) => {
5022
+ if (t.gapPt > 0) gaps.set(li, t.gapPt);
5023
+ });
5024
+ const { lineGaps: _g, shapes: _s, floats: _f, ...restOfCell } = tpl;
2982
5025
  const chunkCell = {
2983
- ...tpl,
5026
+ ...restOfCell,
5027
+ ...isFirst && tpl.shapes ? { shapes: tpl.shapes } : {},
5028
+ ...isFirst && tpl.floats ? { floats: tpl.floats } : {},
5029
+ ...gaps.size > 0 ? { lineGaps: gaps } : {},
2984
5030
  padTopPt: padTop,
2985
5031
  padBottomPt: 0,
2986
- lines: taken,
5032
+ lines: taken.map((t) => t.line),
2987
5033
  contentHeightPt: takenHeight,
2988
5034
  totalHeightPt: cellHeight
2989
5035
  };
@@ -3022,7 +5068,8 @@ function splitRowIntoChunks(row, capacity) {
3022
5068
  cells: chunkCells,
3023
5069
  columnXOffsets: row.columnXOffsets,
3024
5070
  rowIdx: row.rowIdx,
3025
- rowCount: row.rowCount
5071
+ rowCount: row.rowCount,
5072
+ ...row.isHeader ? { isHeader: true } : {}
3026
5073
  });
3027
5074
  isFirst = false;
3028
5075
  }
@@ -3147,4 +5194,4 @@ function emitRowBottomEdge(out, row, marginLeft, rowBottom, pageHeight) {
3147
5194
  }
3148
5195
  }
3149
5196
  //#endregion
3150
- export { layoutStyledDocument };
5197
+ export { GLUE_SHRINK_RATIO, layoutStyledDocument };