reamkit 1.15.4 → 1.17.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 (127) hide show
  1. package/dist/esm/core/bidi/segments.js +1 -2
  2. package/dist/esm/core/converter/facade.d.ts +5 -0
  3. package/dist/esm/core/converter/project.js +5 -1
  4. package/dist/esm/core/converter/ream.d.ts +6 -0
  5. package/dist/esm/core/converter/ream.js +34 -8
  6. package/dist/esm/core/document-model/index.d.ts +1 -1
  7. package/dist/esm/core/document-model/types.d.ts +659 -7
  8. package/dist/esm/core/drawingml/chart-geometry.d.ts +34 -10
  9. package/dist/esm/core/drawingml/chart-geometry.js +424 -92
  10. package/dist/esm/core/drawingml/chart-parser.js +301 -11
  11. package/dist/esm/core/drawingml/colors.d.ts +7 -7
  12. package/dist/esm/core/drawingml/colors.js +84 -18
  13. package/dist/esm/core/drawingml/preset-geometry.js +240 -6
  14. package/dist/esm/core/drawingml/shape-render.d.ts +12 -0
  15. package/dist/esm/core/drawingml/shape-render.js +116 -3
  16. package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
  17. package/dist/esm/core/drawingml/theme-parser.d.ts +51 -0
  18. package/dist/esm/core/drawingml/theme-parser.js +92 -1
  19. package/dist/esm/core/font/measure.d.ts +5 -0
  20. package/dist/esm/core/font/measure.js +21 -1
  21. package/dist/esm/core/font/ttf-subset.js +26 -1
  22. package/dist/esm/core/images.d.ts +2 -2
  23. package/dist/esm/core/images.js +353 -43
  24. package/dist/esm/core/indexed-colors.d.ts +11 -0
  25. package/dist/esm/core/indexed-colors.js +81 -0
  26. package/dist/esm/core/ir/flow.d.ts +23 -1
  27. package/dist/esm/core/ir/sheet.d.ts +61 -0
  28. package/dist/esm/{excel → core}/number-format.d.ts +37 -0
  29. package/dist/esm/core/number-format.js +800 -0
  30. package/dist/esm/core/numbering/apply.d.ts +5 -3
  31. package/dist/esm/core/numbering/apply.js +42 -11
  32. package/dist/esm/core/numbering/index.js +2 -0
  33. package/dist/esm/core/numbering/state.d.ts +13 -1
  34. package/dist/esm/core/numbering/state.js +256 -15
  35. package/dist/esm/core/opc/alternate-content.d.ts +13 -0
  36. package/dist/esm/core/opc/alternate-content.js +72 -0
  37. package/dist/esm/core/opc/core-properties.js +1 -0
  38. package/dist/esm/core/opc/package.d.ts +14 -1
  39. package/dist/esm/core/opc/package.js +43 -4
  40. package/dist/esm/core/opc/relationships.js +1 -0
  41. package/dist/esm/core/opc/xml-entities.d.ts +12 -0
  42. package/dist/esm/core/opc/xml-entities.js +91 -0
  43. package/dist/esm/core/po-helpers.js +16 -1
  44. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  45. package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
  46. package/dist/esm/core/style-cascade/resolver.js +66 -11
  47. package/dist/esm/core/style-cascade/table.js +35 -11
  48. package/dist/esm/core/style-cascade/types.d.ts +42 -1
  49. package/dist/esm/core/style-cascade/types.js +4 -0
  50. package/dist/esm/core/vector.d.ts +30 -0
  51. package/dist/esm/excel/activex-parser.js +1 -0
  52. package/dist/esm/excel/column-bands.d.ts +26 -1
  53. package/dist/esm/excel/column-bands.js +156 -20
  54. package/dist/esm/excel/comments-parser.js +5 -3
  55. package/dist/esm/excel/conditional-format.d.ts +25 -2
  56. package/dist/esm/excel/conditional-format.js +63 -19
  57. package/dist/esm/excel/escaped-text.d.ts +9 -0
  58. package/dist/esm/excel/escaped-text.js +20 -0
  59. package/dist/esm/excel/form-control-parser.js +1 -0
  60. package/dist/esm/excel/formula/dates.js +1 -1
  61. package/dist/esm/excel/header-footer.d.ts +9 -1
  62. package/dist/esm/excel/header-footer.js +162 -14
  63. package/dist/esm/excel/index.d.ts +1 -1
  64. package/dist/esm/excel/index.js +2 -2
  65. package/dist/esm/excel/pivot-table-parser.js +1 -0
  66. package/dist/esm/excel/print-model.d.ts +75 -0
  67. package/dist/esm/excel/print-model.js +899 -77
  68. package/dist/esm/excel/printer-settings.d.ts +16 -0
  69. package/dist/esm/excel/printer-settings.js +36 -0
  70. package/dist/esm/excel/rich-value.d.ts +13 -0
  71. package/dist/esm/excel/rich-value.js +120 -0
  72. package/dist/esm/excel/shared-strings-parser.js +11 -7
  73. package/dist/esm/excel/sheet-drawing.d.ts +11 -0
  74. package/dist/esm/excel/sheet-drawing.js +26 -3
  75. package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
  76. package/dist/esm/excel/sheet-shape-parser.js +461 -32
  77. package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
  78. package/dist/esm/excel/sheet-to-flow.js +553 -36
  79. package/dist/esm/excel/slicer-parser.js +1 -0
  80. package/dist/esm/excel/styles-parser.d.ts +30 -2
  81. package/dist/esm/excel/styles-parser.js +177 -40
  82. package/dist/esm/excel/table-parser.js +1 -0
  83. package/dist/esm/excel/vml-drawing.d.ts +68 -0
  84. package/dist/esm/excel/vml-drawing.js +158 -0
  85. package/dist/esm/excel/workbook-parser.d.ts +5 -0
  86. package/dist/esm/excel/workbook-parser.js +5 -1
  87. package/dist/esm/excel/worksheet-parser.d.ts +2 -1
  88. package/dist/esm/excel/worksheet-parser.js +150 -61
  89. package/dist/esm/excel/xlsx-reader.js +242 -11
  90. package/dist/esm/excel/xlsx-to-pdf.js +12 -1
  91. package/dist/esm/html/html-writer.js +29 -6
  92. package/dist/esm/layout/page-doc.d.ts +87 -1
  93. package/dist/esm/layout/styled-layout.d.ts +46 -2
  94. package/dist/esm/layout/styled-layout.js +2345 -274
  95. package/dist/esm/pdf/cid-font.js +20 -9
  96. package/dist/esm/pdf/objects.d.ts +13 -0
  97. package/dist/esm/pdf/objects.js +18 -1
  98. package/dist/esm/pdf/shading.js +76 -6
  99. package/dist/esm/pdf/styled-page-emitter.js +355 -49
  100. package/dist/esm/pdf/text-page-renderer.js +1 -0
  101. package/dist/esm/pdf/vector-graphics.d.ts +31 -2
  102. package/dist/esm/pdf/vector-graphics.js +103 -3
  103. package/dist/esm/pptx/pptx-reader.js +2 -0
  104. package/dist/esm/pptx/slide-parser.d.ts +21 -4
  105. package/dist/esm/pptx/slide-parser.js +98 -24
  106. package/dist/esm/svg/svg-writer.js +23 -4
  107. package/dist/esm/word/document-parser.d.ts +96 -7
  108. package/dist/esm/word/document-parser.js +538 -116
  109. package/dist/esm/word/docx-reader.js +135 -43
  110. package/dist/esm/word/docx-writer.js +6 -1
  111. package/dist/esm/word/drawing-parser.d.ts +86 -5
  112. package/dist/esm/word/drawing-parser.js +1590 -54
  113. package/dist/esm/word/index.d.ts +4 -4
  114. package/dist/esm/word/numbering-parser.d.ts +2 -1
  115. package/dist/esm/word/numbering-parser.js +108 -21
  116. package/dist/esm/word/paragraph-properties.d.ts +11 -1
  117. package/dist/esm/word/paragraph-properties.js +219 -10
  118. package/dist/esm/word/run-properties.js +45 -1
  119. package/dist/esm/word/settings-parser.d.ts +24 -0
  120. package/dist/esm/word/settings-parser.js +32 -5
  121. package/dist/esm/word/shading.d.ts +11 -0
  122. package/dist/esm/word/shading.js +64 -0
  123. package/dist/esm/word/styles-parser.js +9 -2
  124. package/dist/esm/word/table-parser.d.ts +9 -1
  125. package/dist/esm/word/table-parser.js +182 -30
  126. package/package.json +7 -3
  127. package/dist/esm/excel/number-format.js +0 -476
@@ -10,17 +10,18 @@ import { FORCED_BREAK, breakLines } from "../core/line-breaker/knuth-plass.js";
10
10
  import { greedyBreakLines } from "../core/line-breaker/greedy.js";
11
11
  import { cjkBreakBetween, splitCjkSegment } from "../core/line-breaker/cjk.js";
12
12
  import "../core/line-breaker/index.js";
13
- import { applyNumbering, applyNumberingToHeadersFooters } from "../core/numbering/apply.js";
14
13
  import { DEFAULT_RESOLVED_PARAGRAPH, DEFAULT_RESOLVED_RUN } from "../core/style-cascade/types.js";
15
14
  import { resolveParagraphProperties, resolveRunProperties } from "../core/style-cascade/resolver.js";
16
15
  import "../core/style-cascade/index.js";
16
+ import { applyNumbering, applyNumberingToHeadersFooters } from "../core/numbering/apply.js";
17
+ import "../core/numbering/index.js";
17
18
  import { PathBuilder, flipTransform } from "../core/vector.js";
18
19
  import { buildSparkline } from "../core/drawingml/sparkline-geometry.js";
19
20
  import { arcPoint, arcToBeziers } from "../core/arc-to-bezier.js";
20
- import { buildChartScene } from "../core/drawingml/chart-geometry.js";
21
+ import { buildChartScene, legendSeriesName } from "../core/drawingml/chart-geometry.js";
21
22
  import { layoutMath, mathGlyphSegments, variantStyle } from "./math-layout.js";
22
23
  import { rectPath } from "../core/drawingml/preset-geometry.js";
23
- import { buildShapePaths, buildShapeTransform, buildStroke, gradientToSolid } from "../core/drawingml/shape-render.js";
24
+ import { buildShapePaths, buildShapeTransform, buildStroke, gradientToSolid, lineEndPaths } from "../core/drawingml/shape-render.js";
24
25
  import { StructTreeBuilder } from "../pdf/struct-tree.js";
25
26
  //#region src/layout/styled-layout.ts
26
27
  function parsePdfAProfile(pdfA) {
@@ -33,6 +34,14 @@ function parsePdfAProfile(pdfA) {
33
34
  version: part === 1 ? "1.4" : "1.7"
34
35
  };
35
36
  }
37
+ /** §17.6.4 — how heavy Word draws the rule between columns. */
38
+ var COLUMN_SEPARATOR_PT = .5;
39
+ /**
40
+ * How far a space may be squeezed below its natural width — the shrink the
41
+ * line breaker offers when it weighs a line, and therefore the shrink the
42
+ * emitter owes it back when it draws one.
43
+ */
44
+ var GLUE_SHRINK_RATIO = .3;
36
45
  var TWIP_TO_PT = 1 / 20;
37
46
  var EIGHTH_PT = 1 / 8;
38
47
  var DEFAULT_CELL_PADDING_TWIPS = 108;
@@ -40,6 +49,7 @@ var DEFAULT_BORDER_SIZE_EIGHTH = 4;
40
49
  var FOOTNOTE_RULE_PT = .75;
41
50
  var FOOTNOTE_RULE_GAP_ABOVE = 4;
42
51
  var FOOTNOTE_SEPARATOR_HEIGHT = 10;
52
+ var LINE_NUMBER_GAP_PT = 18;
43
53
  var FLOAT_TEXT_GAP = 6;
44
54
  var MIN_WRAP_WIDTH = 36;
45
55
  function isOutOfFlowFloat(f) {
@@ -257,21 +267,48 @@ function commentTailBlocks(comment, n, opts) {
257
267
  */
258
268
  function layoutStyledDocument(body, options) {
259
269
  const sectionList = resolveSectionList(body, options);
260
- const noteAssigned = assignNoteNumbers(applyNumbering(body, options.numbering), options.comments ? new Set(options.comments.keys()) : void 0);
270
+ const noteAssigned = assignNoteNumbers(applyNumbering(body, options.numbering, options.styles), options.comments ? new Set(options.comments.keys()) : void 0);
261
271
  const numberedBody = noteAssigned.body;
262
- const numberedHeadersFooters = applyNumberingToHeadersFooters(options.headersFooters, options.numbering);
272
+ const numberedHeadersFooters = applyNumberingToHeadersFooters(options.headersFooters, options.numbering, options.styles);
263
273
  const pdfaProfile = options.pdfA ? parsePdfAProfile(options.pdfA) : void 0;
264
274
  const tagged = options.tagged === true || options.pdfUA === true || (pdfaProfile?.tagged ?? false);
265
275
  const structBuilder = tagged ? new StructTreeBuilder() : void 0;
266
276
  const fontResources = collectFontResources(numberedBody, numberedHeadersFooters, options);
267
277
  const imageResources = collectImageResources(numberedBody, numberedHeadersFooters, options);
268
- const sectionCtxs = sectionList.map((s) => buildSectionContext(s, options, numberedHeadersFooters, fontResources));
278
+ const sectionCtxs = sectionList.map((s) => buildSectionContext(s, options, numberedHeadersFooters, fontResources, imageResources));
269
279
  let sectionIdx = 0;
280
+ const bodyFrames = frameGroups(numberedBody);
270
281
  const blocks = numberedBody.map((el, idx) => {
271
282
  while (sectionIdx < sectionCtxs.length - 1 && idx >= sectionCtxs[sectionIdx].endIndex) sectionIdx++;
272
283
  const ctx = sectionCtxs[sectionIdx];
273
- return layoutBodyElement(el, options, fontResources, imageResources, ctx.columns ? ctx.columns[0].widthPt : ctx.contentWidth, ctx.pageContentHeight);
284
+ const width = ctx.columns ? ctx.columns[0].widthPt : ctx.contentWidth;
285
+ const box = {
286
+ pageWidthPt: ctx.pageWidth,
287
+ pageHeightPt: ctx.pageHeight,
288
+ marginLeftPt: ctx.marginLeft,
289
+ marginRightPt: ctx.pageWidth - ctx.marginLeft - ctx.contentWidth,
290
+ marginTopPt: ctx.marginTop,
291
+ marginBottomPt: ctx.marginBottom
292
+ };
293
+ const group = bodyFrames.starts.get(idx);
294
+ if (group) return layoutFrameBlock(group, ctx.options ?? options, fontResources, imageResources, width);
295
+ if (bodyFrames.continued.has(idx)) return emptyBlock(options);
296
+ return layoutBodyElement(el, ctx.options ?? options, fontResources, imageResources, width, ctx.pageContentHeight, box);
274
297
  });
298
+ for (let i = 0; i < blocks.length; i++) {
299
+ const here = blocks[i];
300
+ const next = blocks[i + 1];
301
+ if (here?.kind !== "paragraph" || next?.kind !== "paragraph") continue;
302
+ if (here.source?.properties.styleId !== next.source?.properties.styleId) continue;
303
+ if (here.resolved.contextualSpacing) blocks[i] = {
304
+ ...here,
305
+ spacingAfterPt: 0
306
+ };
307
+ if (next.resolved.contextualSpacing) blocks[i + 1] = {
308
+ ...next,
309
+ spacingBeforePt: 0
310
+ };
311
+ }
275
312
  const noteBlockCache = /* @__PURE__ */ new Map();
276
313
  const notePlan = options.footnotes && noteAssigned.footnotes.size > 0 ? {
277
314
  numbers: noteAssigned.footnotes,
@@ -286,7 +323,7 @@ function layoutStyledDocument(body, options) {
286
323
  const n = noteAssigned.footnotes.get(id);
287
324
  let laid = void 0;
288
325
  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));
326
+ const noteBlocks = substituteNoteNumber(content, n).map((el) => layoutBodyElement(el, sectionCtx.options ?? options, fontResources, imageResources, sectionCtx.contentWidth, sectionCtx.pageContentHeight));
290
327
  laid = {
291
328
  blocks: noteBlocks,
292
329
  heightPt: noteBlocks.reduce((sum, b) => sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : 0), 0)
@@ -314,7 +351,24 @@ function layoutStyledDocument(body, options) {
314
351
  n
315
352
  });
316
353
  }
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));
354
+ if (tailNotes.length > 0) blocks.push(layoutShapeBlock({
355
+ width: pt(FOOTNOTE_RULE_WIDTH),
356
+ height: pt(FOOTNOTE_RULE_PT),
357
+ geometry: {
358
+ kind: "preset",
359
+ preset: "rect",
360
+ adjust: /* @__PURE__ */ new Map()
361
+ },
362
+ fill: {
363
+ kind: "solid",
364
+ colorHex: "000000"
365
+ },
366
+ paragraphProperties: {
367
+ spacingBefore: pt(FOOTNOTE_SEPARATOR_HEIGHT),
368
+ spacingAfter: pt(FOOTNOTE_RULE_GAP_ABOVE)
369
+ }
370
+ }, options, fontResources, imageResources, lastCtx.contentWidth));
371
+ 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
372
  const commentNums = noteAssigned.comments;
319
373
  const threadDepth = (id) => {
320
374
  let depth = 0;
@@ -346,7 +400,7 @@ function layoutStyledDocument(body, options) {
346
400
  const commentNotes = options.commentAnnotations && options.comments ? buildCommentNotes(noteAssigned.comments, options.comments) : void 0;
347
401
  const reflowParagraph = (paragraph, width, widths) => layoutParagraphBlock(paragraph, options, fontResources, imageResources, width, widths);
348
402
  return {
349
- pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph),
403
+ 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
404
  resources: options.resources ?? new ResourceStore(),
351
405
  fontResources,
352
406
  imageResources,
@@ -367,14 +421,16 @@ function resolvePageDimensions(options, section) {
367
421
  const sectionRight = section?.margins?.right !== void 0 ? section.margins.right : void 0;
368
422
  const sectionTop = section?.margins?.top !== void 0 ? section.margins.top : void 0;
369
423
  const sectionBottom = section?.margins?.bottom !== void 0 ? section.margins.bottom : void 0;
424
+ const gutter = section?.margins?.gutter ?? 0;
425
+ const atTop = options.gutterAtTop === true;
370
426
  const headerOffsetPt = section?.margins?.header ?? 720 * TWIP_TO_PT;
371
427
  const footerOffsetPt = section?.margins?.footer ?? 720 * TWIP_TO_PT;
372
428
  return {
373
429
  pageWidth: options.pageWidth ?? sectionPageWidth ?? 595,
374
430
  pageHeight: options.pageHeight ?? sectionPageHeight ?? 842,
375
- marginLeft: options.marginLeft ?? sectionLeft ?? 72,
431
+ marginLeft: (options.marginLeft ?? sectionLeft ?? 72) + (atTop ? 0 : gutter),
376
432
  marginRight: options.marginRight ?? sectionRight ?? 72,
377
- marginTop: options.marginTop ?? sectionTop ?? 72,
433
+ marginTop: (options.marginTop ?? sectionTop ?? 72) + (atTop ? gutter : 0),
378
434
  marginBottom: options.marginBottom ?? sectionBottom ?? 72,
379
435
  headerOffsetPt,
380
436
  footerOffsetPt
@@ -394,27 +450,50 @@ function resolveSectionList(body, options) {
394
450
  endIndex: body.length
395
451
  }];
396
452
  }
397
- function buildSectionContext(section, options, headersFooters, fontResources) {
453
+ function buildSectionContext(section, options, headersFooters, fontResources, imageResources) {
398
454
  const dims = resolvePageDimensions(options, section.properties);
399
455
  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);
456
+ const secOptions = section.properties.gridLinePitchPt !== void 0 ? {
457
+ ...options,
458
+ gridLinePitchPt: section.properties.gridLinePitchPt
459
+ } : options;
460
+ const relativeBox = {
461
+ pageWidthPt: dims.pageWidth,
462
+ pageHeightPt: dims.pageHeight,
463
+ marginLeftPt: dims.marginLeft,
464
+ marginRightPt: dims.marginRight,
465
+ marginTopPt: dims.marginTop,
466
+ marginBottomPt: dims.marginBottom
467
+ };
468
+ const headerSet = layoutHeaderSet(section.properties, headersFooters, secOptions, fontResources, imageResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.headerOffsetPt, {
469
+ top: dims.marginTop,
470
+ bottom: dims.marginBottom
471
+ }, relativeBox);
472
+ const footerSet = layoutFooterSet(section.properties, headersFooters, secOptions, fontResources, imageResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.footerOffsetPt, {
473
+ top: dims.marginTop,
474
+ bottom: dims.marginBottom
475
+ }, relativeBox);
402
476
  const columns = buildColumnGeometry(section.properties.columns, contentWidth);
477
+ 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);
478
+ const headerBottom = headerHeight > 0 ? dims.headerOffsetPt + headerHeight : 0;
479
+ const marginTop = Math.max(dims.marginTop, headerBottom);
403
480
  return {
404
481
  endIndex: section.endIndex,
405
482
  properties: section.properties,
406
483
  pageWidth: dims.pageWidth,
407
484
  pageHeight: dims.pageHeight,
408
485
  marginLeft: dims.marginLeft,
409
- marginTop: dims.marginTop,
486
+ marginTop,
410
487
  marginBottom: dims.marginBottom,
411
488
  contentWidth,
412
- pageContentHeight: dims.pageHeight - dims.marginTop - dims.marginBottom,
489
+ pageContentHeight: dims.pageHeight - marginTop - dims.marginBottom,
413
490
  ...columns ? { columns } : {},
414
491
  headerSet,
415
492
  footerSet,
416
493
  titlePg: section.properties.titlePg === true,
417
- evenAndOddHeaders: section.properties.evenAndOddHeaders === true
494
+ evenAndOddHeaders: section.properties.evenAndOddHeaders === true,
495
+ continuous: section.properties.sectionStart === "continuous",
496
+ options: secOptions
418
497
  };
419
498
  }
420
499
  function buildColumnGeometry(cols, contentWidth) {
@@ -449,20 +528,30 @@ function markPagination(cmds) {
449
528
  artifact: "pagination"
450
529
  }));
451
530
  }
452
- function layoutHeaderSet(section, headersFooters, options, fontResources, contentWidth, marginLeft, pageHeight, headerOffsetPt) {
531
+ function layoutHeaderSet(section, headersFooters, options, fontResources, imageResources, contentWidth, marginLeft, pageHeight, headerOffsetPt, bandMargins, relativeBox) {
453
532
  const band = (type) => {
454
533
  const ref = refByType(section.headers, type);
455
534
  if (!ref) return { commands: [] };
456
535
  const content = headersFooters.get(ref.relationshipId);
457
536
  if (!content) return { commands: [] };
537
+ let measured = 0;
458
538
  const render = (c) => {
459
- return markPagination(drawBlocksSequentially(laidOutBlocksFor(c, options, fontResources, contentWidth), marginLeft, pageHeight - headerOffsetPt, pageHeight));
539
+ const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth, imageResources, relativeBox);
540
+ measured = blocksHeight(blocks);
541
+ return markPagination(drawBlocksSequentially(blocks, marginLeft, pageHeight - headerOffsetPt, pageHeight, contentWidth, void 0, bandMargins));
460
542
  };
461
- if (contentHasPageFields(content)) return {
462
- commands: [],
463
- renderDynamic: (n, total) => render(substitutePageFields(content, n, total))
543
+ if (contentHasPageFields(content)) {
544
+ render(substitutePageFields(content, 1, 1));
545
+ return {
546
+ commands: [],
547
+ heightPt: measured,
548
+ renderDynamic: (n, total) => render(substitutePageFields(content, n, total))
549
+ };
550
+ }
551
+ return {
552
+ commands: render(content),
553
+ heightPt: measured
464
554
  };
465
- return { commands: render(content) };
466
555
  };
467
556
  return {
468
557
  default: band("default"),
@@ -470,15 +559,37 @@ function layoutHeaderSet(section, headersFooters, options, fontResources, conten
470
559
  even: band("even")
471
560
  };
472
561
  }
473
- function layoutFooterSet(section, headersFooters, options, fontResources, contentWidth, marginLeft, pageHeight, footerOffsetPt) {
562
+ /**
563
+ * §17.6.4 — the height each column of a BALANCED band is given: the band's
564
+ * share, but never more than the page has room for, and never so little that
565
+ * the band could not fit the columns at all.
566
+ *
567
+ * @param bandHeightPt The band's whole laid-out height.
568
+ * @param colCount How many columns it has to share.
569
+ * @param roomPt What the page has left below the band's top.
570
+ * @returns The per-column height, or 0 when the band should just fill.
571
+ */
572
+ function balancedColumnHeight(bandHeightPt, colCount, roomPt) {
573
+ if (bandHeightPt <= 0 || colCount < 2 || roomPt <= 0) return 0;
574
+ if (bandHeightPt > roomPt * colCount) return 0;
575
+ return Math.min(roomPt, bandHeightPt / colCount);
576
+ }
577
+ /** Total laid-out height of a run of blocks, paragraph spacing included. */
578
+ function blocksHeight(blocks) {
579
+ return blocks.reduce((sum, b) => {
580
+ if (b.kind !== "paragraph" && b.kind !== "table" && isOutOfFlowFloat(b.float)) return sum;
581
+ return sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt);
582
+ }, 0);
583
+ }
584
+ function layoutFooterSet(section, headersFooters, options, fontResources, imageResources, contentWidth, marginLeft, pageHeight, footerOffsetPt, bandMargins, relativeBox) {
474
585
  const band = (type) => {
475
586
  const ref = refByType(section.footers, type);
476
587
  if (!ref) return { commands: [] };
477
588
  const content = headersFooters.get(ref.relationshipId);
478
589
  if (!content) return { commands: [] };
479
590
  const render = (c) => {
480
- const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth);
481
- return markPagination(drawBlocksSequentially(blocks, marginLeft, footerOffsetPt + blocks.reduce((sum, b) => sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt), 0), pageHeight));
591
+ const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth, imageResources, relativeBox);
592
+ return markPagination(drawBlocksSequentially(blocks, marginLeft, footerOffsetPt + blocksHeight(blocks), pageHeight, contentWidth, void 0, bandMargins));
482
593
  };
483
594
  if (contentHasPageFields(content)) return {
484
595
  commands: [],
@@ -514,7 +625,7 @@ function substitutePageFields(content, pageNumber, totalPages) {
514
625
  }
515
626
  function pickBand(set, band) {
516
627
  const has = (e) => e.commands.length > 0 || e.renderDynamic !== void 0;
517
- if (band === "first") return has(set.first) ? set.first : set.default;
628
+ if (band === "first") return set.first;
518
629
  if (band === "even") return has(set.even) ? set.even : set.default;
519
630
  return set.default;
520
631
  }
@@ -523,24 +634,236 @@ function bandForPage(pageInSection, globalPageIdx, titlePg, evenAndOddHeaders) {
523
634
  if (evenAndOddHeaders && (globalPageIdx + 1) % 2 === 0) return "even";
524
635
  return "default";
525
636
  }
526
- function laidOutBlocksFor(elements, options, fontResources, contentWidth) {
527
- return elements.map((el) => layoutBodyElement(el, options, fontResources, void 0, contentWidth));
637
+ /**
638
+ * §17.3.1.11 — the runs of consecutive paragraphs that carry the SAME frame.
639
+ * Word treats them as one floating text frame, so they lay out together in one
640
+ * box; the entries after the first become empty blocks, which keeps every
641
+ * caller's element indexing (and its section bounds) intact.
642
+ *
643
+ * @param elements The body elements, in order.
644
+ * @returns `starts`: the index each frame begins at → its paragraphs;
645
+ * `continued`: the indices swallowed by a frame that began earlier.
646
+ */
647
+ function frameGroups(elements) {
648
+ const starts = /* @__PURE__ */ new Map();
649
+ const continued = /* @__PURE__ */ new Set();
650
+ const key = (p) => p.properties.frame ? JSON.stringify(p.properties.frame) : void 0;
651
+ let i = 0;
652
+ while (i < elements.length) {
653
+ const el = elements[i];
654
+ const k = el.kind === "paragraph" ? key(el.paragraph) : void 0;
655
+ if (k === void 0) {
656
+ i++;
657
+ continue;
658
+ }
659
+ const members = [el.paragraph];
660
+ let j = i + 1;
661
+ while (j < elements.length) {
662
+ const next = elements[j];
663
+ if (next.kind !== "paragraph" || key(next.paragraph) !== k) break;
664
+ members.push(next.paragraph);
665
+ continued.add(j);
666
+ j++;
667
+ }
668
+ starts.set(i, members);
669
+ i = j;
670
+ }
671
+ return {
672
+ starts,
673
+ continued
674
+ };
675
+ }
676
+ /** A block that occupies no space at all — the tail of a frame group. */
677
+ function emptyBlock(options) {
678
+ return {
679
+ kind: "paragraph",
680
+ resolved: resolveParagraphProperties({}, options.styles),
681
+ lines: [],
682
+ heightPt: 0,
683
+ spacingBeforePt: 0,
684
+ spacingAfterPt: 0
685
+ };
686
+ }
687
+ /**
688
+ * A text frame as the floating box it is: the paragraphs inside it lay out at
689
+ * the frame's width, the box takes the height the frame states or the text
690
+ * needs, and the body flows past it as its `w:wrap` says.
691
+ *
692
+ * @param paragraphs The frame's paragraphs, in order.
693
+ * @param options Render options.
694
+ * @param fontResources Fonts by variant.
695
+ * @param imageResources Images by resource id.
696
+ * @param contentWidth The width available where the frame is anchored.
697
+ * @returns The laid-out frame.
698
+ */
699
+ function layoutFrameBlock(paragraphs, options, fontResources, imageResources, contentWidth) {
700
+ const frame = paragraphs[0].properties.frame;
701
+ const exact = frame.heightRule === "exact" && frame.heightPt !== void 0;
702
+ const raw = (widthPt, pin) => layoutShapeBlock(frameShape(frame, paragraphs, widthPt, pin), options, fontResources, imageResources, contentWidth);
703
+ const atLeast = !exact && frame.heightPt !== void 0 && frame.heightRule !== "auto" ? frame.heightPt : 0;
704
+ const build = (widthPt) => {
705
+ const laid = raw(widthPt, exact);
706
+ return laid.kind === "shape" && atLeast > laid.heightPt ? raw(widthPt, true) : laid;
707
+ };
708
+ if (frame.widthPt !== void 0) return build(frame.widthPt);
709
+ const measured = build(contentWidth);
710
+ const natural = measured.kind === "shape" ? Math.max(1, ...measured.textLines.map((l) => l.contentWidthPt)) : contentWidth;
711
+ return build(Math.min(contentWidth, natural));
712
+ }
713
+ var MAX_FIT_PASSES = 4;
714
+ function scaleTextBody(text, k) {
715
+ return {
716
+ ...text,
717
+ content: text.content.map((el) => el.kind === "paragraph" ? {
718
+ ...el,
719
+ paragraph: {
720
+ ...el.paragraph,
721
+ runs: el.paragraph.runs.map((run) => ({
722
+ ...run,
723
+ properties: {
724
+ ...run.properties,
725
+ fontSizePt: pt((run.properties.fontSizePt ?? 11) * k)
726
+ }
727
+ }))
728
+ }
729
+ } : el)
730
+ };
731
+ }
732
+ function frameShape(frame, paragraphs, widthPt, exact) {
733
+ const lead = paragraphs[0]?.properties;
734
+ const edge = lead?.borders?.top ?? lead?.borders?.left;
735
+ const vertical = lead?.textDirection === "btLr" ? "vert270" : lead?.textDirection === "tbRl" ? "vert" : void 0;
736
+ const painted = lead?.shading !== void 0 || edge !== void 0 && edge.style !== "none";
737
+ return {
738
+ width: pt(widthPt),
739
+ height: pt(exact ? frame.heightPt ?? 0 : 0),
740
+ geometry: painted ? {
741
+ kind: "preset",
742
+ preset: "rect",
743
+ adjust: /* @__PURE__ */ new Map()
744
+ } : {
745
+ kind: "custom",
746
+ custom: {
747
+ pathWidth: 0,
748
+ pathHeight: 0,
749
+ commands: []
750
+ }
751
+ },
752
+ fill: lead?.shading ? {
753
+ kind: "solid",
754
+ colorHex: lead.shading.colorHex
755
+ } : { kind: "none" },
756
+ ...edge && edge.style !== "none" ? { line: {
757
+ fill: "solid",
758
+ colorHex: edge.colorHex ?? "000000",
759
+ width: edge.width ?? pt(.75)
760
+ } } : {},
761
+ float: frameFloat(frame),
762
+ paragraphProperties: {},
763
+ text: {
764
+ content: paragraphs.map((paragraph) => ({
765
+ kind: "paragraph",
766
+ paragraph
767
+ })),
768
+ insetLeft: pt(0),
769
+ insetRight: pt(0),
770
+ insetTop: pt(0),
771
+ insetBottom: pt(0),
772
+ ...vertical ? { vertical } : {},
773
+ ...exact ? {} : { autoFit: true }
774
+ }
775
+ };
776
+ }
777
+ /** §17.3.1.11 → §20.4.2.3: the frame's anchor, in the terms floats are placed in. */
778
+ function frameFloat(frame) {
779
+ const wrap = frame.wrap === "none" ? "none" : frame.wrap === "notBeside" ? "topAndBottom" : "square";
780
+ const hRel = frame.hAnchor === "page" ? "page" : frame.hAnchor === "margin" ? "margin" : "column";
781
+ const vRel = frame.vAnchor === "page" ? "page" : frame.vAnchor === "margin" ? "margin" : "paragraph";
782
+ const align = frame.xAlign === "left" || frame.xAlign === "center" || frame.xAlign === "right" ? frame.xAlign : void 0;
783
+ const vAlign = frame.yAlign === "top" || frame.yAlign === "center" || frame.yAlign === "bottom" ? frame.yAlign : void 0;
784
+ return {
785
+ wrap,
786
+ posH: {
787
+ relativeFrom: hRel,
788
+ ...align ? { align } : { offsetPt: frame.xPt ?? pt(0) }
789
+ },
790
+ posV: {
791
+ relativeFrom: vRel,
792
+ ...vAlign ? { align: vAlign } : { offsetPt: frame.yPt ?? pt(0) }
793
+ }
794
+ };
795
+ }
796
+ /**
797
+ * A drawing's width when it states one as a share of the page or the margins.
798
+ *
799
+ * @param rel The relative size, if any.
800
+ * @param content The margin box's width.
801
+ * @param box The page box, when the caller knows it.
802
+ * @returns The width in points, or `undefined` when none is stated.
803
+ */
804
+ function relativeWidth(rel, content, box) {
805
+ if (!rel?.widthPct) return void 0;
806
+ const from = rel.widthFrom;
807
+ return (box === void 0 || from === void 0 || from === "margin" ? content : from === "page" ? box.pageWidthPt : from === "leftMargin" ? box.marginLeftPt : box.marginRightPt) * rel.widthPct;
808
+ }
809
+ /** The height twin of {@link relativeWidth}; the margin box's height is the page's content height. */
810
+ function relativeHeight(rel, contentHeight, box) {
811
+ if (!rel?.heightPct) return void 0;
812
+ const from = rel.heightFrom;
813
+ const base = box === void 0 || from === void 0 || from === "margin" ? contentHeight : from === "page" ? box.pageHeightPt : from === "topMargin" ? box.marginTopPt : box.marginBottomPt;
814
+ return base === void 0 ? void 0 : base * rel.heightPct;
528
815
  }
529
- function layoutBodyElement(el, options, fontResources, imageResources, contentWidth, maxHeight) {
816
+ function laidOutBlocksFor(elements, options, fontResources, contentWidth, imageResources, box) {
817
+ const frames = frameGroups(elements);
818
+ return elements.map((el, idx) => {
819
+ const group = frames.starts.get(idx);
820
+ if (group) return layoutFrameBlock(group, options, fontResources, imageResources, contentWidth);
821
+ if (frames.continued.has(idx)) return emptyBlock(options);
822
+ return layoutBodyElement(el, options, fontResources, imageResources, contentWidth, void 0, box);
823
+ });
824
+ }
825
+ function layoutBodyElement(el, options, fontResources, imageResources, contentWidth, maxHeight, box) {
530
826
  if (el.kind === "paragraph") return layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, contentWidth);
531
827
  if (el.kind === "table") return layoutTableBlock(el.table, options, fontResources, imageResources, contentWidth);
532
- if (el.kind === "image") return layoutImageBlock(el.image, imageResources, contentWidth);
828
+ if (el.kind === "image") return layoutImageBlock(el.image, imageResources, contentWidth, maxHeight, box);
533
829
  if (el.kind === "chart") return layoutChartBlock(el.chart, options, fontResources, contentWidth, maxHeight);
534
- return layoutShapeBlock(el.shape, options, fontResources, imageResources, contentWidth, maxHeight);
830
+ return layoutShapeBlock(el.shape, options, fontResources, imageResources, contentWidth, maxHeight, box);
535
831
  }
536
- function layoutImageBlock(image, imageResources, contentWidth) {
537
- let widthPt = image.width;
538
- let heightPt = image.height;
539
- if (widthPt > contentWidth) {
832
+ function layoutImageBlock(image, imageResources, contentWidth, maxHeight, box) {
833
+ let widthPt = relativeWidth(image.relativeSize, contentWidth, box) ?? image.width;
834
+ let heightPt = relativeHeight(image.relativeSize, maxHeight, box) ?? (image.relativeSize?.widthPct && image.width > 0 ? image.height * (widthPt / image.width) : image.height);
835
+ if (widthPt > contentWidth && !image.float) {
540
836
  const scale = contentWidth / widthPt;
541
837
  widthPt = contentWidth;
542
838
  heightPt = heightPt * scale;
543
839
  }
840
+ let drawInset;
841
+ const eff = image.effectExtent;
842
+ if (eff) {
843
+ const s = image.width > 0 ? widthPt / image.width : 1;
844
+ let box = {
845
+ dxPt: eff.leftPt * s,
846
+ dyTopPt: eff.topPt * s,
847
+ widthPt,
848
+ heightPt
849
+ };
850
+ let reservedW = widthPt + (eff.leftPt + eff.rightPt) * s;
851
+ let reservedH = heightPt + (eff.topPt + eff.bottomPt) * s;
852
+ if (reservedW > contentWidth) {
853
+ const k = contentWidth / reservedW;
854
+ reservedW = contentWidth;
855
+ reservedH *= k;
856
+ box = {
857
+ dxPt: box.dxPt * k,
858
+ dyTopPt: box.dyTopPt * k,
859
+ widthPt: box.widthPt * k,
860
+ heightPt: box.heightPt * k
861
+ };
862
+ }
863
+ widthPt = reservedW;
864
+ heightPt = reservedH;
865
+ drawInset = box;
866
+ }
544
867
  const res = image.resource ? imageResources?.get(image.resource) : void 0;
545
868
  const resolvedAlignment = image.paragraphProperties.alignment ?? "left";
546
869
  return {
@@ -549,27 +872,33 @@ function layoutImageBlock(image, imageResources, contentWidth) {
549
872
  heightPt,
550
873
  resolvedAlignment,
551
874
  resourceName: res?.resourceName ?? "",
875
+ ...image.outline ? { outline: image.outline } : {},
876
+ ...image.crop ? { crop: image.crop } : {},
877
+ ...image.rotation60k ? { rotationDeg: image.rotation60k / 6e4 } : {},
878
+ ...image.flipH ? { flipH: true } : {},
879
+ ...image.flipV ? { flipV: true } : {},
880
+ ...drawInset ? { drawInset } : {},
552
881
  spacingBeforePt: image.paragraphProperties.spacingBefore ?? 0,
553
882
  spacingAfterPt: image.paragraphProperties.spacingAfter ?? 0,
554
883
  ...image.altText ? { altText: image.altText } : {},
555
884
  ...image.float ? { float: image.float } : {}
556
885
  };
557
886
  }
558
- function layoutShapeBlock(shape, options, fontResources, imageResources, contentWidth, maxHeight) {
559
- let widthPt = shape.width;
560
- let heightPt = shape.height;
561
- if (widthPt > contentWidth && widthPt > 0) {
887
+ function layoutShapeBlock(shape, options, fontResources, imageResources, contentWidth, maxHeight, box, fitPass = 0) {
888
+ let widthPt = relativeWidth(shape.relativeSize, contentWidth, box) ?? shape.width;
889
+ let heightPt = relativeHeight(shape.relativeSize, maxHeight, box) ?? (shape.relativeSize?.widthPct && shape.width > 0 ? shape.height * (widthPt / shape.width) : shape.height);
890
+ if (!shape.float && widthPt > contentWidth && widthPt > 0) {
562
891
  const scale = contentWidth / widthPt;
563
892
  widthPt = contentWidth;
564
893
  heightPt *= scale;
565
894
  }
566
- if (maxHeight !== void 0 && heightPt > maxHeight && heightPt > 0) {
895
+ if (!shape.float && maxHeight !== void 0 && heightPt > maxHeight && heightPt > 0) {
567
896
  const scale = maxHeight / heightPt;
568
897
  widthPt *= scale;
569
898
  heightPt = maxHeight;
570
899
  }
571
- const paths = buildShapePaths(shape.geometry, widthPt, heightPt);
572
900
  const fillGradient = shape.fill.kind === "gradient" ? shape.fill.gradient : void 0;
901
+ const fillImageResourceName = shape.fill.kind === "picture" && shape.fill.imageResource !== void 0 ? imageResources?.get(shape.fill.imageResource)?.resourceName : void 0;
573
902
  const fillColorHex = shape.fill.kind === "solid" ? shape.fill.colorHex : fillGradient ? gradientToSolid(fillGradient) : void 0;
574
903
  const stroke = buildStroke(shape.line);
575
904
  const t = shape.transform;
@@ -580,27 +909,154 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
580
909
  const insetTopPt = text?.insetTop ?? 3.6;
581
910
  const insetBottomPt = text?.insetBottom ?? 3.6;
582
911
  const textLines = [];
912
+ const textCharts = /* @__PURE__ */ new Map();
913
+ const textShapes = /* @__PURE__ */ new Map();
914
+ const textTables = /* @__PURE__ */ new Map();
915
+ const textLineGaps = /* @__PURE__ */ new Map();
583
916
  let textHeightPt = 0;
917
+ const vertical = text?.vertical;
584
918
  if (text && text.content.length > 0) {
585
- const innerWidth = Math.max(1, widthPt - insetLeftPt - insetRightPt);
919
+ const innerWidth = vertical ? Math.max(1, heightPt - insetTopPt - insetBottomPt) : Math.max(1, widthPt - insetLeftPt - insetRightPt);
586
920
  for (const el of text.content) {
587
- if (el.kind !== "paragraph") continue;
921
+ if (el.kind === "image") {
922
+ const res = el.image.resource ? imageResources?.get(el.image.resource) : void 0;
923
+ const scaled = Math.min(1, innerWidth / Math.max(1, el.image.width));
924
+ const line = {
925
+ tokens: [{
926
+ kind: "image",
927
+ imageResourceName: res?.resourceName ?? "",
928
+ widthPt: el.image.width * scaled,
929
+ heightPt: el.image.height * scaled,
930
+ isSpace: false,
931
+ bidiLevel: 0
932
+ }],
933
+ contentWidthPt: el.image.width * scaled,
934
+ maxFontSizePt: el.image.height * scaled,
935
+ availableWidthPt: innerWidth,
936
+ firstLine: true,
937
+ resolved: resolveParagraphProperties(el.image.paragraphProperties, options.styles),
938
+ isLastInParagraph: true
939
+ };
940
+ textLines.push(line);
941
+ textHeightPt += computeLineHeight(line, line.resolved);
942
+ continue;
943
+ }
944
+ if (el.kind === "chart") {
945
+ const laid = layoutChartBlock(el.chart, options, fontResources, innerWidth);
946
+ const line = {
947
+ tokens: [],
948
+ contentWidthPt: laid.widthPt,
949
+ maxFontSizePt: laid.heightPt,
950
+ availableWidthPt: innerWidth,
951
+ firstLine: true,
952
+ resolved: resolveParagraphProperties(el.chart.paragraphProperties, options.styles),
953
+ isLastInParagraph: true
954
+ };
955
+ textCharts.set(textLines.length, laid);
956
+ textLines.push(line);
957
+ textHeightPt += computeLineHeight(line, line.resolved);
958
+ continue;
959
+ }
960
+ if (el.kind === "shape") {
961
+ const laid = layoutShapeBlock(el.shape, options, fontResources, imageResources, innerWidth);
962
+ const line = {
963
+ tokens: [],
964
+ contentWidthPt: laid.widthPt,
965
+ maxFontSizePt: laid.heightPt,
966
+ availableWidthPt: innerWidth,
967
+ firstLine: true,
968
+ resolved: resolveParagraphProperties(el.shape.paragraphProperties, options.styles),
969
+ isLastInParagraph: true
970
+ };
971
+ textShapes.set(textLines.length, laid);
972
+ textLines.push(line);
973
+ textHeightPt += computeLineHeight(line, line.resolved);
974
+ continue;
975
+ }
976
+ if (el.kind === "table") {
977
+ const laid = layoutTableBlock(el.table, options, fontResources, imageResources, innerWidth);
978
+ const line = {
979
+ tokens: [],
980
+ contentWidthPt: laid.totalWidthPt,
981
+ maxFontSizePt: laid.heightPt,
982
+ metricHeightPt: laid.heightPt,
983
+ availableWidthPt: innerWidth,
984
+ firstLine: true,
985
+ resolved: resolveParagraphProperties({}, options.styles),
986
+ isLastInParagraph: true
987
+ };
988
+ textTables.set(textLines.length, laid);
989
+ textLines.push(line);
990
+ textHeightPt += computeLineHeight(line, line.resolved);
991
+ continue;
992
+ }
588
993
  const blk = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, innerWidth);
994
+ const before = textLines.length > 0 ? blk.spacingBeforePt : 0;
995
+ if (before > 0) {
996
+ const last = textLines.length - 1;
997
+ textLineGaps.set(last, (textLineGaps.get(last) ?? 0) + before);
998
+ textHeightPt += before;
999
+ }
589
1000
  for (const line of blk.lines) {
590
1001
  textLines.push(line);
591
1002
  textHeightPt += computeLineHeight(line, blk.resolved);
592
1003
  }
1004
+ if (blk.spacingAfterPt > 0) {
1005
+ const last = textLines.length - 1;
1006
+ textLineGaps.set(last, (textLineGaps.get(last) ?? 0) + blk.spacingAfterPt);
1007
+ }
593
1008
  textHeightPt += blk.spacingAfterPt;
594
1009
  }
595
1010
  }
1011
+ if (text?.autoFit && !vertical && textLines.length > 0) heightPt = textHeightPt + insetTopPt + insetBottomPt;
1012
+ if (text?.fitToBox === true && textLines.length > 0 && fitPass < MAX_FIT_PASSES) {
1013
+ const innerW = Math.max(1, widthPt - insetLeftPt - insetRightPt);
1014
+ const innerH = Math.max(1, heightPt - insetTopPt - insetBottomPt);
1015
+ let widest = 0;
1016
+ let running = 0;
1017
+ let paragraphs = 0;
1018
+ for (const l of textLines) {
1019
+ running += l.contentWidthPt;
1020
+ if (l.isLastInParagraph) {
1021
+ widest = Math.max(widest, running);
1022
+ running = 0;
1023
+ paragraphs++;
1024
+ }
1025
+ }
1026
+ widest = Math.max(1, widest, running);
1027
+ const perLine = textHeightPt / Math.max(1, textLines.length);
1028
+ const naturalHeight = Math.max(1, perLine * Math.max(1, paragraphs || textLines.length));
1029
+ const k = Math.min(innerW / widest, innerH / naturalHeight);
1030
+ if (Number.isFinite(k) && k > .05 && k < 20 && Math.abs(k - 1) > .02) return layoutShapeBlock({
1031
+ ...shape,
1032
+ text: scaleTextBody(text, k)
1033
+ }, options, fontResources, imageResources, contentWidth, maxHeight, box, fitPass + 1);
1034
+ }
1035
+ const paths = buildShapePaths(shape.geometry, widthPt, heightPt);
1036
+ const markerPaths = lineEndPaths(paths, shape.line, stroke?.widthPt ?? 0);
1037
+ const scale = shape.width > 0 ? widthPt / shape.width : 1;
1038
+ const children = (shape.children ?? []).map((child) => ({
1039
+ laid: layoutShapeBlock({
1040
+ ...child.shape,
1041
+ width: pt(child.shape.width * scale),
1042
+ height: pt(child.shape.height * scale)
1043
+ }, options, fontResources, imageResources, Number.POSITIVE_INFINITY),
1044
+ xPt: child.xPt * scale,
1045
+ yPt: child.yPt * scale
1046
+ }));
596
1047
  return {
597
1048
  kind: "shape",
598
1049
  widthPt,
599
1050
  heightPt,
1051
+ ...children.length > 0 ? { children } : {},
600
1052
  paths,
1053
+ ...markerPaths.length > 0 ? { markerPaths } : {},
1054
+ ...fillImageResourceName ? { fillImageResourceName } : {},
1055
+ ...fillImageResourceName && shape.fill.imageCrop ? { fillImageCrop: shape.fill.imageCrop } : {},
601
1056
  ...fillColorHex ? { fillColorHex } : {},
602
1057
  ...fillGradient ? { fillGradient } : {},
603
1058
  ...stroke ? { stroke } : {},
1059
+ ...shape.shadow ? { shadow: shape.shadow } : {},
604
1060
  rotation60k: t?.rotation60k ?? 0,
605
1061
  flipH: t?.flipH ?? false,
606
1062
  flipV: t?.flipV ?? false,
@@ -608,12 +1064,17 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
608
1064
  spacingBeforePt: pp.spacingBefore ?? 0,
609
1065
  spacingAfterPt: pp.spacingAfter ?? 0,
610
1066
  textLines,
1067
+ ...textCharts.size > 0 ? { textCharts } : {},
1068
+ ...textShapes.size > 0 ? { textShapes } : {},
1069
+ ...textTables.size > 0 ? { textTables } : {},
1070
+ ...textLineGaps.size > 0 ? { textLineGaps } : {},
611
1071
  textHeightPt,
612
1072
  insetLeftPt,
613
1073
  insetRightPt,
614
1074
  insetTopPt,
615
1075
  insetBottomPt,
616
1076
  anchor: text?.anchor ?? "t",
1077
+ ...vertical ? { vertical } : {},
617
1078
  ...shape.altText ? { altText: shape.altText } : {},
618
1079
  ...shape.float ? { float: shape.float } : {}
619
1080
  };
@@ -666,6 +1127,9 @@ function buildChartLayout(chart, widthPt, heightPt, font) {
666
1127
  texts: []
667
1128
  };
668
1129
  const shapes = [];
1130
+ if (scene.background) shapes.push(rectPrim(scene.background));
1131
+ if (scene.plotBackground) shapes.push(rectPrim(scene.plotBackground));
1132
+ for (const g of scene.gridlines ?? []) shapes.push(polylinePrim(g));
669
1133
  for (const pg of scene.polygons ?? []) shapes.push(polygonPrim(pg));
670
1134
  for (const r of scene.rects) shapes.push(rectPrim(r));
671
1135
  for (const p of scene.polylines) shapes.push(polylinePrim(p));
@@ -682,7 +1146,6 @@ var CF_ICON_SIZE_PT = 9;
682
1146
  var CF_ICON_GUTTER_PT = 12;
683
1147
  var CF_ICON_EMPTY_HEX = "BFBFBF";
684
1148
  var DROPDOWN_BUTTON_PT = 11;
685
- var DROPDOWN_GUTTER_PT = 13;
686
1149
  var DROPDOWN_FILL_HEX = "F0F0F0";
687
1150
  var DROPDOWN_BORDER_HEX = "B0B0B0";
688
1151
  var DROPDOWN_ARROW_HEX = "595959";
@@ -791,14 +1254,20 @@ function circlePath(cx, cy, r) {
791
1254
  const k = .5522847498307936 * r;
792
1255
  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();
793
1256
  }
1257
+ function strokeOfRect(r) {
1258
+ if (!r.strokeHex) return {};
1259
+ const stroke = buildStroke({
1260
+ colorHex: r.strokeHex,
1261
+ width: pt(r.strokeWidthPt ?? 1),
1262
+ ...r.strokeDash ? { dash: r.strokeDash } : {}
1263
+ });
1264
+ return stroke ? { stroke } : {};
1265
+ }
794
1266
  function rectPrim(r) {
795
1267
  return {
796
1268
  paths: [rectAtPath(r.x, r.y, r.w, r.h)],
797
1269
  ...r.fillHex ? { fillColorHex: r.fillHex } : {},
798
- ...r.strokeHex ? { stroke: {
799
- colorHex: r.strokeHex,
800
- widthPt: r.strokeWidthPt ?? 1
801
- } } : {}
1270
+ ...strokeOfRect(r)
802
1271
  };
803
1272
  }
804
1273
  function polylinePrim(p) {
@@ -839,9 +1308,16 @@ function wedgePrim(w) {
839
1308
  function labelPrim(l, font) {
840
1309
  const line = makeChartLabelLine(l.text, font, l.sizePt, l.colorHex);
841
1310
  const w = line.contentWidthPt;
1311
+ const shift = l.align === "center" ? -w / 2 : l.align === "right" ? -w : 0;
1312
+ if (l.rotationDeg) return {
1313
+ line,
1314
+ x: l.x,
1315
+ y: l.y + shift,
1316
+ rotationDeg: l.rotationDeg
1317
+ };
842
1318
  return {
843
1319
  line,
844
- x: l.align === "center" ? l.x - w / 2 : l.align === "right" ? l.x - w : l.x,
1320
+ x: l.x + shift,
845
1321
  y: l.y
846
1322
  };
847
1323
  }
@@ -881,10 +1357,16 @@ function collectImageResources(body, headersFooters, options) {
881
1357
  for (const run of el.paragraph.runs) if (run.inlineImage?.resource) seen.add(run.inlineImage.resource);
882
1358
  } else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) visit(cell.content);
883
1359
  else if (el.kind === "shape") {
884
- if (el.shape.text) visit(el.shape.text.content);
1360
+ const shapeImages = (sh) => {
1361
+ if (sh.fill.imageResource) seen.add(sh.fill.imageResource);
1362
+ if (sh.text) visit(sh.text.content);
1363
+ for (const child of sh.children ?? []) shapeImages(child.shape);
1364
+ };
1365
+ shapeImages(el.shape);
885
1366
  }
886
1367
  };
887
1368
  visit(body);
1369
+ if (options.pageBackgroundFill?.imageResource !== void 0) seen.add(options.pageBackgroundFill.imageResource);
888
1370
  for (const hf of headersFooters.values()) visit(hf);
889
1371
  for (const note of options.footnotes?.values() ?? []) visit(note);
890
1372
  for (const note of options.endnotes?.values() ?? []) visit(note);
@@ -908,12 +1390,257 @@ function collectImageResources(body, headersFooters, options) {
908
1390
  }
909
1391
  return out;
910
1392
  }
911
- function drawBlocksSequentially(blocks, startX, startY, pageHeight, structId) {
1393
+ function chartPageItems(laid, x, bottomYUp, pageHeight, structId) {
1394
+ const fig = structId !== void 0 ? { structId } : {};
1395
+ const out = [];
1396
+ for (const sh of laid.layout.shapes) out.push({
1397
+ type: "shape",
1398
+ shape: {
1399
+ paths: sh.paths,
1400
+ ...sh.fillColorHex ? { fillColorHex: sh.fillColorHex } : {},
1401
+ ...sh.stroke ? { stroke: sh.stroke } : {},
1402
+ transform: flipTransform([
1403
+ 1,
1404
+ 0,
1405
+ 0,
1406
+ 1,
1407
+ x,
1408
+ bottomYUp
1409
+ ], pageHeight)
1410
+ },
1411
+ ...fig
1412
+ });
1413
+ for (const t of laid.layout.texts) out.push({
1414
+ type: "line",
1415
+ line: t.line,
1416
+ originX: pt(x + t.x),
1417
+ baselineY: pt(pageHeight - (bottomYUp + t.y)),
1418
+ ...t.rotationDeg ? { rotationDeg: t.rotationDeg } : {},
1419
+ ...fig
1420
+ });
1421
+ return out;
1422
+ }
1423
+ function emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId) {
1424
+ if (sh.textLines.length === 0) return;
1425
+ if (sh.vertical) {
1426
+ const up = sh.vertical === "vert270";
1427
+ const innerHeight = Math.max(1, sh.heightPt - sh.insetTopPt - sh.insetBottomPt);
1428
+ let across = sh.anchor === "b" ? sh.widthPt - sh.insetRightPt - sh.textHeightPt : sh.anchor === "ctr" ? (sh.widthPt - sh.textHeightPt) / 2 : sh.insetLeftPt;
1429
+ for (const line of sh.textLines) {
1430
+ const h = computeLineHeight(line, line.resolved);
1431
+ const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerHeight);
1432
+ const off = lineBaselineOffset(line, line.resolved);
1433
+ const baselineAcross = up ? across + h - off : across + off;
1434
+ sink.push({
1435
+ type: "line",
1436
+ line,
1437
+ originX: pt(up ? x + baselineAcross : x + sh.widthPt - baselineAcross),
1438
+ baselineY: pt(pageHeight - (up ? bottomYUp + sh.insetBottomPt + lineOffset : bottomYUp + sh.heightPt - sh.insetTopPt - lineOffset)),
1439
+ rotationDeg: up ? 90 : -90,
1440
+ ...figId !== void 0 ? { structId: figId } : {}
1441
+ });
1442
+ across += h;
1443
+ }
1444
+ return;
1445
+ }
1446
+ const shapeTop = bottomYUp + sh.heightPt;
1447
+ const innerWidth = Math.max(1, sh.widthPt - sh.insetLeftPt - sh.insetRightPt);
1448
+ let textY;
1449
+ if (sh.anchor === "b") textY = bottomYUp + sh.insetBottomPt + sh.textHeightPt;
1450
+ else if (sh.anchor === "ctr") textY = bottomYUp + (sh.heightPt + sh.textHeightPt) / 2;
1451
+ else textY = shapeTop - sh.insetTopPt;
1452
+ sh.textLines.forEach((line, i) => {
1453
+ textY -= computeLineHeight(line, line.resolved);
1454
+ const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerWidth);
1455
+ const nested = sh.textCharts?.get(i);
1456
+ if (nested) {
1457
+ sink.push(...chartPageItems(nested, x + sh.insetLeftPt + lineOffset, textY, pageHeight, figId));
1458
+ return;
1459
+ }
1460
+ const drawing = sh.textShapes?.get(i);
1461
+ if (drawing) {
1462
+ emitShapeItems(drawing, x + sh.insetLeftPt + lineOffset, textY, sink, pageHeight, figId);
1463
+ return;
1464
+ }
1465
+ const table = sh.textTables?.get(i);
1466
+ if (table) {
1467
+ let rowY = textY + table.heightPt;
1468
+ for (const row of table.rows) {
1469
+ emitRowChunk(sink, row, x + sh.insetLeftPt + lineOffset + table.xOffsetPt, rowY, pageHeight, table.colCount);
1470
+ rowY -= row.heightPt;
1471
+ }
1472
+ return;
1473
+ }
1474
+ sink.push({
1475
+ type: "line",
1476
+ line,
1477
+ originX: pt(x + sh.insetLeftPt + lineOffset),
1478
+ baselineY: pt(pageHeight - (textY + lineBaselineOffset(line, line.resolved))),
1479
+ ...figId !== void 0 ? { structId: figId } : {}
1480
+ });
1481
+ textY -= sh.textLineGaps?.get(i) ?? 0;
1482
+ });
1483
+ }
1484
+ function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
1485
+ if (sh.fillImageResourceName) sink.push({
1486
+ type: "image",
1487
+ x: pt(x),
1488
+ y: pt(pageHeight - (bottomYUp + sh.heightPt)),
1489
+ width: pt(sh.widthPt),
1490
+ height: pt(sh.heightPt),
1491
+ imageResourceName: sh.fillImageResourceName,
1492
+ ...sh.fillImageCrop ? { crop: sh.fillImageCrop } : {},
1493
+ clip: {
1494
+ paths: sh.paths,
1495
+ transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
1496
+ },
1497
+ ...figId !== void 0 ? { structId: figId } : {}
1498
+ });
1499
+ if (sh.markerPaths && sh.stroke) sink.push({
1500
+ type: "shape",
1501
+ shape: {
1502
+ paths: sh.markerPaths,
1503
+ fillColorHex: sh.stroke.colorHex,
1504
+ transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
1505
+ },
1506
+ ...figId !== void 0 ? { structId: figId } : {}
1507
+ });
1508
+ if (sh.paths.some((path) => path.segments.length > 0)) sink.push({
1509
+ type: "shape",
1510
+ shape: {
1511
+ paths: sh.paths,
1512
+ ...sh.fillColorHex ? { fillColorHex: sh.fillColorHex } : {},
1513
+ ...sh.fillGradient ? { fillGradient: sh.fillGradient } : {},
1514
+ ...sh.stroke ? { stroke: sh.stroke } : {},
1515
+ ...sh.shadow ? { shadow: sh.shadow } : {},
1516
+ transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
1517
+ },
1518
+ ...figId !== void 0 ? { structId: figId } : {}
1519
+ });
1520
+ for (const child of sh.children ?? []) emitShapeItems(child.laid, x + child.xPt, bottomYUp + sh.heightPt - child.yPt - child.laid.heightPt, sink, pageHeight, figId);
1521
+ emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId);
1522
+ }
1523
+ /**
1524
+ * §20.1.2.2.24 — the four rules of a picture's own frame, around the box the
1525
+ * picture is drawn in. Borders paint after images and before shapes, so the
1526
+ * frame lands on top of the picture the way Word draws it.
1527
+ *
1528
+ * @param outline The picture's frame.
1529
+ * @param x Left edge of the picture box.
1530
+ * @param topY Distance from the page TOP down to the box's top edge.
1531
+ * @param widthPt Box width.
1532
+ * @param heightPt Box height.
1533
+ * @param structId Tagged-PDF structure node the picture belongs to.
1534
+ * @returns One {@link BorderItem} per side.
1535
+ */
1536
+ function pictureOutlineItems(outline, x, topY, widthPt, heightPt, structId) {
1537
+ return [
1538
+ "top",
1539
+ "bottom",
1540
+ "left",
1541
+ "right"
1542
+ ].map((side) => ({
1543
+ type: "border",
1544
+ side,
1545
+ x: pt(x),
1546
+ y: pt(topY),
1547
+ width: pt(widthPt),
1548
+ height: pt(heightPt),
1549
+ borderSizePt: outline.widthPt,
1550
+ borderColorHex: outline.colorHex,
1551
+ ...structId !== void 0 ? { structId } : {}
1552
+ }));
1553
+ }
1554
+ function bandAnchor(float, widthPt, contentWidth, bandLeftPt = 0) {
1555
+ if (!float) return void 0;
1556
+ const align = float.posH?.align;
1557
+ if (align === "right") return contentWidth - widthPt;
1558
+ if (align === "center") return (contentWidth - widthPt) / 2;
1559
+ if (align === "left") return 0;
1560
+ const offset = float.posH?.offsetPt ?? 0;
1561
+ return float.posH?.relativeFrom === "page" ? offset - bandLeftPt : offset;
1562
+ }
1563
+ /**
1564
+ * §20.4.2.3 — how far DOWN the page a band's anchored drawing starts, in the
1565
+ * band emitter's y-UP frame. A `page`-relative anchor is measured from the
1566
+ * paper's own top edge, whatever the band's cursor has reached: n773061.docx
1567
+ * hangs its header text box 92pt down the page and, measured from the header's
1568
+ * first paragraph, it landed 60pt lower than Word and LibreOffice put it.
1569
+ *
1570
+ * @param float The anchor.
1571
+ * @param cursorY The band's cursor (y-up), where a paragraph-relative
1572
+ * anchor is measured from.
1573
+ * @param pageHeight The page height.
1574
+ * @returns The drawing's top edge, y-up.
1575
+ */
1576
+ function bandAnchorTop(float, cursorY, pageHeight, heightPt = 0, margins) {
1577
+ const align = float.posV?.align;
1578
+ if (align !== void 0) {
1579
+ const page = float.posV?.relativeFrom === "page" || !margins;
1580
+ const topYUp = page ? pageHeight : pageHeight - margins.top;
1581
+ const bottomYUp = page ? 0 : margins.bottom;
1582
+ if (align === "top") return topYUp;
1583
+ if (align === "bottom") return bottomYUp + heightPt;
1584
+ return bottomYUp + (topYUp - bottomYUp + heightPt) / 2;
1585
+ }
1586
+ const offset = float.posV?.offsetPt ?? 0;
1587
+ return float.posV?.relativeFrom === "page" ? pageHeight - offset : cursorY - offset;
1588
+ }
1589
+ function drawBlocksSequentially(blocks, startX, startY, pageHeight, contentWidth, structId, bandMargins) {
912
1590
  const out = [];
913
1591
  let cursorY = startY;
914
- for (const block of blocks) {
915
- if (block.kind !== "paragraph") continue;
1592
+ const isBehind = (b) => "float" in b && b.float?.behind === true;
1593
+ const ordered = blocks.some(isBehind) ? [...blocks.filter(isBehind), ...blocks.filter((b) => !isBehind(b))] : blocks;
1594
+ for (const block of ordered) {
1595
+ if (block.kind === "table") {
1596
+ const tableX = startX + block.xOffsetPt;
1597
+ for (const row of block.rows) {
1598
+ emitRowChunk(out, row, tableX, cursorY, pageHeight, block.colCount);
1599
+ cursorY -= row.heightPt;
1600
+ }
1601
+ continue;
1602
+ }
1603
+ if (block.kind === "chart") {
1604
+ cursorY -= block.spacingBeforePt + block.heightPt;
1605
+ const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
1606
+ out.push(...chartPageItems(block, startX + offset, cursorY, pageHeight, structId));
1607
+ cursorY -= block.spacingAfterPt;
1608
+ continue;
1609
+ }
1610
+ if (block.kind === "image") {
1611
+ cursorY -= block.spacingBeforePt;
1612
+ const anchored = bandAnchor(block.float, block.widthPt, contentWidth, startX);
1613
+ const offset = anchored ?? alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
1614
+ const top = anchored === void 0 || !block.float ? cursorY : bandAnchorTop(block.float, cursorY, pageHeight, block.heightPt, bandMargins);
1615
+ out.push({
1616
+ type: "image",
1617
+ x: pt(startX + offset),
1618
+ y: pt(pageHeight - top),
1619
+ width: pt(block.widthPt),
1620
+ height: pt(block.heightPt),
1621
+ imageResourceName: block.resourceName,
1622
+ ...block.crop ? { crop: block.crop } : {},
1623
+ ...block.rotationDeg ? { rotationDeg: block.rotationDeg } : {},
1624
+ ...block.flipH ? { flipH: true } : {},
1625
+ ...block.flipV ? { flipV: true } : {}
1626
+ });
1627
+ if (block.outline) out.push(...pictureOutlineItems(block.outline, startX + offset, pageHeight - top, block.widthPt, block.heightPt, structId));
1628
+ if (anchored === void 0) cursorY -= block.heightPt;
1629
+ cursorY -= block.spacingAfterPt;
1630
+ continue;
1631
+ }
1632
+ if (block.kind === "shape") {
1633
+ cursorY -= block.spacingBeforePt;
1634
+ const anchored = bandAnchor(block.float, block.widthPt, contentWidth, startX);
1635
+ const offset = anchored ?? alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
1636
+ const top = anchored === void 0 || !block.float ? cursorY : bandAnchorTop(block.float, cursorY, pageHeight, block.heightPt, bandMargins);
1637
+ emitShapeItems(block, startX + offset, top - block.heightPt, out, pageHeight, structId);
1638
+ if (anchored === void 0) cursorY -= block.heightPt;
1639
+ cursorY -= block.spacingAfterPt;
1640
+ continue;
1641
+ }
916
1642
  cursorY -= block.spacingBeforePt;
1643
+ const bandTopY = cursorY;
917
1644
  for (const line of block.lines) {
918
1645
  const h = computeLineHeight(line, block.resolved);
919
1646
  cursorY -= h;
@@ -923,14 +1650,60 @@ function drawBlocksSequentially(blocks, startX, startY, pageHeight, structId) {
923
1650
  type: "line",
924
1651
  line,
925
1652
  originX: pt(startX + indentLeft + offset),
926
- baselineY: pt(pageHeight - (cursorY + lineDescent(line))),
1653
+ baselineY: pt(pageHeight - (cursorY + lineBaselineOffset(line, block.resolved))),
927
1654
  ...structId !== void 0 ? { structId } : {}
928
1655
  });
929
1656
  }
1657
+ out.push(...paragraphDecoration(block.resolved, startX + block.resolved.indentLeft, startX + contentWidth - block.resolved.indentRight, bandTopY, cursorY, pageHeight));
930
1658
  cursorY -= block.spacingAfterPt;
931
1659
  }
932
1660
  return out;
933
1661
  }
1662
+ /**
1663
+ * §17.3.1.31 `w:shd` and §17.3.1.24 `w:pBdr` — the band behind a paragraph and
1664
+ * the rules around it, for a paragraph occupying `topY`…`bottomY` (both in the
1665
+ * y-up page frame) between `x0` and `x1`.
1666
+ *
1667
+ * @returns The fill and border items, in paint order.
1668
+ */
1669
+ function paragraphDecoration(resolved, x0, x1, topY, bottomY, pageHeight) {
1670
+ const out = [];
1671
+ const width = Math.max(0, x1 - x0);
1672
+ if (resolved.shading) out.push({
1673
+ type: "fill",
1674
+ x: pt(x0),
1675
+ y: pt(pageHeight - topY),
1676
+ width: pt(width),
1677
+ height: pt(Math.max(0, topY - bottomY)),
1678
+ fillColorHex: resolved.shading.colorHex
1679
+ });
1680
+ const borders = resolved.borders;
1681
+ if (!borders) return out;
1682
+ for (const side of [
1683
+ "top",
1684
+ "bottom",
1685
+ "left",
1686
+ "right"
1687
+ ]) {
1688
+ const edge = borders[side];
1689
+ if (!edge || edge.style === "none") continue;
1690
+ const gap = edge.spacePt ?? 0;
1691
+ const top = topY + gap;
1692
+ const bottom = bottomY - gap;
1693
+ out.push({
1694
+ type: "border",
1695
+ side,
1696
+ x: pt(x0),
1697
+ y: pt(pageHeight - top),
1698
+ width: pt(width),
1699
+ height: pt(Math.max(0, top - bottom)),
1700
+ borderSizePt: edge.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT,
1701
+ borderColorHex: edge.colorHex ?? "000000",
1702
+ ...edge.style !== "single" ? { borderStyle: edge.style } : {}
1703
+ });
1704
+ }
1705
+ return out;
1706
+ }
934
1707
  function runFontKeyAndParsed(options, ascii, bold, italic) {
935
1708
  if (ascii && options.embeddedFonts) {
936
1709
  const name = ascii.trim().toLowerCase();
@@ -981,6 +1754,10 @@ function collectFontResources(body, headersFooters, options) {
981
1754
  }
982
1755
  const shaped = shapeText(run.text, parsed.glyphForCodepoint, parsed.advanceWidths, parsed.ligatures, parsed.kerning, parsed.joiningForms);
983
1756
  for (const g of shaped.gids) bucket.gids.add(g);
1757
+ if (run.text.includes(" ")) for (const stop of resolveParagraphProperties(para.properties, options.styles).tabs) {
1758
+ const ch = stop.leader === void 0 ? void 0 : TAB_LEADER_CHARS.get(stop.leader);
1759
+ if (ch !== void 0) bucket.gids.add(parsed.glyphForCodepoint(ch.codePointAt(0)));
1760
+ }
984
1761
  };
985
1762
  const addMath = (node) => {
986
1763
  for (const seg of mathGlyphSegments(node)) {
@@ -998,7 +1775,7 @@ function collectFontResources(body, headersFooters, options) {
998
1775
  }
999
1776
  };
1000
1777
  const visit = (elements) => {
1001
- for (const el of elements) if (el.kind === "paragraph") for (const run of el.paragraph.runs) {
1778
+ for (const el of elements) if (el.kind === "paragraph") for (const run of displayCapsRuns(el.paragraph, options)) {
1002
1779
  if (run.math) {
1003
1780
  addMath(run.math);
1004
1781
  continue;
@@ -1020,9 +1797,22 @@ function collectFontResources(body, headersFooters, options) {
1020
1797
  }
1021
1798
  addRun(run, el.paragraph);
1022
1799
  }
1023
- else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) visit(cell.content);
1800
+ else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) {
1801
+ if (cell.properties.hashOnOverflow === true) for (const inner of cell.content) {
1802
+ if (inner.kind !== "paragraph") continue;
1803
+ for (const run of inner.paragraph.runs) addRun({
1804
+ text: "#",
1805
+ properties: run.properties
1806
+ }, inner.paragraph);
1807
+ }
1808
+ visit(cell.content);
1809
+ }
1024
1810
  else if (el.kind === "shape") {
1025
- if (el.shape.text) visit(el.shape.text.content);
1811
+ const shapeText = (sh) => {
1812
+ if (sh.text) visit(sh.text.content);
1813
+ for (const child of sh.children ?? []) shapeText(child.shape);
1814
+ };
1815
+ shapeText(el.shape);
1026
1816
  } else if (el.kind === "chart") {
1027
1817
  const chart = options.charts?.get(el.chart.chartRelId);
1028
1818
  if (chart) {
@@ -1040,21 +1830,52 @@ function collectFontResources(body, headersFooters, options) {
1040
1830
  };
1041
1831
  if (chart.title) add(chart.title);
1042
1832
  for (const c of chart.categories) add(c);
1043
- for (const sr of chart.series) if (sr.name) add(sr.name);
1833
+ chart.series.forEach((sr, i) => add(legendSeriesName(sr, i)));
1834
+ if (chart.catAxisTitle) add(chart.catAxisTitle);
1835
+ if (chart.valAxisTitle) add(chart.valAxisTitle);
1836
+ for (const sr of chart.series) for (const label of sr.pointLabels ?? []) add(label.text);
1044
1837
  add("0123456789.,-%() ");
1838
+ if (chart.numberFormat) add(chart.numberFormat);
1045
1839
  }
1046
1840
  }
1047
1841
  };
1048
1842
  visit(body);
1843
+ if ((options.sections ?? []).some((sec) => sec.properties.lineNumbering)) visit([{
1844
+ kind: "paragraph",
1845
+ paragraph: {
1846
+ properties: {},
1847
+ runs: [{
1848
+ text: "0123456789",
1849
+ properties: {}
1850
+ }]
1851
+ }
1852
+ }]);
1049
1853
  for (const hf of headersFooters.values()) visit(hf);
1050
1854
  for (const note of options.footnotes?.values() ?? []) visit(note);
1051
1855
  for (const note of options.endnotes?.values() ?? []) visit(note);
1052
- for (const comment of options.comments?.values() ?? []) visit(comment.content);
1856
+ for (const comment of options.comments?.values() ?? []) {
1857
+ visit(comment.content);
1858
+ visit(commentTailBlocks(comment, 1, {
1859
+ parentN: 1,
1860
+ depth: 1,
1861
+ done: true
1862
+ }));
1863
+ visit([{
1864
+ kind: "paragraph",
1865
+ paragraph: {
1866
+ properties: {},
1867
+ runs: [{
1868
+ text: "0123456789",
1869
+ properties: {}
1870
+ }]
1871
+ }
1872
+ }]);
1873
+ }
1053
1874
  if (used.size === 0) {
1054
1875
  const regular = options.registry.resolveByStyle(false, false);
1055
1876
  used.set(regular.variant, {
1056
1877
  parsed: regular.parsed,
1057
- gids: new Set([0])
1878
+ gids: /* @__PURE__ */ new Set()
1058
1879
  });
1059
1880
  }
1060
1881
  const out = /* @__PURE__ */ new Map();
@@ -1071,17 +1892,83 @@ function collectFontResources(body, headersFooters, options) {
1071
1892
  }
1072
1893
  return out;
1073
1894
  }
1895
+ /**
1896
+ * The one line an empty paragraph still occupies — no tokens, but as tall as
1897
+ * the paragraph mark's own font (§17.3.1.31 `w:rPr` on the `w:pPr`, which is
1898
+ * exactly the run properties Word keeps for a paragraph that holds no runs).
1899
+ */
1900
+ function emptyLine(paragraph, options, resolved, availableWidthPt, fontResources) {
1901
+ const mark = resolveRunProperties(paragraph.properties.runProperties ?? {}, paragraph.properties, options.styles);
1902
+ const fontSizePt = mark.fontSizePt;
1903
+ const profile = options.layoutProfile ?? "ream";
1904
+ const { variant } = options.registry.resolveByStyle(mark.bold, mark.italic);
1905
+ const parsed = fontResources.get(variant)?.parsed;
1906
+ const metric = profile !== "ream" && parsed ? fontLeadingPt(parsed, fontSizePt, profile) : void 0;
1907
+ return {
1908
+ tokens: [],
1909
+ contentWidthPt: 0,
1910
+ maxFontSizePt: fontSizePt,
1911
+ availableWidthPt,
1912
+ firstLine: true,
1913
+ resolved,
1914
+ isLastInParagraph: true,
1915
+ ...metric && metric.ascentPt + metric.descentPt > 0 ? {
1916
+ metricHeightPt: metric.ascentPt + metric.descentPt + metric.lineGapPt,
1917
+ metricDescentPt: metric.descentPt
1918
+ } : {}
1919
+ };
1920
+ }
1074
1921
  function layoutParagraphBlock(paragraph, options, fontResources, imageResources, contentWidth, lineWidths) {
1075
- const baseResolved = resolveParagraphProperties(paragraph.properties, options.styles);
1922
+ const cascaded = resolveParagraphProperties(paragraph.properties, options.styles);
1923
+ const baseResolved = options.gridLinePitchPt !== void 0 && cascaded.snapToGrid !== false ? {
1924
+ ...cascaded,
1925
+ gridLinePitchPt: options.gridLinePitchPt
1926
+ } : cascaded;
1927
+ if (paragraph.properties.sectionBreak === true && paragraph.runs.length === 0) return {
1928
+ kind: "paragraph",
1929
+ resolved: baseResolved,
1930
+ lines: [],
1931
+ heightPt: 0,
1932
+ spacingBeforePt: 0,
1933
+ spacingAfterPt: 0
1934
+ };
1076
1935
  const baseDir = paragraphBaseDirection(paragraph, baseResolved);
1077
- const resolved = baseDir === "rtl" && baseResolved.alignment === "left" ? {
1936
+ const swapAlign = (a) => a === "left" ? "right" : a === "right" ? "left" : a;
1937
+ const resolved = baseResolved.bidi === true ? {
1938
+ ...baseResolved,
1939
+ alignment: swapAlign(baseResolved.alignment),
1940
+ indentLeft: baseResolved.indentRight,
1941
+ indentRight: baseResolved.indentLeft
1942
+ } : baseDir === "rtl" && baseResolved.alignment === "left" ? {
1078
1943
  ...baseResolved,
1079
1944
  alignment: "right"
1080
1945
  } : baseResolved;
1081
- 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);
1946
+ const tokens = tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir);
1947
+ const firstLineWidth = paragraphMaxWidth(resolved, contentWidth, true);
1948
+ const otherWidth = paragraphMaxWidth(resolved, contentWidth, false);
1949
+ const runWrap = (part, firstWidth, widths) => wrap(part, firstWidth, otherWidth, resolved, options.hyphenator, options.layoutProfile ?? "ream", options.doNotExpandShiftReturn === true, widths);
1950
+ const parts = splitAtColumnBreaks(tokens);
1951
+ const columnBreakLines = /* @__PURE__ */ new Set();
1952
+ let wrapped;
1953
+ if (parts.length === 1) wrapped = runWrap(tokens, firstLineWidth, lineWidths);
1954
+ else {
1955
+ wrapped = [];
1956
+ for (const [i, part] of parts.entries()) {
1957
+ const lines = runWrap(part, i === 0 ? firstLineWidth : otherWidth, lineWidths?.slice(wrapped.length));
1958
+ if (i > 0 && lines.length > 0) columnBreakLines.add(wrapped.length);
1959
+ wrapped.push(...i === 0 ? lines : lines.map((l) => ({
1960
+ ...l,
1961
+ firstLine: false
1962
+ })));
1963
+ }
1964
+ }
1965
+ const lines = wrapped.length > 0 ? wrapped : [emptyLine(paragraph, options, resolved, Math.max(firstLineWidth, 0), fontResources)];
1082
1966
  let heightPt = 0;
1083
1967
  for (const line of lines) heightPt += computeLineHeight(line, resolved);
1084
1968
  const numbering = paragraph.properties.numbering;
1969
+ const breakIdx = paragraph.runs.findIndex((r) => r.pageBreak === true);
1970
+ const leadingPageBreak = breakIdx >= 0 && paragraph.runs.slice(0, breakIdx + 1).every((r) => r.text === "" && r.inlineImage === void 0);
1971
+ const trailingPageBreak = paragraph.runs.some((r, i) => r.pageBreak === true && (!leadingPageBreak || i > breakIdx));
1085
1972
  return {
1086
1973
  kind: "paragraph",
1087
1974
  resolved,
@@ -1093,11 +1980,23 @@ function layoutParagraphBlock(paragraph, options, fontResources, imageResources,
1093
1980
  numId: numbering.numId,
1094
1981
  level: numbering.ilvl
1095
1982
  } } : {},
1096
- ...paragraph.runs.some((r) => r.pageBreak) ? { pageBreakAfter: true } : {},
1983
+ ...leadingPageBreak ? { pageBreakBefore: true } : {},
1984
+ ...trailingPageBreak ? { pageBreakAfter: true } : {},
1985
+ ...columnBreakLines.size > 0 ? { columnBreakLines } : {},
1097
1986
  ...paragraph.bookmarks && paragraph.bookmarks.length > 0 ? { bookmarks: paragraph.bookmarks } : {},
1098
1987
  source: paragraph
1099
1988
  };
1100
1989
  }
1990
+ /**
1991
+ * Word and Excel both draw a super/subscript at about two thirds of the base
1992
+ * size, raised a third of it or dropped a sixth. The base line height does not
1993
+ * change: a superscript inside a line of body text must not open the leading.
1994
+ */
1995
+ var SCRIPT_SCALE = .66;
1996
+ var SCRIPT_OFFSET = {
1997
+ superscript: 1 / 3,
1998
+ subscript: -1 / 6
1999
+ };
1101
2000
  function resolveMathItems(items, fontFor) {
1102
2001
  return items.map((it) => {
1103
2002
  if (it.kind === "glyph") return {
@@ -1123,13 +2022,76 @@ function resolveMathItems(items, fontFor) {
1123
2022
  };
1124
2023
  });
1125
2024
  }
2025
+ /**
2026
+ * §17.3.2.5 `w:caps` / §17.3.2.33 `w:smallCaps` — the paragraph's runs as they
2027
+ * are DISPLAYED. A capitals run is upper-cased; a small-capitals run is too,
2028
+ * and the letters that were lower case are split out to be set smaller.
2029
+ *
2030
+ * @param paragraph The paragraph.
2031
+ * @param options The render options (for the style cascade).
2032
+ * @returns The runs to lay out — the same array when none asks for capitals.
2033
+ */
2034
+ function displayCapsRuns(paragraph, options) {
2035
+ let any = false;
2036
+ const out = [];
2037
+ for (const raw of paragraph.runs) {
2038
+ const run = raw.text.includes(SOFT_HYPHEN) ? (any = true, {
2039
+ ...raw,
2040
+ text: raw.text.replaceAll(SOFT_HYPHEN, "")
2041
+ }) : raw;
2042
+ const resolved = run.text.length > 0 ? resolveRunProperties(run.properties, paragraph.properties, options.styles) : void 0;
2043
+ if (!resolved || !resolved.caps && !resolved.smallCaps) {
2044
+ out.push(run);
2045
+ continue;
2046
+ }
2047
+ any = true;
2048
+ if (resolved.caps) {
2049
+ out.push({
2050
+ ...run,
2051
+ text: run.text.toUpperCase()
2052
+ });
2053
+ continue;
2054
+ }
2055
+ let start = 0;
2056
+ const wasLower = (i) => {
2057
+ const ch = run.text[i];
2058
+ return ch.toLowerCase() === ch && ch.toUpperCase() !== ch;
2059
+ };
2060
+ for (let i = 1; i <= run.text.length; i++) {
2061
+ if (i < run.text.length && wasLower(i) === wasLower(start)) continue;
2062
+ const segment = run.text.slice(start, i).toUpperCase();
2063
+ out.push(wasLower(start) ? {
2064
+ ...run,
2065
+ text: segment,
2066
+ properties: {
2067
+ ...run.properties,
2068
+ fontSizePt: pt(resolved.fontSizePt * .8)
2069
+ }
2070
+ } : {
2071
+ ...run,
2072
+ text: segment
2073
+ });
2074
+ start = i;
2075
+ }
2076
+ }
2077
+ return any ? out : paragraph.runs;
2078
+ }
1126
2079
  function tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir) {
1127
- const plans = paragraph.runs.map((run) => {
2080
+ const plans = displayCapsRuns(paragraph, options).map((run) => {
1128
2081
  if (run.inlineImage) {
1129
- const naturalW = run.inlineImage.width;
2082
+ const eff = run.inlineImage.effectExtent;
2083
+ const padW = (eff?.leftPt ?? 0) + (eff?.rightPt ?? 0);
2084
+ const padH = (eff?.topPt ?? 0) + (eff?.bottomPt ?? 0);
2085
+ const naturalW = run.inlineImage.width + padW;
1130
2086
  const widthPt = Math.min(naturalW, contentWidth);
1131
2087
  const scale = naturalW > 0 ? widthPt / naturalW : 1;
1132
- const heightPt = run.inlineImage.height * scale;
2088
+ const heightPt = (run.inlineImage.height + padH) * scale;
2089
+ const drawBox = eff ? {
2090
+ dxPt: eff.leftPt * scale,
2091
+ dyPt: eff.bottomPt * scale,
2092
+ widthPt: run.inlineImage.width * scale,
2093
+ heightPt: run.inlineImage.height * scale
2094
+ } : void 0;
1133
2095
  const res = run.inlineImage.resource ? imageResources?.get(run.inlineImage.resource) : void 0;
1134
2096
  const resolvedRun = resolveRunProperties(run.properties, paragraph.properties, options.styles);
1135
2097
  const { fontKey } = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
@@ -1141,7 +2103,14 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
1141
2103
  isImage: true,
1142
2104
  imageWidthPt: widthPt,
1143
2105
  imageHeightPt: heightPt,
1144
- imageResourceName: res?.resourceName ?? ""
2106
+ imageResourceName: res?.resourceName ?? "",
2107
+ ...run.inlineImage.outline ? { imageOutline: run.inlineImage.outline } : {},
2108
+ ...run.inlineImage.shadow ? { imageShadow: run.inlineImage.shadow } : {},
2109
+ ...run.inlineImage.crop ? { imageCrop: run.inlineImage.crop } : {},
2110
+ ...run.inlineImage.rotation60k ? { imageRotationDeg: run.inlineImage.rotation60k / 6e4 } : {},
2111
+ ...run.inlineImage.flipH ? { imageFlipH: true } : {},
2112
+ ...run.inlineImage.flipV ? { imageFlipV: true } : {},
2113
+ ...drawBox ? { imageDrawBox: drawBox } : {}
1145
2114
  };
1146
2115
  }
1147
2116
  if (run.math) {
@@ -1174,11 +2143,13 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
1174
2143
  }
1175
2144
  const resolvedRun = resolveRunProperties(run.properties, paragraph.properties, options.styles);
1176
2145
  const { fontKey } = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
2146
+ const script = SCRIPT_OFFSET[resolvedRun.verticalAlign] ?? 0;
1177
2147
  return {
1178
2148
  run,
1179
2149
  resolvedRun,
1180
2150
  font: lookupFont(fontResources, fontKey),
1181
- fontSizePt: resolvedRun.fontSizePt,
2151
+ fontSizePt: script === 0 ? resolvedRun.fontSizePt : resolvedRun.fontSizePt * SCRIPT_SCALE,
2152
+ ...script === 0 ? {} : { risePt: resolvedRun.fontSizePt * script },
1182
2153
  isImage: false,
1183
2154
  imageWidthPt: 0,
1184
2155
  imageHeightPt: 0,
@@ -1194,6 +2165,19 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
1194
2165
  ...plan.resolvedRun.rtl ? { rtl: true } : {}
1195
2166
  })), baseDir));
1196
2167
  }
2168
+ /**
2169
+ * Split a paragraph's tokens at every §17.3.3.1 column break, dropping the
2170
+ * break token itself (its line ends there anyway). One part ⇒ no break.
2171
+ *
2172
+ * @param tokens The paragraph's tokens in order.
2173
+ * @returns The parts, in order; always at least one.
2174
+ */
2175
+ function splitAtColumnBreaks(tokens) {
2176
+ const parts = [[]];
2177
+ for (const t of tokens) if (t.kind === "text" && t.columnBreak) parts.push([]);
2178
+ else parts[parts.length - 1].push(t);
2179
+ return parts.filter((p, i) => i === 0 || p.length > 0);
2180
+ }
1197
2181
  function tokenizePlansLtr(plans) {
1198
2182
  const tokens = [];
1199
2183
  for (const plan of plans) {
@@ -1203,6 +2187,13 @@ function tokenizePlansLtr(plans) {
1203
2187
  imageResourceName: plan.imageResourceName,
1204
2188
  widthPt: plan.imageWidthPt,
1205
2189
  heightPt: plan.imageHeightPt,
2190
+ ...plan.imageOutline ? { outline: plan.imageOutline } : {},
2191
+ ...plan.imageShadow ? { shadow: plan.imageShadow } : {},
2192
+ ...plan.imageCrop ? { crop: plan.imageCrop } : {},
2193
+ ...plan.imageRotationDeg ? { rotationDeg: plan.imageRotationDeg } : {},
2194
+ ...plan.imageFlipH ? { flipH: true } : {},
2195
+ ...plan.imageFlipV ? { flipV: true } : {},
2196
+ ...plan.imageDrawBox ? { drawBox: plan.imageDrawBox } : {},
1206
2197
  isSpace: false,
1207
2198
  bidiLevel: 0
1208
2199
  });
@@ -1225,20 +2216,37 @@ function tokenizePlansLtr(plans) {
1225
2216
  kind: "text",
1226
2217
  text: t.text,
1227
2218
  isSpace: t.isSpace,
2219
+ ...t.tab ? { tab: true } : {},
1228
2220
  ...plan.run.href !== void 0 ? { href: plan.run.href } : {},
1229
2221
  ...plan.run.footnoteRef !== void 0 ? { footnoteRef: plan.run.footnoteRef } : {},
1230
2222
  ...plan.run.anchor !== void 0 ? { anchor: plan.run.anchor } : {},
1231
2223
  ...plan.run.listMarker ? { listMarker: true } : {},
2224
+ ...plan.run.columnBreak ? { columnBreak: true } : {},
1232
2225
  ...highlight ? { highlight: true } : {},
1233
2226
  resolvedRun: plan.resolvedRun,
1234
2227
  font: plan.font,
1235
2228
  fontSizePt: plan.fontSizePt,
1236
- widthPt: plan.font.measure.textWidthPt(t.text, plan.fontSizePt),
2229
+ ...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
2230
+ widthPt: measureRunText(plan, t.text),
1237
2231
  bidiLevel: 0
1238
2232
  });
1239
2233
  }
1240
2234
  return tokens;
1241
2235
  }
2236
+ /**
2237
+ * §17.3.2.35 — a run's text measured with the space it puts between its
2238
+ * characters. PDF adds the same amount per glyph through `Tc`, so the width the
2239
+ * line is broken at and the width the page draws agree.
2240
+ *
2241
+ * @param plan The run being tokenized.
2242
+ * @param text The token's text.
2243
+ * @returns The advance width in points.
2244
+ */
2245
+ function measureRunText(plan, text) {
2246
+ const base = plan.font.measure.textWidthPt(text, plan.fontSizePt);
2247
+ const extra = plan.resolvedRun.letterSpacingPt;
2248
+ return extra === void 0 || extra === 0 ? base : base + extra * [...text].length;
2249
+ }
1242
2250
  function tokenizePlansBidi(plans, realLevels) {
1243
2251
  const tokens = [];
1244
2252
  let realIdx = 0;
@@ -1250,6 +2258,13 @@ function tokenizePlansBidi(plans, realLevels) {
1250
2258
  imageResourceName: plan.imageResourceName,
1251
2259
  widthPt: plan.imageWidthPt,
1252
2260
  heightPt: plan.imageHeightPt,
2261
+ ...plan.imageOutline ? { outline: plan.imageOutline } : {},
2262
+ ...plan.imageShadow ? { shadow: plan.imageShadow } : {},
2263
+ ...plan.imageCrop ? { crop: plan.imageCrop } : {},
2264
+ ...plan.imageRotationDeg ? { rotationDeg: plan.imageRotationDeg } : {},
2265
+ ...plan.imageFlipH ? { flipH: true } : {},
2266
+ ...plan.imageFlipV ? { flipV: true } : {},
2267
+ ...plan.imageDrawBox ? { drawBox: plan.imageDrawBox } : {},
1253
2268
  isSpace: false,
1254
2269
  bidiLevel: realLevels[realIdx] ?? 0
1255
2270
  });
@@ -1284,11 +2299,13 @@ function tokenizePlansBidi(plans, realLevels) {
1284
2299
  ...plan.run.footnoteRef !== void 0 ? { footnoteRef: plan.run.footnoteRef } : {},
1285
2300
  ...plan.run.anchor !== void 0 ? { anchor: plan.run.anchor } : {},
1286
2301
  ...plan.run.listMarker ? { listMarker: true } : {},
2302
+ ...plan.run.columnBreak ? { columnBreak: true } : {},
1287
2303
  ...(plan.run.commentRangeRefs?.length ?? 0) > 0 ? { highlight: true } : {},
1288
2304
  resolvedRun: plan.resolvedRun,
1289
2305
  font: plan.font,
1290
2306
  fontSizePt: plan.fontSizePt,
1291
- widthPt: plan.font.measure.textWidthPt(text, plan.fontSizePt),
2307
+ ...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
2308
+ widthPt: measureRunText(plan, text),
1292
2309
  bidiLevel: curLevel
1293
2310
  });
1294
2311
  };
@@ -1315,6 +2332,7 @@ function paragraphBaseDirection(paragraph, resolved) {
1315
2332
  if (resolved.bidi) return "rtl";
1316
2333
  let text = "";
1317
2334
  for (const run of paragraph.runs) {
2335
+ if (run.listMarker === true) continue;
1318
2336
  if (!run.inlineImage) text += run.text;
1319
2337
  if (text.length > 64) break;
1320
2338
  }
@@ -1327,16 +2345,29 @@ function paragraphMaxWidth(p, contentWidth, firstLine) {
1327
2345
  const firstLineExtra = firstLine ? p.indentFirstLine : 0;
1328
2346
  return Math.max(1, contentWidth - indentLeft - indentRight - firstLineExtra);
1329
2347
  }
2348
+ var HYPHENS = new Set([
2349
+ 45,
2350
+ 8208,
2351
+ 8211,
2352
+ 8212
2353
+ ]);
2354
+ /** §17.3.3.29 — the optional hyphen, printed only where a line breaks on it. */
2355
+ var SOFT_HYPHEN = "­";
1330
2356
  function tokenizeText(text) {
1331
2357
  const out = [];
1332
2358
  if (text.length === 0) return out;
1333
- const re = /(\s+)|(\S+)/g;
2359
+ const re = /(\t)|(\s+)|(\S+)/g;
1334
2360
  let m;
1335
2361
  while ((m = re.exec(text)) !== null) if (m[1] !== void 0) out.push({
1336
- text: m[1],
2362
+ text: "",
2363
+ isSpace: true,
2364
+ tab: true
2365
+ });
2366
+ else if (m[2] !== void 0) out.push({
2367
+ text: m[2],
1337
2368
  isSpace: true
1338
2369
  });
1339
- else if (m[2] !== void 0) for (const piece of splitCjkSegment(m[2])) out.push({
2370
+ else if (m[3] !== void 0) for (const piece of splitCjkSegment(m[3])) out.push({
1340
2371
  text: piece,
1341
2372
  isSpace: false
1342
2373
  });
@@ -1347,17 +2378,54 @@ function lastCodePoint(s) {
1347
2378
  const chars = [...s];
1348
2379
  return chars.length > 0 ? chars[chars.length - 1].codePointAt(0) : 0;
1349
2380
  }
1350
- function paragraphItemStream(tokens, hyphenator) {
2381
+ function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1351
2382
  const entries = [];
1352
2383
  let prevBoxEndCp = null;
2384
+ let prevWasImage = false;
1353
2385
  for (const tok of tokens) {
2386
+ const breaks = tok.isSpace ? tok.text.match(/\r\n|[\r\n]/gu)?.length ?? 0 : 0;
2387
+ if (breaks > 0) {
2388
+ for (let i = 0; i < breaks; i++) {
2389
+ entries.push({
2390
+ item: {
2391
+ type: "glue",
2392
+ width: 0,
2393
+ stretch: 1e6,
2394
+ shrink: 0
2395
+ },
2396
+ token: null
2397
+ });
2398
+ entries.push({
2399
+ item: {
2400
+ type: "penalty",
2401
+ width: 0,
2402
+ penalty: FORCED_BREAK,
2403
+ flagged: false
2404
+ },
2405
+ token: null,
2406
+ softBreak: true
2407
+ });
2408
+ }
2409
+ prevBoxEndCp = null;
2410
+ continue;
2411
+ }
1354
2412
  if (tok.isSpace || tok.kind === "image" || tok.kind === "math") {
2413
+ if (tok.kind === "image" && (prevWasImage || prevBoxEndCp !== null)) entries.push({
2414
+ item: {
2415
+ type: "penalty",
2416
+ width: 0,
2417
+ penalty: 0,
2418
+ flagged: false
2419
+ },
2420
+ token: null
2421
+ });
2422
+ prevWasImage = tok.kind === "image";
1355
2423
  entries.push({
1356
2424
  item: tok.isSpace ? {
1357
2425
  type: "glue",
1358
2426
  width: tok.widthPt,
1359
2427
  stretch: tok.widthPt * .6,
1360
- shrink: tok.widthPt * .3
2428
+ shrink: tok.widthPt * GLUE_SHRINK_RATIO
1361
2429
  } : {
1362
2430
  type: "box",
1363
2431
  width: tok.widthPt
@@ -1368,7 +2436,7 @@ function paragraphItemStream(tokens, hyphenator) {
1368
2436
  continue;
1369
2437
  }
1370
2438
  const startCp = tok.text.codePointAt(0) ?? 0;
1371
- if (prevBoxEndCp !== null && cjkBreakBetween(prevBoxEndCp, startCp)) entries.push({
2439
+ if (prevWasImage || prevBoxEndCp !== null && cjkBreakBetween(prevBoxEndCp, startCp)) entries.push({
1372
2440
  item: {
1373
2441
  type: "penalty",
1374
2442
  width: 0,
@@ -1377,9 +2445,33 @@ function paragraphItemStream(tokens, hyphenator) {
1377
2445
  },
1378
2446
  token: null
1379
2447
  });
2448
+ prevWasImage = false;
1380
2449
  prevBoxEndCp = lastCodePoint(tok.text);
1381
2450
  const positions = hyphenator ? hyphenator.hyphenate(tok.text) : [];
1382
2451
  if (positions.length === 0) {
2452
+ const tooWide = maxLineWidthPt !== void 0 && maxLineWidthPt > 0;
2453
+ const chunks = (tooWide && tok.widthPt > maxLineWidthPt ? splitAtHyphens(tok) : void 0) ?? (tooWide && tok.widthPt > maxLineWidthPt * 1.1 ? splitToWidth(tok, maxLineWidthPt) : void 0);
2454
+ if (chunks) {
2455
+ chunks.forEach((chunk, ci) => {
2456
+ if (ci > 0) entries.push({
2457
+ item: {
2458
+ type: "penalty",
2459
+ width: 0,
2460
+ penalty: 0,
2461
+ flagged: false
2462
+ },
2463
+ token: null
2464
+ });
2465
+ entries.push({
2466
+ item: {
2467
+ type: "box",
2468
+ width: chunk.widthPt
2469
+ },
2470
+ token: chunk
2471
+ });
2472
+ });
2473
+ continue;
2474
+ }
1383
2475
  entries.push({
1384
2476
  item: {
1385
2477
  type: "box",
@@ -1445,17 +2537,165 @@ function paragraphItemStream(tokens, hyphenator) {
1445
2537
  });
1446
2538
  return entries;
1447
2539
  }
1448
- function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved, profile) {
1449
- let st = start;
1450
- let et = breakIdx;
1451
- while (st < et && (entries[st].token === null || entries[st].token?.isSpace)) st++;
1452
- while (et > st && (entries[et - 1].token === null || entries[et - 1].token?.isSpace)) et--;
1453
- if (st >= et) return null;
1454
- const lineTokens = [];
1455
- for (let i = st; i < et; i++) {
1456
- const ft = entries[i].token;
2540
+ /** Word's own default when a document names none (§17.15.1.25): half an inch. */
2541
+ var DEFAULT_TAB_STOP_PT = 36;
2542
+ var TAB_LEADER_CHARS = new Map([
2543
+ ["dot", "."],
2544
+ ["hyphen", "-"],
2545
+ ["underscore", "_"],
2546
+ ["middleDot", "·"]
2547
+ ]);
2548
+ /**
2549
+ * Give each tab on a line the width that carries it to its stop, and the
2550
+ * leader that fills the way there.
2551
+ *
2552
+ * §17.3.1.37 — a tab advances to a POSITION, not by a fixed amount, and where
2553
+ * that position is depends on how far the line has already run. Measured as
2554
+ * ordinary whitespace it collapsed to a space: the page numbers of
2555
+ * FDO77715.docx's index sat against their titles with no dot leader between,
2556
+ * and its header ran its left and right halves together on one line.
2557
+ *
2558
+ * A `right`, `center` or `decimal` stop positions the text AFTER the tab, so
2559
+ * each is resolved against the segment that follows it.
2560
+ *
2561
+ * @param tokens The line's tokens, replaced in place.
2562
+ * @param resolved The paragraph's resolved properties: the stops and indents.
2563
+ * @param isFirst Whether this is the paragraph's first line.
2564
+ */
2565
+ function resolveTabs(tokens, resolved, isFirst, availableWidthPt) {
2566
+ const stops = resolved.tabs;
2567
+ let x = resolved.indentLeft + (isFirst ? resolved.indentFirstLine : 0);
2568
+ for (let i = 0; i < tokens.length; i++) {
2569
+ const tok = tokens[i];
2570
+ if (!tok) continue;
2571
+ if (!(tok.kind === "text" && tok.tab === true)) {
2572
+ x += tok.widthPt;
2573
+ continue;
2574
+ }
2575
+ let segment = 0;
2576
+ let decimalAt = 0;
2577
+ let seenDecimal = false;
2578
+ for (let j = i + 1; j < tokens.length; j++) {
2579
+ const next = tokens[j];
2580
+ if (next.kind === "text" && next.tab === true) break;
2581
+ if (!seenDecimal && next.kind === "text") {
2582
+ const dot = next.text.indexOf(".");
2583
+ if (dot >= 0) {
2584
+ decimalAt = segment + next.font.measure.textWidthPt(next.text.slice(0, dot), next.fontSizePt);
2585
+ seenDecimal = true;
2586
+ }
2587
+ }
2588
+ segment += next.widthPt;
2589
+ }
2590
+ const stopAt = (sp) => sp.relativeTo === "center" ? availableWidthPt / 2 : sp.relativeTo === "right" ? availableWidthPt : sp.positionPt;
2591
+ const stop = stops.find((sp) => stopAt(sp) > x + .01);
2592
+ const hangingStop = isFirst && resolved.indentFirstLine < 0 && resolved.indentLeft > x + .01 ? resolved.indentLeft : void 0;
2593
+ const explicitAt = stop ? stopAt(stop) : void 0;
2594
+ const position = explicitAt !== void 0 && (hangingStop === void 0 || explicitAt <= hangingStop) ? explicitAt : hangingStop ?? (Math.floor(x / DEFAULT_TAB_STOP_PT) + 1) * DEFAULT_TAB_STOP_PT;
2595
+ const alignment = stop?.alignment ?? "left";
2596
+ if (segment === 0 && alignment !== "left") {
2597
+ const after = tokens[i + 1];
2598
+ if (after?.kind === "text" && after.tab === true) {
2599
+ tokens[i] = {
2600
+ ...tok,
2601
+ widthPt: 0,
2602
+ text: ""
2603
+ };
2604
+ continue;
2605
+ }
2606
+ }
2607
+ let target = position;
2608
+ if (alignment === "right") target = position - segment;
2609
+ else if (alignment === "center") target = position - segment / 2;
2610
+ else if (alignment === "decimal") target = position - (seenDecimal ? decimalAt : segment);
2611
+ const width = Math.max(0, target - x);
2612
+ const leaderChar = stop?.leader === void 0 ? void 0 : TAB_LEADER_CHARS.get(stop.leader);
2613
+ const text = leaderChar === void 0 ? "" : fillLeader(tok, leaderChar, width);
2614
+ tokens[i] = {
2615
+ ...tok,
2616
+ widthPt: width,
2617
+ text
2618
+ };
2619
+ x += width;
2620
+ }
2621
+ }
2622
+ /** As many leader characters as fit the gap, measured in the tab's own font. */
2623
+ function fillLeader(tab, leader, widthPt) {
2624
+ const unit = tab.font.measure.textWidthPt(leader, tab.fontSizePt);
2625
+ if (!(unit > 0)) return "";
2626
+ return leader.repeat(Math.max(0, Math.floor(widthPt / unit)));
2627
+ }
2628
+ /**
2629
+ * Cut an unbreakable word into the widest pieces that fit `widthPt`, measured
2630
+ * with the token's own font. Each piece keeps the run's formatting, so the line
2631
+ * builder reassembles them exactly as it does hyphenation fragments.
2632
+ *
2633
+ * @param tok The text token.
2634
+ * @param widthPt The line width to fit each piece into.
2635
+ * @returns The pieces in order, or undefined when even one character overruns
2636
+ * (nothing can be gained by splitting then).
2637
+ */
2638
+ function splitAtHyphens(tok) {
2639
+ if (tok.kind !== "text") return void 0;
2640
+ const parts = [];
2641
+ let start = 0;
2642
+ for (let i = 0; i < tok.text.length; i++) {
2643
+ if (!HYPHENS.has(tok.text.codePointAt(i) ?? 0)) continue;
2644
+ if (i === start || i === tok.text.length - 1) continue;
2645
+ parts.push(tok.text.slice(start, i + 1));
2646
+ start = i + 1;
2647
+ }
2648
+ if (parts.length === 0) return void 0;
2649
+ parts.push(tok.text.slice(start));
2650
+ return parts.map((text) => ({
2651
+ ...tok,
2652
+ text,
2653
+ widthPt: tok.font.measure.textWidthPt(text, tok.fontSizePt)
2654
+ }));
2655
+ }
2656
+ function splitToWidth(tok, widthPt) {
2657
+ if (tok.kind !== "text") return void 0;
2658
+ const chars = [...tok.text];
2659
+ if (tok.font.measure.textWidthPt(chars.slice(0, MIN_SPLIT_CHARS).join(""), tok.fontSizePt) > widthPt) return;
2660
+ const out = [];
2661
+ let start = 0;
2662
+ while (start < chars.length) {
2663
+ let end = start + 1;
2664
+ let text = chars[start];
2665
+ let width = tok.font.measure.textWidthPt(text, tok.fontSizePt);
2666
+ if (width > widthPt && start === 0 && chars.length === 1) return void 0;
2667
+ while (end < chars.length) {
2668
+ const next = text + chars[end];
2669
+ const nextWidth = tok.font.measure.textWidthPt(next, tok.fontSizePt);
2670
+ if (nextWidth > widthPt) break;
2671
+ text = next;
2672
+ width = nextWidth;
2673
+ end++;
2674
+ }
2675
+ out.push({
2676
+ ...tok,
2677
+ text,
2678
+ widthPt: width
2679
+ });
2680
+ start = end;
2681
+ }
2682
+ return out.length > 1 ? out : void 0;
2683
+ }
2684
+ /** The fewest characters a mid-word break may leave on a line. */
2685
+ var MIN_SPLIT_CHARS = 6;
2686
+ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved, profile, allowEmpty = false) {
2687
+ let st = start;
2688
+ let et = breakIdx;
2689
+ while (st < et && (entries[st].token === null || !isFirst && entries[st].token?.isSpace === true)) st++;
2690
+ const paints = (t) => t !== null && t !== void 0 && t.kind === "text" && (t.resolvedRun.underline !== "none" || t.resolvedRun.strike);
2691
+ while (et > st && (entries[et - 1].token === null || entries[et - 1].token?.isSpace === true && !paints(entries[et - 1].token))) et--;
2692
+ if (st >= et && !allowEmpty) return null;
2693
+ const lineTokens = [];
2694
+ for (let i = st; i < et; i++) {
2695
+ const ft = entries[i].token;
1457
2696
  if (ft) lineTokens.push(ft);
1458
2697
  }
2698
+ resolveTabs(lineTokens, resolved, isFirst, availableWidthPt);
1459
2699
  const hyphenWidth = entries[breakIdx]?.hyphenWidthPt;
1460
2700
  if (hyphenWidth !== void 0) {
1461
2701
  const lastIdx = lineTokens.length - 1;
@@ -1475,8 +2715,10 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
1475
2715
  let metricAscent = 0;
1476
2716
  let metricDescent = 0;
1477
2717
  let metricLineGap = 0;
2718
+ const prints = lineTokens.some((t) => !t.isSpace);
1478
2719
  for (const t of lineTokens) {
1479
2720
  contentWidth += t.widthPt;
2721
+ if (prints && t.isSpace) continue;
1480
2722
  const sz = tokenLineSize(t);
1481
2723
  if (sz > maxSize) maxSize = sz;
1482
2724
  if (t.kind === "math") {
@@ -1506,23 +2748,51 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
1506
2748
  } : {}
1507
2749
  };
1508
2750
  }
1509
- function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile, lineWidths) {
2751
+ function withLeadingTabWidths(tokens, resolved, firstLineWidth) {
2752
+ const first = tokens[0];
2753
+ if (!(first?.kind === "text" && first.tab === true)) return tokens;
2754
+ const probe = tokens.map((t) => ({ ...t }));
2755
+ resolveTabs(probe, resolved, true, firstLineWidth);
2756
+ const out = tokens.slice();
2757
+ for (let i = 0; i < out.length; i++) {
2758
+ const tok = out[i];
2759
+ if (!(tok.kind === "text" && tok.tab === true)) break;
2760
+ out[i] = {
2761
+ ...tok,
2762
+ widthPt: probe[i].widthPt
2763
+ };
2764
+ }
2765
+ return out;
2766
+ }
2767
+ function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile, noExpandShiftReturn, lineWidths) {
1510
2768
  if (tokens.length === 0) return [];
1511
2769
  const widths = lineWidths ?? [firstLineWidth, otherWidth];
1512
- const entries = paragraphItemStream(tokens, hyphenator);
2770
+ const entries = paragraphItemStream(withLeadingTabWidths(tokens, resolved, widths[0] ?? firstLineWidth), hyphenator, Math.max(0, ...widths));
1513
2771
  const items = entries.map((e) => e.item);
1514
2772
  const breaks = profile === "ream" ? breakLines(items, widths).breaks : greedyBreakLines(items, widths);
1515
- const lines = [];
1516
- let start = 0;
1517
- let isFirst = true;
1518
- let lineIdx = 0;
1519
- for (const breakIdx of breaks) {
1520
- const width = lineIdx < widths.length ? widths[lineIdx] : widths[widths.length - 1];
1521
- const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved, profile);
1522
- if (line) lines.push(line);
1523
- start = breakIdx + 1;
1524
- isFirst = false;
1525
- lineIdx++;
2773
+ const buildLines = (at) => {
2774
+ const out = [];
2775
+ let start = 0;
2776
+ let isFirst = true;
2777
+ let lineIdx = 0;
2778
+ for (const [i, breakIdx] of at.entries()) {
2779
+ const width = lineIdx < widths.length ? widths[lineIdx] : widths[widths.length - 1];
2780
+ const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved, profile, i < at.length - 1);
2781
+ if (line) out.push(noExpandShiftReturn && entries[breakIdx]?.softBreak ? {
2782
+ ...line,
2783
+ noJustify: true
2784
+ } : line);
2785
+ start = breakIdx + 1;
2786
+ isFirst = false;
2787
+ lineIdx++;
2788
+ }
2789
+ return out;
2790
+ };
2791
+ let lines = buildLines(breaks);
2792
+ const spill = (ls) => Math.max(0, ...ls.map((l) => l.contentWidthPt - l.availableWidthPt));
2793
+ if (profile === "ream" && lines.length === 1 && spill(lines) > .01) {
2794
+ const greedy = buildLines(greedyBreakLines(items, widths));
2795
+ if (greedy.length > 1 && spill(greedy) < spill(lines) - .01) lines = greedy;
1526
2796
  }
1527
2797
  if (lines.length > 0) lines[lines.length - 1].isLastInParagraph = true;
1528
2798
  return lines;
@@ -1545,11 +2815,17 @@ function fontLeadingPt(parsed, sizePt, profile) {
1545
2815
  };
1546
2816
  }
1547
2817
  function computeLineHeight(line, p) {
2818
+ const natural = naturalLineHeight(line, p);
2819
+ const pitch = p.gridLinePitchPt;
2820
+ if (pitch !== void 0 && pitch > 0) return Math.ceil(natural / pitch - 1e-6) * pitch;
2821
+ return natural;
2822
+ }
2823
+ function naturalLineHeight(line, p) {
1548
2824
  const fontSize = line.maxFontSizePt || 12;
1549
2825
  const natural = line.metricHeightPt ?? fontSize * 1.2;
1550
2826
  const mathH = (line.mathAscentPt ?? 0) + (line.mathDescentPt ?? 0);
1551
2827
  const mathNeed = mathH > 0 ? mathH * 1.05 : 0;
1552
- if (p.spacingLineRule === "exact" && p.spacingLine > 0) return Math.max(p.spacingLine, mathNeed);
2828
+ if (p.spacingLineRule === "exact" && p.spacingLine >= 0) return Math.max(p.spacingLine, mathNeed);
1553
2829
  if (p.spacingLineRule === "atLeast" && p.spacingLine > 0) return Math.max(natural, p.spacingLine, mathNeed);
1554
2830
  const lineTwips = Math.round(p.spacingLine * 20);
1555
2831
  const multiple = lineTwips > 0 ? lineTwips / 240 : 1;
@@ -1560,6 +2836,37 @@ function lineDescent(line) {
1560
2836
  const base = line.metricDescentPt ?? fs * .2;
1561
2837
  return Math.max(base, line.mathDescentPt ?? 0);
1562
2838
  }
2839
+ /**
2840
+ * §17.3.1.33 — how far the baseline stands above the BOTTOM of the line box,
2841
+ * which is what says where the extra height of a spaced-out line goes.
2842
+ *
2843
+ * A line given a fixed height (`exact`, `atLeast`) hangs from the bottom of its
2844
+ * box: the space a taller `w:line` asks for opens ABOVE the text, which is what
2845
+ * makes "at least 24pt" push a heading down its own line.
2846
+ *
2847
+ * `auto` — Word's "Multiple", the 1.15 every modern template sets — scales the
2848
+ * line instead, and the space it gains falls BELOW: the first line of a spaced
2849
+ * paragraph starts exactly where a single-spaced one does. Read the other way
2850
+ * round, fdo80800.docx's 1.5-line cells sat 6.6pt under the plain cell beside
2851
+ * them, where Word and LibreOffice write all three on one baseline.
2852
+ *
2853
+ * §17.6.5 — the extra a document GRID adds is not the paragraph's at all, and
2854
+ * is split evenly: the line is centred on its grid row.
2855
+ *
2856
+ * @param line The laid-out line.
2857
+ * @param p The owning paragraph's resolved properties.
2858
+ * @returns The distance from the line box's bottom edge up to the baseline.
2859
+ */
2860
+ function lineBaselineOffset(line, p) {
2861
+ const descent = lineDescent(line);
2862
+ const natural = naturalLineHeight(line, p);
2863
+ const singled = naturalLineHeight(line, {
2864
+ ...p,
2865
+ spacingLineRule: "auto",
2866
+ spacingLine: pt(0)
2867
+ });
2868
+ return descent + (p.spacingLineRule === "auto" ? Math.max(0, natural - singled) : 0) + (computeLineHeight(line, p) - natural) / 2;
2869
+ }
1563
2870
  function alignmentOffset(alignment, lineWidth, available) {
1564
2871
  if (alignment === "right") return Math.max(0, available - lineWidth);
1565
2872
  if (alignment === "center") return Math.max(0, (available - lineWidth) / 2);
@@ -1574,7 +2881,7 @@ function layoutTableBlock(table, options, fontResources, imageResources, content
1574
2881
  for (let r = 0; r < table.rows.length; r++) {
1575
2882
  const rl = layoutTableRow(table.rows[r], r, table.rows.length, colCount, table.properties, columnWidthsPt, options, fontResources, imageResources, mergeRoles[r] ?? [], aboveBordersByCol);
1576
2883
  const nextAbove = [];
1577
- let ci = 0;
2884
+ let ci = table.rows[r].properties.gridBefore ?? 0;
1578
2885
  for (const cell of table.rows[r].cells) {
1579
2886
  const span = Math.max(1, cell.properties.colSpan ?? 1);
1580
2887
  for (let k = 0; k < span; k++) nextAbove[ci + k] = cell.properties.borders;
@@ -1588,17 +2895,46 @@ function layoutTableBlock(table, options, fontResources, imageResources, content
1588
2895
  ...rp.pageBreakBefore ? { breakBefore: true } : {}
1589
2896
  } : rl);
1590
2897
  }
2898
+ growRowsForMerges(rows);
1591
2899
  const heightPt = rows.reduce((s, r) => s + r.heightPt, 0);
1592
2900
  const totalWidthPt = columnWidthsPt.reduce((s, w) => s + w, 0);
2901
+ const xOffsetPt = (table.properties.indentPt ?? 0) + tableXOffset(table.properties.alignment, contentWidth, totalWidthPt);
1593
2902
  return {
1594
2903
  kind: "table",
2904
+ ...table.properties.float ? { float: table.properties.float } : {},
1595
2905
  rows,
1596
2906
  heightPt,
1597
2907
  totalWidthPt,
1598
2908
  colCount,
1599
- xOffsetPt: tableXOffset(table.properties.alignment, contentWidth, totalWidthPt)
2909
+ xOffsetPt
1600
2910
  };
1601
2911
  }
2912
+ /**
2913
+ * §17.4.85 `w:vMerge` — make every vertical merge fit. Each row is already as
2914
+ * tall as its own (unmerged) cells; a merge that needs more than its rows add
2915
+ * up to grows the LAST row of the run, which is where Word puts the slack.
2916
+ *
2917
+ * @param rows The table's laid-out rows, adjusted in place.
2918
+ */
2919
+ function growRowsForMerges(rows) {
2920
+ for (let r = 0; r < rows.length; r++) for (const cell of rows[r].cells) {
2921
+ if (cell.mergeRole !== "start") continue;
2922
+ let last = r;
2923
+ for (let k = r + 1; k < rows.length; k++) {
2924
+ const below = rows[k].cells.find((c) => c.colStart === cell.colStart);
2925
+ if (!below || below.mergeRole !== "middle" && below.mergeRole !== "end") break;
2926
+ last = k;
2927
+ if (below.mergeRole === "end") break;
2928
+ }
2929
+ let have = 0;
2930
+ for (let k = r; k <= last; k++) have += rows[k].heightPt;
2931
+ const deficit = cell.totalHeightPt - have;
2932
+ if (deficit > .01) rows[last] = {
2933
+ ...rows[last],
2934
+ heightPt: rows[last].heightPt + deficit
2935
+ };
2936
+ }
2937
+ }
1602
2938
  function tableXOffset(alignment, contentWidth, totalWidthPt) {
1603
2939
  const slack = contentWidth - totalWidthPt;
1604
2940
  if (slack <= 0 || !alignment || alignment === "left") return 0;
@@ -1607,15 +2943,18 @@ function tableXOffset(alignment, contentWidth, totalWidthPt) {
1607
2943
  function computeColumnWidths(table, options, fontResources, contentWidth) {
1608
2944
  let colCount = table.grid.length;
1609
2945
  for (const row of table.rows) {
1610
- let rowCols = 0;
2946
+ let rowCols = row.properties.gridBefore ?? 0;
1611
2947
  for (const cell of row.cells) rowCols += Math.max(1, cell.properties.colSpan ?? 1);
1612
2948
  if (rowCols > colCount) colCount = rowCols;
1613
2949
  }
1614
2950
  if (colCount === 0) return [];
1615
- if (table.properties.layout === "fixed") return gridWidthsScaled(table, contentWidth, colCount);
2951
+ const gridShort = gridColTwips(table).length < colCount;
2952
+ const declared = declaredColumnWidths(table, colCount, contentWidth, gridShort);
2953
+ if (declared) return declared;
2954
+ if (gridColTwips(table).some((w) => w > 0)) return gridWidthsScaled(table, contentWidth, colCount);
1616
2955
  const colNaturalWidths = new Array(colCount).fill(0);
1617
2956
  for (const row of table.rows) {
1618
- let colIdx = 0;
2957
+ let colIdx = row.properties.gridBefore ?? 0;
1619
2958
  for (let i = 0; i < row.cells.length; i++) {
1620
2959
  const cell = row.cells[i];
1621
2960
  const span = Math.max(1, cell.properties.colSpan ?? 1);
@@ -1648,6 +2987,81 @@ function computeColumnWidths(table, options, fontResources, contentWidth) {
1648
2987
  function gridColTwips(table) {
1649
2988
  return table.grid.map((w) => Math.round(w * 20));
1650
2989
  }
2990
+ /**
2991
+ * §17.4.53 — column widths taken from the cells' own `w:tcW` for a table laid
2992
+ * out `fixed`. Each cell shares its preferred width over the columns it spans
2993
+ * and the widest claim wins; a column nobody declares keeps its grid share.
2994
+ * Returns `undefined` (⇒ the grid decides) unless the table says `fixed` and
2995
+ * enough of it is declared to trust.
2996
+ */
2997
+ function declaredColumnWidths(table, colCount, contentWidth, gridShort = false) {
2998
+ if (table.properties.layout !== "fixed" && !gridShort) return void 0;
2999
+ const target = totalTableTarget(table, contentWidth);
3000
+ const declaredWidth = (cell) => {
3001
+ const p = cell.properties;
3002
+ if (p.widthFraction !== void 0) return p.widthFraction * target;
3003
+ if (p.width !== void 0 && p.widthType !== "auto" && p.widthType !== "nil") return p.width;
3004
+ };
3005
+ const claimed = new Array(colCount).fill(0);
3006
+ const spanning = [];
3007
+ let anyDeclared = false;
3008
+ let reach = 0;
3009
+ for (const row of table.rows) {
3010
+ if (row.cells.length < 2) continue;
3011
+ let span = row.properties.gridBefore ?? 0;
3012
+ for (const cell of row.cells) span += Math.max(1, cell.properties.colSpan ?? 1);
3013
+ if (span > reach) reach = span;
3014
+ }
3015
+ const phantomFrom = reach > 0 && reach < colCount ? reach : colCount;
3016
+ for (const row of table.rows) {
3017
+ let colIdx = row.properties.gridBefore ?? 0;
3018
+ for (const cell of row.cells) {
3019
+ const span = Math.max(1, cell.properties.colSpan ?? 1);
3020
+ const widthPt = declaredWidth(cell);
3021
+ if (widthPt !== void 0 && widthPt > 0) {
3022
+ anyDeclared = true;
3023
+ if (span === 1) {
3024
+ if (widthPt > claimed[colIdx]) claimed[colIdx] = widthPt;
3025
+ } else spanning.push({
3026
+ start: colIdx,
3027
+ span,
3028
+ widthPt
3029
+ });
3030
+ }
3031
+ colIdx += span;
3032
+ }
3033
+ }
3034
+ if (!anyDeclared) return void 0;
3035
+ for (const s of spanning) {
3036
+ const end = Math.min(s.start + s.span, phantomFrom);
3037
+ let have = 0;
3038
+ const blank = [];
3039
+ for (let i = s.start; i < end; i++) {
3040
+ have += claimed[i];
3041
+ if (claimed[i] === 0) blank.push(i);
3042
+ }
3043
+ const deficit = s.widthPt - have;
3044
+ if (deficit <= 0) continue;
3045
+ if (blank.length > 0) {
3046
+ const each = deficit / blank.length;
3047
+ for (const i of blank) claimed[i] = each;
3048
+ } else if (have > 0) for (let i = s.start; i < end; i++) claimed[i] = claimed[i] * (s.widthPt / have);
3049
+ }
3050
+ const gridTwips = gridColTwips(table);
3051
+ const rest = [];
3052
+ let claimedTotal = 0;
3053
+ for (let i = 0; i < phantomFrom; i++) if (claimed[i] > 0) claimedTotal += claimed[i];
3054
+ else rest.push(i);
3055
+ if (rest.length > 0) {
3056
+ const leftover = Math.max(0, target - claimedTotal);
3057
+ const restGrid = rest.reduce((s, i) => s + (gridTwips[i] ?? 0), 0);
3058
+ for (const i of rest) claimed[i] = restGrid > 0 ? (gridTwips[i] ?? 0) / restGrid * leftover : leftover / rest.length;
3059
+ }
3060
+ const total = claimed.reduce((s, w) => s + w, 0);
3061
+ if (total <= 0) return void 0;
3062
+ const scale = total > target ? target / total : 1;
3063
+ return claimed.map((w) => w * scale);
3064
+ }
1651
3065
  function gridWidthsScaled(table, contentWidth, colCount) {
1652
3066
  const gridTwips = gridColTwips(table);
1653
3067
  const totalGridTwips = gridTwips.reduce((s, w) => s + w, 0);
@@ -1683,28 +3097,70 @@ function measureSingleLine(paragraph, options, fontResources) {
1683
3097
  }
1684
3098
  return total;
1685
3099
  }
3100
+ /**
3101
+ * The above-neighbour a cell resolves its top edge against.
3102
+ *
3103
+ * A spanning cell covers several columns and paints ONE top across all of them,
3104
+ * so taking the first column's neighbour throws away any rule the others
3105
+ * declare. tdf100034.xlsx rules the bottom of its two header cells, and a label
3106
+ * overflowing from two columns to their left spanned over both and wiped that
3107
+ * rule off the page. Take the heaviest bottom under the span instead — it is
3108
+ * the one edge the span can draw, so it should be the strongest claim on it.
3109
+ */
3110
+ function aboveOfSpan(aboveBordersByCol, colIdx, span) {
3111
+ const first = aboveBordersByCol[colIdx];
3112
+ if (span <= 1) return first;
3113
+ let bottom = first?.bottom;
3114
+ for (let k = 1; k < span; k++) {
3115
+ const b = aboveBordersByCol[colIdx + k]?.bottom;
3116
+ if (heavierBorder(bottom, b) === b) bottom = b;
3117
+ }
3118
+ if (bottom === first?.bottom) return first;
3119
+ return {
3120
+ ...first,
3121
+ ...bottom ? { bottom } : {}
3122
+ };
3123
+ }
1686
3124
  function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidthsPt, options, fontResources, imageResources, rowMergeRoles, aboveBordersByCol) {
1687
3125
  const cells = [];
1688
3126
  const columnXOffsets = [];
1689
- let cursorX = 0;
1690
- let colIdx = 0;
3127
+ const skip = Math.min(Math.max(0, row.properties.gridBefore ?? 0), columnWidthsPt.length);
3128
+ let cursorX = columnWidthsPt.slice(0, skip).reduce((sum, w) => sum + w, 0);
3129
+ let colIdx = skip;
1691
3130
  for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {
1692
3131
  const cell = row.cells[cellIdx];
1693
3132
  const span = Math.max(1, cell.properties.colSpan ?? 1);
1694
3133
  let widthPt = 0;
1695
3134
  for (let k = 0; k < span && colIdx + k < columnWidthsPt.length; k++) widthPt += columnWidthsPt[colIdx + k];
3135
+ const paintSpan = Math.min(span, Math.max(1, cell.properties.paintColumns ?? span));
3136
+ let paintWidthPt = 0;
3137
+ for (let k = 0; k < paintSpan && colIdx + k < columnWidthsPt.length; k++) paintWidthPt += columnWidthsPt[colIdx + k];
1696
3138
  columnXOffsets.push(cursorX);
1697
3139
  const mergeRole = rowMergeRoles[cellIdx] ?? "standalone";
1698
3140
  const leftNeighbor = cellIdx > 0 ? row.cells[cellIdx - 1].properties.borders : void 0;
1699
- const cellLayout = layoutTableCell(cell, tableProps, widthPt, options, fontResources, imageResources, rowIdx, colIdx, span, rowCount, colCount, mergeRole, leftNeighbor, aboveBordersByCol[colIdx]);
1700
- cells.push(cellLayout);
3141
+ const cellLayout = layoutTableCell(cell, tableProps, widthPt, options, fontResources, imageResources, rowIdx, colIdx, span, rowCount, colCount, mergeRole, leftNeighbor, aboveOfSpan(aboveBordersByCol, colIdx, span));
3142
+ cells.push(paintWidthPt < widthPt ? {
3143
+ ...cellLayout,
3144
+ paintWidthPt
3145
+ } : cellLayout);
1701
3146
  cursorX += widthPt;
1702
3147
  colIdx += span;
1703
3148
  }
1704
3149
  let heightPt = 0;
1705
- for (const c of cells) if (c.totalHeightPt > heightPt) heightPt = c.totalHeightPt;
1706
- if (row.properties.height && row.properties.heightRule === "exact") heightPt = row.properties.height;
1707
- else if (row.properties.height && row.properties.heightRule === "atLeast") heightPt = Math.max(heightPt, row.properties.height);
3150
+ for (const c of cells) {
3151
+ if (c.mergeRole === "start") continue;
3152
+ if (c.totalHeightPt > heightPt) heightPt = c.totalHeightPt;
3153
+ }
3154
+ if (row.properties.height && row.properties.heightRule === "exact") {
3155
+ heightPt = row.properties.height;
3156
+ return {
3157
+ heightPt,
3158
+ cells: cells.map((c) => clipCellToHeight(c, heightPt)),
3159
+ columnXOffsets,
3160
+ rowIdx,
3161
+ rowCount
3162
+ };
3163
+ } else if (row.properties.height && row.properties.heightRule === "atLeast") heightPt = Math.max(heightPt, row.properties.height);
1708
3164
  return {
1709
3165
  heightPt,
1710
3166
  cells,
@@ -1713,6 +3169,59 @@ function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidth
1713
3169
  rowCount
1714
3170
  };
1715
3171
  }
3172
+ /**
3173
+ * A cell with only the lines that fit inside `heightPt`.
3174
+ *
3175
+ * Whole lines only: half a line of text reads as a rendering fault, where a
3176
+ * missing one reads as the clipping it is. A cell that already fits, or whose
3177
+ * first line does not, is returned untouched — the first line always draws, as
3178
+ * it does in Excel.
3179
+ *
3180
+ * @param cell The laid-out cell.
3181
+ * @param heightPt The row's pinned height.
3182
+ * @returns The cell, clipped.
3183
+ */
3184
+ function clipCellToHeight(cell, heightPt) {
3185
+ const room = heightPt - cell.padTopPt - cell.padBottomPt;
3186
+ if (cell.lines.length <= 1 || cell.contentHeightPt <= room) return cell;
3187
+ const kept = [];
3188
+ let used = 0;
3189
+ for (const line of cell.lines) {
3190
+ const h = computeLineHeight(line, line.resolved);
3191
+ if (kept.length > 0 && used + h > room) break;
3192
+ kept.push(line);
3193
+ used += h;
3194
+ }
3195
+ return {
3196
+ ...cell,
3197
+ lines: kept,
3198
+ contentHeightPt: used
3199
+ };
3200
+ }
3201
+ /**
3202
+ * The one run a picture BLOCK is made of, so a context that lays out paragraphs
3203
+ * (a table cell) can draw it without a block path of its own.
3204
+ *
3205
+ * @param image The image block.
3206
+ * @returns A run carrying it as an inline picture.
3207
+ */
3208
+ function imageRun(image) {
3209
+ return {
3210
+ text: "",
3211
+ properties: {},
3212
+ inlineImage: {
3213
+ ...image.resource ? { resource: image.resource } : {},
3214
+ ...image.outline ? { outline: image.outline } : {},
3215
+ width: image.width,
3216
+ height: image.height,
3217
+ ...image.crop ? { crop: image.crop } : {},
3218
+ ...image.rotation60k ? { rotation60k: image.rotation60k } : {},
3219
+ ...image.flipH ? { flipH: true } : {},
3220
+ ...image.flipV ? { flipV: true } : {},
3221
+ ...image.effectExtent ? { effectExtent: image.effectExtent } : {}
3222
+ }
3223
+ };
3224
+ }
1716
3225
  function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imageResources, rowIdx, colStart, colSpan, rowCount, colCount, mergeRole, leftNeighborBorders, aboveNeighborBorders) {
1717
3226
  const cellMar = cell.properties.margins;
1718
3227
  const tableMar = tableProps.defaultCellMargins;
@@ -1721,24 +3230,66 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
1721
3230
  const padLeftBase = cellMar?.left ?? tableMar?.left ?? DEFAULT_CELL_PADDING_TWIPS * TWIP_TO_PT;
1722
3231
  const padRightBase = cellMar?.right ?? tableMar?.right ?? DEFAULT_CELL_PADDING_TWIPS * TWIP_TO_PT;
1723
3232
  const padLeftPt = padLeftBase + (cell.properties.icon ? CF_ICON_GUTTER_PT : 0);
1724
- const padRightPt = padRightBase + (cell.properties.dropdown ? DROPDOWN_GUTTER_PT : 0);
3233
+ const padRightPt = padRightBase;
1725
3234
  const innerWidth = Math.max(1, widthPt - padLeftPt - padRightPt);
1726
3235
  const lines = [];
1727
3236
  const nestedTables = [];
3237
+ const cellShapes = [];
3238
+ const lineGaps = /* @__PURE__ */ new Map();
1728
3239
  let contentHeightPt = 0;
3240
+ let clipped = false;
3241
+ let pendingGapPt = 0;
3242
+ const openParagraph = (spacingBeforePt) => {
3243
+ const gap = pendingGapPt + spacingBeforePt;
3244
+ pendingGapPt = 0;
3245
+ if (gap <= 0) return;
3246
+ lineGaps.set(lines.length, (lineGaps.get(lines.length) ?? 0) + gap);
3247
+ contentHeightPt += gap;
3248
+ };
1729
3249
  if (mergeRole !== "middle" && mergeRole !== "end") {
1730
3250
  for (const el of cell.content) if (el.kind === "paragraph") {
1731
- const block = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, innerWidth);
1732
- for (const line of block.lines) {
3251
+ const noWrap = cell.properties.noWrap === true;
3252
+ const block = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, noWrap ? NO_WRAP_MEASURE_WIDTH : innerWidth);
3253
+ const cutAt = widthPt - padLeftPt;
3254
+ if (noWrap && (block.lines[0]?.contentWidthPt ?? 0) > cutAt) clipped = true;
3255
+ const keep = noWrap ? clipLineToWidth(block.lines, cutAt, cell.properties.hashOnOverflow === true) : block.lines;
3256
+ const markOnly = cell.properties.hideMark === true && el.paragraph.runs.every((r) => r.text === "");
3257
+ if (!markOnly) openParagraph(block.spacingBeforePt);
3258
+ for (const line of keep) {
1733
3259
  lines.push(line);
1734
- contentHeightPt += computeLineHeight(line, block.resolved);
3260
+ if (!markOnly) contentHeightPt += computeLineHeight(line, block.resolved);
1735
3261
  }
1736
- contentHeightPt += block.spacingAfterPt;
3262
+ if (!markOnly) pendingGapPt = block.spacingAfterPt;
1737
3263
  } else if (el.kind === "table") {
1738
3264
  const nested = layoutTableBlock(el.table, options, fontResources, imageResources, innerWidth);
1739
- nestedTables.push(nested);
3265
+ nestedTables.push({
3266
+ block: nested,
3267
+ afterLine: lines.length
3268
+ });
1740
3269
  contentHeightPt += nested.heightPt;
3270
+ } else if (el.kind === "image" && !isOutOfFlowFloat(el.image.float)) {
3271
+ const block = layoutParagraphBlock({
3272
+ properties: el.image.paragraphProperties,
3273
+ runs: [imageRun(el.image)]
3274
+ }, options, fontResources, imageResources, innerWidth);
3275
+ openParagraph(block.spacingBeforePt);
3276
+ for (const line of block.lines) {
3277
+ lines.push(line);
3278
+ contentHeightPt += computeLineHeight(line, block.resolved);
3279
+ }
3280
+ contentHeightPt += block.spacingAfterPt;
3281
+ pendingGapPt = block.spacingAfterPt;
3282
+ } else if (el.kind === "shape" && !isOutOfFlowFloat(el.shape.float)) {
3283
+ const block = layoutShapeBlock(el.shape, options, fontResources, imageResources, innerWidth);
3284
+ openParagraph(block.spacingBeforePt);
3285
+ cellShapes.push({
3286
+ block,
3287
+ afterLine: lines.length
3288
+ });
3289
+ contentHeightPt += block.heightPt;
3290
+ pendingGapPt = block.spacingAfterPt;
1741
3291
  }
3292
+ contentHeightPt += pendingGapPt;
1742
3293
  }
1743
3294
  const colEnd = colStart + colSpan - 1;
1744
3295
  const borders = resolveCellBorders(cell.properties.borders, tableProps.borders, rowIdx, colStart, colEnd, rowCount, colCount, mergeRole, leftNeighborBorders, aboveNeighborBorders);
@@ -1754,14 +3305,17 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
1754
3305
  ...cell.properties.dataBar ? { dataBar: cell.properties.dataBar } : {},
1755
3306
  ...cell.properties.icon ? { icon: cell.properties.icon } : {},
1756
3307
  ...cell.properties.sparkline ? { sparkline: cell.properties.sparkline } : {},
1757
- ...cell.properties.dropdown ? { dropdown: true } : {},
1758
3308
  lines,
3309
+ ...lineGaps.size > 0 ? { lineGaps } : {},
1759
3310
  ...nestedTables.length > 0 ? { nestedTables } : {},
3311
+ ...cellShapes.length > 0 ? { shapes: cellShapes } : {},
1760
3312
  contentHeightPt,
1761
3313
  totalHeightPt,
3314
+ ...cell.properties.verticalAlign ? { verticalAlign: cell.properties.verticalAlign } : {},
1762
3315
  colStart,
1763
3316
  colSpan,
1764
- mergeRole
3317
+ mergeRole,
3318
+ ...clipped ? { clipped: true } : {}
1765
3319
  };
1766
3320
  }
1767
3321
  var BORDER_STYLE_RANK = {
@@ -1769,6 +3323,9 @@ var BORDER_STYLE_RANK = {
1769
3323
  thick: 4,
1770
3324
  single: 3,
1771
3325
  dashed: 2,
3326
+ dashSmallGap: 2,
3327
+ dashDot: 2,
3328
+ dashDotDot: 2,
1772
3329
  dotted: 1,
1773
3330
  none: 0
1774
3331
  };
@@ -1874,7 +3431,7 @@ function listItemParagraphNode(builder, stack, list, wantLbl) {
1874
3431
  * Push order into `current`/`pages` IS the byte order of the emitted PDF, so the
1875
3432
  * bodies move unchanged.
1876
3433
  */
1877
- var PageAssembler = class {
3434
+ var PageAssembler = class PageAssembler {
1878
3435
  /**
1879
3436
  * @param sectionCtxs The per-section render contexts, in order; assembly
1880
3437
  * starts on the first section's first page.
@@ -1884,20 +3441,29 @@ var PageAssembler = class {
1884
3441
  * `undefined` when there are none.
1885
3442
  * @param bookmarkPositions Out-param: bookmark name → its GoTo destination.
1886
3443
  */
1887
- constructor(sectionCtxs, builder, notes, bookmarkPositions) {
3444
+ constructor(sectionCtxs, builder, notes, bookmarkPositions, backgroundImageName) {
1888
3445
  this.sectionCtxs = sectionCtxs;
1889
3446
  this.builder = builder;
1890
3447
  this.notes = notes;
1891
3448
  this.bookmarkPositions = bookmarkPositions;
3449
+ this.backgroundImageName = backgroundImageName;
1892
3450
  this.ctx = sectionCtxs[0];
3451
+ this.pageStartCtx = this.ctx;
3452
+ this.pageNumber = this.ctx.properties.pageNumberStart ?? 1;
1893
3453
  this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
1894
3454
  this.colStartY = this.cursorY;
3455
+ this.bandTopY = this.cursorY;
1895
3456
  }
1896
3457
  pages = [];
1897
3458
  ctx;
1898
3459
  secIdx = 0;
1899
3460
  pageInSection = 0;
1900
3461
  globalPageIdx = 0;
3462
+ /**
3463
+ * §17.6.12 — the number the NEXT page prints for a PAGE field. Counts up with
3464
+ * the pages and restarts wherever a section names a `w:pgNumType w:start`.
3465
+ */
3466
+ pageNumber = 1;
1901
3467
  current = [];
1902
3468
  pendingPageBreak = false;
1903
3469
  cursorY;
@@ -1911,6 +3477,12 @@ var PageAssembler = class {
1911
3477
  colStartLen = 0;
1912
3478
  /** The cursor's y at the top of the current column — see {@link colHasContent}. */
1913
3479
  colStartY;
3480
+ /**
3481
+ * Where this page's column band begins. The top margin on an ordinary page;
3482
+ * further down when a §17.6.22 `continuous` section started mid-page, whose
3483
+ * columns run beside each other from there, not from the paper's top.
3484
+ */
3485
+ bandTopY;
1914
3486
  /** The current column's left edge (`marginLeft` plus the column x-offset). */
1915
3487
  colLeft = () => this.ctx.marginLeft + (this.ctx.columns?.[this.colIdx]?.xOffsetPt ?? 0);
1916
3488
  /** The current column's width (the section content width when single-column). */
@@ -1929,20 +3501,131 @@ var PageAssembler = class {
1929
3501
  * Consumed space counts as use, whether or not any ink went with it.
1930
3502
  */
1931
3503
  colHasContent = () => this.current.length > this.colStartLen || this.cursorY < this.colStartY;
3504
+ /**
3505
+ * Whether this page has ANYTHING on it — including drawings, which is the
3506
+ * whole point: a page carrying only floats has an empty `current`, so a page
3507
+ * break asked for after it was silently dropped and the next band's drawings
3508
+ * landed on top of the first's. tdf111980_radioButtons.xlsx has no cells at
3509
+ * all and eleven controls wider than its page.
3510
+ */
3511
+ pageHasContent = () => this.current.length > 0 || this.floatsBehind.length > 0 || this.floatsFront.length > 0;
1932
3512
  /** Overflow step: next column on this page, or a fresh page after the last. */
1933
3513
  advanceColumn = () => {
1934
3514
  if (this.ctx.columns && this.colIdx + 1 < this.ctx.columns.length) {
1935
3515
  this.colIdx++;
1936
3516
  this.colStartLen = this.current.length;
1937
- this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
3517
+ this.cursorY = this.bandTopY;
1938
3518
  this.colStartY = this.cursorY;
3519
+ const isLast = this.colIdx + 1 >= this.ctx.columns.length;
3520
+ this.balanceBottomY = this.balanceHeightPt > 0 && !isLast ? this.cursorY - this.balanceHeightPt : void 0;
1939
3521
  } else this.flushPage();
1940
3522
  };
1941
3523
  /**
3524
+ * §17.6.8 — the running line count for the margin numbers. Reset by the
3525
+ * section's own `w:restart` rule.
3526
+ */
3527
+ lineNumber = 0;
3528
+ /**
3529
+ * §17.6.8 — the margin number for the body line just placed, if the section
3530
+ * asks for one at this count. Pushed at the line's own baseline, right-
3531
+ * aligned a quarter inch off the text.
3532
+ *
3533
+ * @param baselineY The line's baseline, in page (y-down) coordinates.
3534
+ * @param line The line being numbered — its font draws the digits.
3535
+ */
3536
+ emitLineNumber = (baselineY, line) => {
3537
+ const ln = this.ctx.properties.lineNumbering;
3538
+ if (!ln) return;
3539
+ this.lineNumber++;
3540
+ const n = ln.start + this.lineNumber - 1;
3541
+ if ((n - ln.start + 1) % ln.countBy !== 0) return;
3542
+ const token = line.tokens.find((t) => t.kind === "text");
3543
+ if (!token) return;
3544
+ const text = String(n);
3545
+ const sizePt = token.fontSizePt;
3546
+ const widthPt = token.font.measure.textWidthPt(text, sizePt);
3547
+ const digits = {
3548
+ kind: "text",
3549
+ text,
3550
+ isSpace: false,
3551
+ resolvedRun: token.resolvedRun,
3552
+ font: token.font,
3553
+ fontSizePt: sizePt,
3554
+ widthPt,
3555
+ bidiLevel: 0
3556
+ };
3557
+ const numbered = {
3558
+ ...line,
3559
+ tokens: [digits],
3560
+ contentWidthPt: widthPt
3561
+ };
3562
+ this.current.push({
3563
+ type: "line",
3564
+ line: numbered,
3565
+ originX: pt(this.colLeft() - (ln.distancePt ?? LINE_NUMBER_GAP_PT) - widthPt),
3566
+ baselineY: pt(baselineY)
3567
+ });
3568
+ };
3569
+ /**
3570
+ * §17.6.12 — a section that names its own first page number restarts the
3571
+ * count there; one that names none carries on from the section before.
3572
+ *
3573
+ * @param next The section being entered.
3574
+ */
3575
+ restartPageNumbers = (next) => {
3576
+ const start = next.properties.pageNumberStart;
3577
+ if (start !== void 0) this.pageNumber = start;
3578
+ if ((next.properties.lineNumbering?.restart ?? "newPage") !== "continuous") this.lineNumber = 0;
3579
+ };
3580
+ /**
3581
+ * §17.6.22 — start a `continuous` section on the page in hand: its columns
3582
+ * begin where the section does, not at the top of the paper.
3583
+ *
3584
+ * @param next The section to continue into.
3585
+ */
3586
+ startBandSection = (next, bandHeightPt) => {
3587
+ this.secIdx++;
3588
+ this.ctx = next;
3589
+ this.restartPageNumbers(next);
3590
+ this.colIdx = 0;
3591
+ this.colStartLen = this.current.length;
3592
+ this.cursorY = Math.min(this.cursorY, next.pageHeight - next.marginTop);
3593
+ this.bandTopY = this.cursorY;
3594
+ this.colStartY = this.cursorY;
3595
+ this.beginBalancedBand(bandHeightPt);
3596
+ };
3597
+ /**
3598
+ * §17.6.4 — even the columns of the band that starts at the cursor: each gets
3599
+ * the same share of `bandHeightPt`. A band too tall for the page fills column
3600
+ * after column as before; there is nothing to even out when it will spill
3601
+ * anyway.
3602
+ *
3603
+ * @param bandHeightPt The height of everything the band holds.
3604
+ */
3605
+ beginBalancedBand = (bandHeightPt) => {
3606
+ const colCount = this.ctx.columns?.length ?? 1;
3607
+ const room = this.cursorY - (this.ctx.marginBottom + this.noteReserve);
3608
+ const share = colCount > 1 ? balancedColumnHeight(bandHeightPt, colCount, room) : 0;
3609
+ this.balanceHeightPt = share;
3610
+ this.balanceBottomY = share > 0 ? this.cursorY - share : void 0;
3611
+ };
3612
+ /**
1942
3613
  * §20.4.2.3 out-of-flow drawings (wrap `'none'`): they render at their anchored
1943
3614
  * position without moving the cursor. `behindDoc` sinks below the body text,
1944
3615
  * everything else above it; both flush with the page.
1945
3616
  */
3617
+ /**
3618
+ * §20.4.2.3 — a float layer, drawn in `relativeHeight` order. A stable sort
3619
+ * by z leaves the ones that state none where the document put them:
3620
+ * dml-rectangle-relsize.docx writes its blue bar first and gives it the
3621
+ * higher z, and drawn in document order the white rectangle covered it.
3622
+ */
3623
+ /** A float layer that stamps each item it takes with the float's z-order. */
3624
+ static zSink = (target, z) => z === void 0 ? target : { push: (...items) => target.push(...items.map((i) => ({
3625
+ ...i,
3626
+ z
3627
+ }))) };
3628
+ static byZ = (items) => items.every((i) => i.z === void 0) ? [...items] : [...items].sort((a, b) => (a.z ?? 0) - (b.z ?? 0));
1946
3629
  floatsBehind = [];
1947
3630
  floatsFront = [];
1948
3631
  /**
@@ -1950,12 +3633,26 @@ var PageAssembler = class {
1950
3633
  * must flow around. Page-scoped, like the float graphics.
1951
3634
  */
1952
3635
  exclusions = [];
3636
+ /**
3637
+ * Claim the area a side-wrapping float keeps to itself: its own box grown by
3638
+ * the §20.4.2.3 `distT/B/L/R` stand-off the anchor asks for.
3639
+ */
3640
+ excludeFloat = (f, x, topYUp, w, h) => {
3641
+ const d = f.wrapDist;
3642
+ this.exclusions.push({
3643
+ x0: x - (d?.leftPt ?? 0),
3644
+ x1: x + w + (d?.rightPt ?? 0),
3645
+ topYUp: topYUp + (d?.topPt ?? 0),
3646
+ bottomYUp: topYUp - h - (d?.bottomPt ?? 0)
3647
+ });
3648
+ };
1953
3649
  /** The float's left edge on the page, resolved from its horizontal anchor. */
1954
3650
  floatX = (f, widthPt) => {
1955
3651
  const h = f.posH;
1956
3652
  if (!h) return this.colLeft();
1957
- const base = h.relativeFrom === "page" ? 0 : h.relativeFrom === "column" ? this.colLeft() : this.ctx.marginLeft;
1958
- const span = h.relativeFrom === "page" ? this.ctx.pageWidth : h.relativeFrom === "column" ? this.colWidth() : this.ctx.contentWidth;
3653
+ const textRight = this.ctx.marginLeft + this.ctx.contentWidth;
3654
+ const base = h.relativeFrom === "page" || h.relativeFrom === "leftMargin" ? 0 : h.relativeFrom === "column" ? this.colLeft() : h.relativeFrom === "rightMargin" ? textRight : this.ctx.marginLeft;
3655
+ const span = h.relativeFrom === "page" ? this.ctx.pageWidth : h.relativeFrom === "column" ? this.colWidth() : h.relativeFrom === "leftMargin" ? this.ctx.marginLeft : h.relativeFrom === "rightMargin" ? this.ctx.pageWidth - textRight : this.ctx.contentWidth;
1959
3656
  if (h.align === "center") return base + (span - widthPt) / 2;
1960
3657
  if (h.align === "right") return base + span - widthPt;
1961
3658
  return base + (h.offsetPt ?? 0);
@@ -1964,10 +3661,19 @@ var PageAssembler = class {
1964
3661
  * The drawing's TOP in the y-up cursor frame. Paragraph/line-relative offsets
1965
3662
  * hang off the anchoring paragraph's current position.
1966
3663
  */
1967
- floatTopYUp = (f) => {
3664
+ floatTopYUp = (f, heightPt = 0) => {
1968
3665
  const v = f.posV;
1969
3666
  if (!v) return this.cursorY;
1970
- if (v.relativeFrom === "page") return this.ctx.pageHeight - (v.offsetPt ?? 0);
3667
+ if (v.align !== void 0) {
3668
+ const page = v.relativeFrom === "page";
3669
+ const topYUp = page ? this.ctx.pageHeight : this.ctx.pageHeight - this.ctx.marginTop;
3670
+ const bottomYUp = page ? 0 : this.ctx.marginBottom;
3671
+ if (v.align === "top") return topYUp;
3672
+ if (v.align === "bottom") return bottomYUp + heightPt;
3673
+ return bottomYUp + (topYUp - bottomYUp + heightPt) / 2;
3674
+ }
3675
+ if (v.relativeFrom === "page" || v.relativeFrom === "topMargin") return this.ctx.pageHeight - (v.offsetPt ?? 0);
3676
+ if (v.relativeFrom === "bottomMargin") return this.ctx.marginBottom - (v.offsetPt ?? 0);
1971
3677
  if (v.relativeFrom === "margin") return this.ctx.pageHeight - this.ctx.marginTop - (v.offsetPt ?? 0);
1972
3678
  return this.cursorY - (v.offsetPt ?? 0);
1973
3679
  };
@@ -1986,7 +3692,21 @@ var PageAssembler = class {
1986
3692
  noteReserve = 0;
1987
3693
  placedNotes = /* @__PURE__ */ new Set();
1988
3694
  /** The page's usable bottom: the margin plus whatever the notes band has claimed so far. */
1989
- bottomLimit = () => this.ctx.marginBottom + this.noteReserve;
3695
+ /**
3696
+ * §17.6.4 — where the current column must stop. Normally the page's bottom
3697
+ * margin (plus any footnote reserve); inside a BALANCED band it is the line
3698
+ * the band's share of the height reaches, so the columns come out even.
3699
+ */
3700
+ bottomLimit = () => Math.max(this.ctx.marginBottom + this.noteReserve, this.balanceBottomY ?? Number.NEGATIVE_INFINITY);
3701
+ /**
3702
+ * §17.6.4 — a `continuous` section of several columns whose content fits the
3703
+ * page is BALANCED: Word evens the columns out rather than filling the first
3704
+ * to the bottom. Set while such a band is being laid out; the height each
3705
+ * column is given is `balanceHeightPt`. IndexFieldFlagF.docx sets its index
3706
+ * that way and we filled column one and left the rest empty.
3707
+ */
3708
+ balanceBottomY;
3709
+ balanceHeightPt = 0;
1990
3710
  /**
1991
3711
  * The first exclusion a line spanning `[yTop-h, yTop]` collides with in the
1992
3712
  * current column (x-overlap with the column required).
@@ -2014,15 +3734,17 @@ var PageAssembler = class {
2014
3734
  const cl = this.colLeft();
2015
3735
  const leftRoom = e.x0 - FLOAT_TEXT_GAP - cl;
2016
3736
  const rightRoom = cl + full - (e.x1 + FLOAT_TEXT_GAP);
2017
- if (rightRoom >= leftRoom) {
2018
- const w = Math.max(MIN_WRAP_WIDTH, rightRoom);
2019
- return {
2020
- width: w,
2021
- xOffset: full - w
2022
- };
2023
- }
3737
+ if (Math.max(leftRoom, rightRoom) < MIN_WRAP_WIDTH) return {
3738
+ width: full,
3739
+ xOffset: 0,
3740
+ blockedUntilYUp: e.bottomYUp
3741
+ };
3742
+ if (rightRoom >= leftRoom) return {
3743
+ width: rightRoom,
3744
+ xOffset: full - rightRoom
3745
+ };
2024
3746
  return {
2025
- width: Math.max(MIN_WRAP_WIDTH, leftRoom),
3747
+ width: leftRoom,
2026
3748
  xOffset: 0
2027
3749
  };
2028
3750
  };
@@ -2041,9 +3763,15 @@ var PageAssembler = class {
2041
3763
  let y = startY;
2042
3764
  for (let i = 0; i < 200; i++) {
2043
3765
  const g = this.lineGeometryAt(y, h0);
3766
+ if (g.blockedUntilYUp !== void 0) {
3767
+ widths.push(this.colWidth());
3768
+ y = g.blockedUntilYUp - h0;
3769
+ if (y < this.bottomLimit()) break;
3770
+ continue;
3771
+ }
2044
3772
  widths.push(g.width);
2045
3773
  if (g.width < this.colWidth()) narrowed = true;
2046
- else if (i > 0) break;
3774
+ else if (narrowed) break;
2047
3775
  y -= h0;
2048
3776
  if (y < this.bottomLimit()) break;
2049
3777
  }
@@ -2095,7 +3823,7 @@ var PageAssembler = class {
2095
3823
  const noteNode = this.builder.create("Note", this.builder.root);
2096
3824
  structId = this.builder.create("P", noteNode).id;
2097
3825
  }
2098
- out.push(...drawBlocksSequentially(note.blocks, this.ctx.marginLeft, cursor, this.ctx.pageHeight, structId));
3826
+ out.push(...drawBlocksSequentially(note.blocks, this.ctx.marginLeft, cursor, this.ctx.pageHeight, this.ctx.contentWidth, structId));
2099
3827
  cursor -= note.heightPt;
2100
3828
  }
2101
3829
  return out;
@@ -2114,28 +3842,31 @@ var PageAssembler = class {
2114
3842
  * guarantee one page for a header/footer-only document).
2115
3843
  */
2116
3844
  flushPage = (force = false) => {
2117
- if (this.current.length === 0 && this.cursorY >= this.colStartY && !force) return;
3845
+ if (!this.pageHasContent() && this.cursorY >= this.colStartY && !force) return;
2118
3846
  const band = bandForPage(this.pageInSection, this.globalPageIdx, this.ctx.titlePg, this.ctx.evenAndOddHeaders);
2119
3847
  const header = pickBand(this.ctx.headerSet, band);
2120
3848
  const footer = pickBand(this.ctx.footerSet, band);
2121
3849
  if (header.renderDynamic) this.dynBands.push({
2122
3850
  pageIdx: this.pages.length,
2123
- pageNumber: this.globalPageIdx + 1,
3851
+ pageNumber: this.pageNumber,
2124
3852
  position: "header",
2125
3853
  render: header.renderDynamic
2126
3854
  });
2127
3855
  if (footer.renderDynamic) this.dynBands.push({
2128
3856
  pageIdx: this.pages.length,
2129
- pageNumber: this.globalPageIdx + 1,
3857
+ pageNumber: this.pageNumber,
2130
3858
  position: "footer",
2131
3859
  render: footer.renderDynamic
2132
3860
  });
2133
3861
  this.pages.push({
2134
3862
  commands: [
3863
+ ...this.pageBackgroundItems(),
3864
+ ...this.pageBorderItems(),
3865
+ ...this.columnSeparatorItems(),
2135
3866
  ...header.commands,
2136
- ...this.floatsBehind,
3867
+ ...PageAssembler.byZ(this.floatsBehind),
2137
3868
  ...this.current,
2138
- ...this.floatsFront,
3869
+ ...PageAssembler.byZ(this.floatsFront),
2139
3870
  ...this.renderNotesBand(),
2140
3871
  ...footer.commands
2141
3872
  ],
@@ -2152,35 +3883,212 @@ var PageAssembler = class {
2152
3883
  this.colStartLen = 0;
2153
3884
  this.pageInSection++;
2154
3885
  this.globalPageIdx++;
3886
+ this.pageNumber++;
3887
+ if ((this.ctx.properties.lineNumbering?.restart ?? "newPage") === "newPage") this.lineNumber = 0;
2155
3888
  this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
2156
3889
  this.colStartY = this.cursorY;
3890
+ this.bandTopY = this.cursorY;
3891
+ this.balanceBottomY = void 0;
3892
+ this.balanceHeightPt = 0;
3893
+ this.pageStartCtx = this.ctx;
3894
+ };
3895
+ /**
3896
+ * §17.6.4 `w:cols w:sep` — the rule down the middle of every gutter, run the
3897
+ * height of the text area. multi-column-separator-with-line.docx asks for one
3898
+ * and we drew its two columns with nothing between them.
3899
+ *
3900
+ * @returns One border item per gutter, or none when the section asks for no rule.
3901
+ */
3902
+ columnSeparatorItems = () => {
3903
+ const ctx = this.pageStartCtx;
3904
+ const cols = ctx.columns;
3905
+ if (!cols || cols.length < 2 || ctx.properties.columns?.separator !== true) return [];
3906
+ let deepest = ctx.marginTop;
3907
+ for (const item of this.current) {
3908
+ const bottom = item.type === "line" ? item.baselineY : item.type === "shape" ? 0 : item.y + item.height;
3909
+ if (bottom > deepest) deepest = bottom;
3910
+ }
3911
+ const height = Math.min(deepest, ctx.pageHeight - ctx.marginBottom) - ctx.marginTop;
3912
+ if (height <= 0) return [];
3913
+ const out = [];
3914
+ for (let i = 1; i < cols.length; i++) {
3915
+ const left = cols[i - 1];
3916
+ const right = cols[i];
3917
+ const x = ctx.marginLeft + (left.xOffsetPt + left.widthPt + right.xOffsetPt) / 2;
3918
+ out.push({
3919
+ type: "border",
3920
+ side: "left",
3921
+ x: pt(x),
3922
+ y: pt(ctx.marginTop),
3923
+ width: pt(0),
3924
+ height: pt(height),
3925
+ borderSizePt: COLUMN_SEPARATOR_PT,
3926
+ borderColorHex: "000000"
3927
+ });
3928
+ }
3929
+ return out;
3930
+ };
3931
+ /** The section context the CURRENT page began in (see {@link columnSeparatorItems}). */
3932
+ pageStartCtx;
3933
+ /**
3934
+ * §17.6.10 `w:pgBorders` — the rules around the page. `@w:offsetFrom` says
3935
+ * what each edge's space is measured from: the paper's own edge, or the text
3936
+ * margin the rule stands outside of. a4andborders.docx frames every page and
3937
+ * we drew nothing.
3938
+ *
3939
+ * @returns The border items for this page, in paint order.
3940
+ */
3941
+ /**
3942
+ * §17.2.1 — what the page is painted before anything is drawn on it: the
3943
+ * `v:background`'s PICTURE or GRADIENT when it has one, else the flat
3944
+ * `w:background @w:color`. Read as the colour alone, fill.docx's five-stop
3945
+ * sweep came out the navy of its fallback and tdf126533_pageBitmap.docx's
3946
+ * paper came out plain.
3947
+ *
3948
+ * @returns The background items for this page (at most one).
3949
+ */
3950
+ pageBackgroundItems = () => {
3951
+ const options = this.ctx.options;
3952
+ const w = this.ctx.pageWidth;
3953
+ const h = this.ctx.pageHeight;
3954
+ const fill = options?.pageBackgroundFill;
3955
+ if (fill?.kind === "picture" && this.backgroundImageName !== void 0) return [{
3956
+ type: "image",
3957
+ x: pt(0),
3958
+ y: pt(0),
3959
+ width: pt(w),
3960
+ height: pt(h),
3961
+ imageResourceName: this.backgroundImageName
3962
+ }];
3963
+ if (fill?.kind === "gradient" && fill.gradient) return [{
3964
+ type: "shape",
3965
+ shape: {
3966
+ paths: [rectAtPath(0, 0, w, h)],
3967
+ fillColorHex: gradientToSolid(fill.gradient),
3968
+ fillGradient: fill.gradient,
3969
+ transform: flipTransform([
3970
+ 1,
3971
+ 0,
3972
+ 0,
3973
+ 1,
3974
+ 0,
3975
+ 0
3976
+ ], h)
3977
+ }
3978
+ }];
3979
+ if (options?.pageBackgroundColorHex !== void 0) return [{
3980
+ type: "fill",
3981
+ x: pt(0),
3982
+ y: pt(0),
3983
+ width: pt(w),
3984
+ height: pt(h),
3985
+ fillColorHex: options.pageBackgroundColorHex
3986
+ }];
3987
+ return [];
3988
+ };
3989
+ pageBorderItems = () => {
3990
+ const pg = this.ctx.properties.pageBorders;
3991
+ if (!pg) return [];
3992
+ const H = this.ctx.pageHeight;
3993
+ const W = this.ctx.pageWidth;
3994
+ const out = [];
3995
+ for (const side of [
3996
+ "top",
3997
+ "bottom",
3998
+ "left",
3999
+ "right"
4000
+ ]) {
4001
+ const edge = pg.borders[side];
4002
+ if (!edge || edge.style === "none") continue;
4003
+ const gap = edge.spacePt ?? 0;
4004
+ const x0 = pg.offsetFrom === "page" ? gap : this.ctx.marginLeft - gap;
4005
+ const x1 = pg.offsetFrom === "page" ? W - gap : W - this.ctx.marginLeft + gap;
4006
+ const yTop = pg.offsetFrom === "page" ? gap : this.ctx.marginTop - gap;
4007
+ const yBottom = pg.offsetFrom === "page" ? H - gap : H - this.ctx.marginBottom + gap;
4008
+ out.push({
4009
+ type: "border",
4010
+ side,
4011
+ x: pt(Math.max(0, x0)),
4012
+ y: pt(Math.max(0, yTop)),
4013
+ width: pt(Math.max(0, x1 - x0)),
4014
+ height: pt(Math.max(0, yBottom - yTop)),
4015
+ borderSizePt: edge.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT,
4016
+ borderColorHex: edge.colorHex ?? "000000",
4017
+ ...edge.style !== "single" ? { borderStyle: edge.style } : {}
4018
+ });
4019
+ }
4020
+ return out;
4021
+ };
4022
+ /**
4023
+ * Throw the in-progress page away and reset for the next one.
4024
+ *
4025
+ * A section boundary reached with a page that draws nothing is the one place
4026
+ * this is wanted: {@link flushPage} keeps such a page whenever the cursor
4027
+ * moved at all, which is right in the middle of a flow and wrong at a
4028
+ * boundary, where the space consumed was the section break itself.
4029
+ */
4030
+ dropPage = () => {
4031
+ this.current = [];
4032
+ this.floatsBehind = [];
4033
+ this.floatsFront = [];
4034
+ this.exclusions = [];
4035
+ this.pageNotes = [];
4036
+ this.noteReserve = 0;
4037
+ this.colIdx = 0;
4038
+ this.colStartLen = 0;
4039
+ this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
4040
+ this.colStartY = this.cursorY;
4041
+ this.bandTopY = this.cursorY;
4042
+ this.balanceBottomY = void 0;
4043
+ this.balanceHeightPt = 0;
2157
4044
  };
2158
4045
  };
2159
- function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph) {
4046
+ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph, backgroundImageName) {
2160
4047
  if (sectionCtxs.length === 0) return [];
2161
- const asm = new PageAssembler(sectionCtxs, builder, notes, bookmarkPositions);
4048
+ const asm = new PageAssembler(sectionCtxs, builder, notes, bookmarkPositions, backgroundImageName);
4049
+ const balanceIfEndsContinuous = (fromBlock) => {
4050
+ const here = asm.ctx;
4051
+ const next = sectionCtxs[asm.secIdx + 1];
4052
+ if ((here.columns?.length ?? 1) < 2) return;
4053
+ if (!next?.continuous) return;
4054
+ if (next.pageWidth !== here.pageWidth || next.pageHeight !== here.pageHeight) return;
4055
+ const end = Math.min(here.endIndex, blocks.length);
4056
+ asm.beginBalancedBand(blocksHeight(blocks.slice(fromBlock, end)));
4057
+ };
4058
+ balanceIfEndsContinuous(0);
2162
4059
  for (let blockIdx = 0; blockIdx < blocks.length; blockIdx++) {
2163
4060
  while (asm.secIdx < sectionCtxs.length - 1 && blockIdx >= asm.ctx.endIndex) {
2164
- asm.flushPage();
4061
+ const next = sectionCtxs[asm.secIdx + 1];
4062
+ if (next.continuous && next.pageWidth === asm.ctx.pageWidth && next.pageHeight === asm.ctx.pageHeight) {
4063
+ const end = Math.min(next.endIndex, blocks.length);
4064
+ asm.startBandSection(next, blocksHeight(blocks.slice(blockIdx, end)));
4065
+ continue;
4066
+ }
4067
+ if (asm.pageHasContent() || asm.pageInSection === 0) asm.flushPage(true);
4068
+ else asm.dropPage();
2165
4069
  asm.secIdx++;
2166
4070
  asm.ctx = sectionCtxs[asm.secIdx];
4071
+ asm.restartPageNumbers(asm.ctx);
2167
4072
  asm.pageInSection = 0;
2168
4073
  asm.cursorY = asm.ctx.pageHeight - asm.ctx.marginTop;
4074
+ balanceIfEndsContinuous(blockIdx);
2169
4075
  }
2170
4076
  const block = blocks[blockIdx];
2171
4077
  if (asm.pendingPageBreak) {
2172
4078
  asm.pendingPageBreak = false;
2173
- if (asm.current.length > 0) asm.flushPage();
4079
+ asm.flushPage(true);
2174
4080
  }
2175
4081
  if (builder && !(block.kind === "paragraph" && block.list)) asm.listStack.length = 0;
2176
4082
  if (block.kind === "paragraph") {
2177
- if (block.resolved.pageBreakBefore && asm.current.length > 0) asm.flushPage();
4083
+ if ((block.resolved.pageBreakBefore || block.pageBreakBefore) && asm.pageHasContent()) asm.flushPage();
2178
4084
  asm.cursorY -= block.spacingBeforePt;
2179
4085
  let pb = block;
2180
4086
  if (reflowParagraph && pb.source && asm.exclusions.length > 0) {
2181
4087
  const widths = asm.lineWidthsFor(block, asm.cursorY);
2182
4088
  if (widths) pb = reflowParagraph(pb.source, asm.colWidth(), widths);
2183
4089
  }
4090
+ const borderTopY = asm.cursorY;
4091
+ const borderPage = asm.pages.length;
2184
4092
  let structId;
2185
4093
  let markerLblId;
2186
4094
  const firstLine = pb.lines[0];
@@ -2195,7 +4103,8 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2195
4103
  if (lang && lang !== defaultLang) builder.node(structId).lang = lang;
2196
4104
  }
2197
4105
  let firstLineOfBlock = true;
2198
- for (const line of pb.lines) {
4106
+ for (const [lineIdx, line] of pb.lines.entries()) {
4107
+ if (pb.columnBreakLines?.has(lineIdx) && asm.colHasContent()) asm.advanceColumn();
2199
4108
  const h = computeLineHeight(line, pb.resolved);
2200
4109
  let newNotes = asm.lineFootnotes(line);
2201
4110
  const addedReserve = (sub) => sub.reduce((sum, x) => sum + x.heightPt, 0) + (asm.pageNotes.length === 0 && sub.length > 0 ? FOOTNOTE_SEPARATOR_HEIGHT : 0);
@@ -2223,10 +4132,15 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2223
4132
  });
2224
4133
  }
2225
4134
  }
4135
+ for (let guard = 0; guard < 8 && asm.exclusions.length > 0; guard++) {
4136
+ const blocked = asm.lineGeometryAt(asm.cursorY, h).blockedUntilYUp;
4137
+ if (blocked === void 0 || blocked >= asm.cursorY) break;
4138
+ asm.cursorY = blocked;
4139
+ }
2226
4140
  asm.cursorY -= h;
2227
4141
  const indentLeft = pb.resolved.indentLeft + (line.firstLine ? pb.resolved.indentFirstLine : 0);
2228
4142
  const offset = alignmentOffset(pb.resolved.alignment, line.contentWidthPt, line.availableWidthPt);
2229
- const baselineY = pt(asm.ctx.pageHeight - (asm.cursorY + lineDescent(line)));
4143
+ const baselineY = pt(asm.ctx.pageHeight - (asm.cursorY + lineBaselineOffset(line, pb.resolved)));
2230
4144
  const exclusionShift = asm.exclusions.length > 0 ? asm.lineGeometryAt(asm.cursorY + h, h).xOffset : 0;
2231
4145
  const originX = asm.colLeft() + indentLeft + offset + exclusionShift;
2232
4146
  if (markerLblId !== void 0 && line === firstLine) {
@@ -2266,86 +4180,53 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2266
4180
  baselineY,
2267
4181
  ...structId !== void 0 ? { structId } : {}
2268
4182
  });
4183
+ asm.emitLineNumber(baselineY, line);
2269
4184
  }
4185
+ 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));
2270
4186
  asm.cursorY -= pb.spacingAfterPt;
2271
4187
  if (pb.pageBreakAfter) asm.pendingPageBreak = true;
2272
4188
  } else if (block.kind === "image") {
2273
4189
  const figId = builder ? createFigure(builder, block.altText, "Image") : void 0;
2274
4190
  const emitImageAt = (x, topYUp, sink) => {
4191
+ const ins = block.drawInset;
2275
4192
  sink.push({
2276
4193
  type: "image",
2277
- x: pt(x),
2278
- y: pt(asm.ctx.pageHeight - topYUp),
2279
- width: pt(block.widthPt),
2280
- height: pt(block.heightPt),
4194
+ x: pt(x + (ins?.dxPt ?? 0)),
4195
+ y: pt(asm.ctx.pageHeight - (topYUp - (ins?.dyTopPt ?? 0))),
4196
+ width: pt(ins?.widthPt ?? block.widthPt),
4197
+ height: pt(ins?.heightPt ?? block.heightPt),
2281
4198
  imageResourceName: block.resourceName,
4199
+ ...block.crop ? { crop: block.crop } : {},
4200
+ ...block.rotationDeg ? { rotationDeg: block.rotationDeg } : {},
4201
+ ...block.flipH ? { flipH: true } : {},
4202
+ ...block.flipV ? { flipV: true } : {},
4203
+ ...block.flipH ? { flipH: true } : {},
4204
+ ...block.flipV ? { flipV: true } : {},
2282
4205
  ...figId !== void 0 ? { structId: figId } : {}
2283
4206
  });
4207
+ 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));
2284
4208
  };
2285
4209
  if (isOutOfFlowFloat(block.float)) {
2286
4210
  const fx = asm.floatX(block.float, block.widthPt);
2287
- const fy = asm.floatTopYUp(block.float);
2288
- emitImageAt(fx, fy, block.float.behind ? asm.floatsBehind : asm.floatsFront);
2289
- if (block.float.wrap !== "none") asm.exclusions.push({
2290
- x0: fx,
2291
- x1: fx + block.widthPt,
2292
- topYUp: fy,
2293
- bottomYUp: fy - block.heightPt
2294
- });
4211
+ const fy = asm.floatTopYUp(block.float, block.heightPt);
4212
+ emitImageAt(fx, fy, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
4213
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
2295
4214
  } else {
2296
4215
  asm.cursorY -= block.spacingBeforePt;
2297
4216
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
2298
4217
  asm.cursorY -= block.heightPt;
2299
- emitImageAt(asm.colLeft(), asm.cursorY + block.heightPt, asm.current);
4218
+ const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, asm.colWidth());
4219
+ emitImageAt(asm.colLeft() + offset, asm.cursorY + block.heightPt, asm.current);
2300
4220
  asm.cursorY -= block.spacingAfterPt;
2301
4221
  }
2302
4222
  } else if (block.kind === "shape") {
2303
4223
  const figId = builder ? createFigure(builder, block.altText, "Shape") : void 0;
2304
- const emitShapeAt = (x, bottomYUp, sink) => {
2305
- const transform = flipTransform(buildShapeTransform(x, bottomYUp, block.widthPt, block.heightPt, block.rotation60k, block.flipH, block.flipV), asm.ctx.pageHeight);
2306
- sink.push({
2307
- type: "shape",
2308
- shape: {
2309
- paths: block.paths,
2310
- ...block.fillColorHex ? { fillColorHex: block.fillColorHex } : {},
2311
- ...block.fillGradient ? { fillGradient: block.fillGradient } : {},
2312
- ...block.stroke ? { stroke: block.stroke } : {},
2313
- transform
2314
- },
2315
- ...figId !== void 0 ? { structId: figId } : {}
2316
- });
2317
- if (block.textLines.length > 0) {
2318
- const shapeBottom = bottomYUp;
2319
- const shapeTop = bottomYUp + block.heightPt;
2320
- const innerWidth = Math.max(1, block.widthPt - block.insetLeftPt - block.insetRightPt);
2321
- let textY;
2322
- if (block.anchor === "b") textY = shapeBottom + block.insetBottomPt + block.textHeightPt;
2323
- else if (block.anchor === "ctr") textY = shapeBottom + (block.heightPt + block.textHeightPt) / 2;
2324
- else textY = shapeTop - block.insetTopPt;
2325
- for (const line of block.textLines) {
2326
- const h = computeLineHeight(line, line.resolved);
2327
- textY -= h;
2328
- const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerWidth);
2329
- sink.push({
2330
- type: "line",
2331
- line,
2332
- originX: pt(x + block.insetLeftPt + lineOffset),
2333
- baselineY: pt(asm.ctx.pageHeight - (textY + lineDescent(line))),
2334
- ...figId !== void 0 ? { structId: figId } : {}
2335
- });
2336
- }
2337
- }
2338
- };
4224
+ const emitShapeAt = (x, bottomYUp, sink) => emitShapeItems(block, x, bottomYUp, sink, asm.ctx.pageHeight, figId);
2339
4225
  if (isOutOfFlowFloat(block.float)) {
2340
4226
  const fx = asm.floatX(block.float, block.widthPt);
2341
- const fy = asm.floatTopYUp(block.float);
2342
- emitShapeAt(fx, fy - block.heightPt, block.float.behind ? asm.floatsBehind : asm.floatsFront);
2343
- if (block.float.wrap !== "none") asm.exclusions.push({
2344
- x0: fx,
2345
- x1: fx + block.widthPt,
2346
- topYUp: fy,
2347
- bottomYUp: fy - block.heightPt
2348
- });
4227
+ const fy = asm.floatTopYUp(block.float, block.heightPt);
4228
+ emitShapeAt(fx, fy - block.heightPt, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
4229
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
2349
4230
  } else {
2350
4231
  asm.cursorY -= block.spacingBeforePt;
2351
4232
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
@@ -2356,43 +4237,14 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2356
4237
  }
2357
4238
  } else if (block.kind === "chart") {
2358
4239
  const figId = builder ? createFigure(builder, block.altText, "Chart") : void 0;
2359
- const fig = figId !== void 0 ? { structId: figId } : {};
2360
4240
  const emitChartAt = (x, bottomYUp, sink) => {
2361
- for (const s of block.layout.shapes) sink.push({
2362
- type: "shape",
2363
- shape: {
2364
- paths: s.paths,
2365
- ...s.fillColorHex ? { fillColorHex: s.fillColorHex } : {},
2366
- ...s.stroke ? { stroke: s.stroke } : {},
2367
- transform: flipTransform([
2368
- 1,
2369
- 0,
2370
- 0,
2371
- 1,
2372
- x,
2373
- bottomYUp
2374
- ], asm.ctx.pageHeight)
2375
- },
2376
- ...fig
2377
- });
2378
- for (const t of block.layout.texts) sink.push({
2379
- type: "line",
2380
- line: t.line,
2381
- originX: pt(x + t.x),
2382
- baselineY: pt(asm.ctx.pageHeight - (bottomYUp + t.y)),
2383
- ...fig
2384
- });
4241
+ sink.push(...chartPageItems(block, x, bottomYUp, asm.ctx.pageHeight, figId));
2385
4242
  };
2386
4243
  if (isOutOfFlowFloat(block.float)) {
2387
4244
  const fx = asm.floatX(block.float, block.widthPt);
2388
- const fy = asm.floatTopYUp(block.float);
2389
- emitChartAt(fx, fy - block.heightPt, block.float.behind ? asm.floatsBehind : asm.floatsFront);
2390
- if (block.float.wrap !== "none") asm.exclusions.push({
2391
- x0: fx,
2392
- x1: fx + block.widthPt,
2393
- topYUp: fy,
2394
- bottomYUp: fy - block.heightPt
2395
- });
4245
+ const fy = asm.floatTopYUp(block.float, block.heightPt);
4246
+ emitChartAt(fx, fy - block.heightPt, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
4247
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
2396
4248
  } else {
2397
4249
  asm.cursorY -= block.spacingBeforePt;
2398
4250
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
@@ -2403,6 +4255,19 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2403
4255
  }
2404
4256
  } else {
2405
4257
  const colCount = block.rows.reduce((max, r) => Math.max(max, r.cells.reduce((s, c) => Math.max(s, c.colStart + c.colSpan), 0)), 0);
4258
+ const anchorY = isOutOfFlowFloat(block.float) ? asm.floatTopYUp(block.float, block.heightPt) : 0;
4259
+ if (isOutOfFlowFloat(block.float) && anchorY - block.heightPt >= asm.ctx.marginBottom) {
4260
+ const fx = asm.floatX(block.float, block.totalWidthPt);
4261
+ const fy = anchorY;
4262
+ const sink = PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder);
4263
+ let rowY = fy;
4264
+ for (const row of block.rows) {
4265
+ emitRowChunk(sink, row, fx, rowY, asm.ctx.pageHeight, colCount);
4266
+ rowY -= row.heightPt;
4267
+ }
4268
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.totalWidthPt, block.heightPt);
4269
+ continue;
4270
+ }
2406
4271
  const headerRows = [];
2407
4272
  for (const r of block.rows) if (r.isHeader) headerRows.push(r);
2408
4273
  else break;
@@ -2439,8 +4304,16 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2439
4304
  asm.cursorY -= hr.heightPt;
2440
4305
  }
2441
4306
  }
2442
- emitRowChunk(asm.current, chunk, tableX, asm.cursorY, asm.ctx.pageHeight, colCount, cellStructIds);
4307
+ 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;
4308
+ emitRowChunk(asm.current, chunk, tableX, asm.cursorY, asm.ctx.pageHeight, colCount, cellStructIds, mergedHeights);
2443
4309
  asm.cursorY -= chunk.heightPt;
4310
+ if (ci === chunks.length - 1 && ri < block.rows.length - 1) {
4311
+ const next = block.rows[ri + 1];
4312
+ const nextHeight = Math.min(next.heightPt, asm.ctx.pageContentHeight);
4313
+ const nextForced = next.breakBefore && ri + 1 >= headerRows.length && asm.current.length > 0;
4314
+ const nextOverflow = asm.cursorY - nextHeight < asm.bottomLimit() && asm.colHasContent();
4315
+ if (nextForced || nextOverflow) emitRowBottomEdge(asm.current, chunk, tableX, asm.cursorY, asm.ctx.pageHeight);
4316
+ }
2444
4317
  }
2445
4318
  }
2446
4319
  }
@@ -2453,8 +4326,44 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2453
4326
  if (d.position === "header") page.commands.unshift(...cmds);
2454
4327
  else page.commands.push(...cmds);
2455
4328
  }
4329
+ for (const page of asm.pages) coalesceVerticalFills(page.commands);
2456
4330
  return asm.pages;
2457
4331
  }
4332
+ function coalesceVerticalFills(commands) {
4333
+ const touching = (a, b) => Math.abs(a.x - b.x) < .01 && Math.abs(a.width - b.width) < .01 && a.fillColorHex === b.fillColorHex && Math.min(a.y + a.height, b.y + b.height) >= Math.max(a.y, b.y) - .01;
4334
+ const open = /* @__PURE__ */ new Map();
4335
+ const dead = /* @__PURE__ */ new Set();
4336
+ for (const item of commands) {
4337
+ if (item.type !== "fill") continue;
4338
+ const fill = item;
4339
+ const k = `${fill.x.toFixed(2)}|${fill.width.toFixed(2)}|${fill.fillColorHex}`;
4340
+ const prev = open.get(k);
4341
+ if (prev && touching(prev, fill)) {
4342
+ const top = Math.max(prev.y + prev.height, fill.y + fill.height);
4343
+ const bottom = Math.min(prev.y, fill.y);
4344
+ prev.y = pt(bottom);
4345
+ prev.height = pt(top - bottom);
4346
+ dead.add(item);
4347
+ continue;
4348
+ }
4349
+ open.set(k, fill);
4350
+ }
4351
+ if (dead.size === 0) return;
4352
+ const kept = commands.filter((c) => !dead.has(c));
4353
+ commands.length = 0;
4354
+ commands.push(...kept);
4355
+ }
4356
+ function mergedBoxHeightPt(rows, startRowIdx, colStart, availablePt) {
4357
+ let height = rows[startRowIdx].heightPt;
4358
+ for (let r = startRowIdx + 1; r < rows.length; r++) {
4359
+ const cont = rows[r].cells.find((c) => c.colStart === colStart && (c.mergeRole === "middle" || c.mergeRole === "end"));
4360
+ if (!cont) break;
4361
+ if (height + rows[r].heightPt > availablePt) break;
4362
+ height += rows[r].heightPt;
4363
+ if (cont.mergeRole === "end") break;
4364
+ }
4365
+ return height;
4366
+ }
2458
4367
  function tableCellRowSpan(rows, startRowIdx, colStart) {
2459
4368
  let span = 1;
2460
4369
  for (let r = startRowIdx + 1; r < rows.length; r++) {
@@ -2465,32 +4374,74 @@ function tableCellRowSpan(rows, startRowIdx, colStart) {
2465
4374
  }
2466
4375
  return span;
2467
4376
  }
2468
- function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellStructIds) {
4377
+ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellStructIds, mergedHeights) {
2469
4378
  const rowTop = cursorY;
2470
4379
  const rowBottom = cursorY - row.heightPt;
4380
+ {
4381
+ let i = 0;
4382
+ while (i < row.cells.length) {
4383
+ const cell = row.cells[i];
4384
+ if (!cell.shadingColorHex) {
4385
+ i++;
4386
+ continue;
4387
+ }
4388
+ let width = cell.paintWidthPt ?? cell.widthPt;
4389
+ let j = i + 1;
4390
+ while (j < row.cells.length && cell.paintWidthPt === void 0) {
4391
+ const next = row.cells[j];
4392
+ if (next.mergeRole === "middle" || next.mergeRole === "end") break;
4393
+ if (next.shadingColorHex !== cell.shadingColorHex) break;
4394
+ width += next.paintWidthPt ?? next.widthPt;
4395
+ j++;
4396
+ }
4397
+ out.push({
4398
+ type: "fill",
4399
+ x: pt(marginLeft + (row.columnXOffsets[i] ?? 0)),
4400
+ y: pt(pageHeight - rowBottom - row.heightPt - FILL_SEAM_PT),
4401
+ width: pt(width),
4402
+ height: pt(row.heightPt + FILL_SEAM_PT),
4403
+ fillColorHex: cell.shadingColorHex
4404
+ });
4405
+ i = j;
4406
+ }
4407
+ }
2471
4408
  for (let i = 0; i < row.cells.length; i++) {
2472
4409
  const cell = row.cells[i];
2473
4410
  const cellX = marginLeft + (row.columnXOffsets[i] ?? 0);
2474
4411
  const structId = cellStructIds?.[i];
2475
- if (cell.shadingColorHex && cell.mergeRole !== "middle" && cell.mergeRole !== "end") out.push({
2476
- type: "fill",
2477
- x: pt(cellX),
2478
- y: pt(pageHeight - rowBottom - row.heightPt),
2479
- width: pt(cell.widthPt),
2480
- height: pt(row.heightPt),
2481
- fillColorHex: cell.shadingColorHex
2482
- });
2483
4412
  if (cell.dataBar && cell.mergeRole !== "middle" && cell.mergeRole !== "end") {
2484
4413
  const start = Math.max(0, Math.min(1, cell.dataBar.startFraction ?? 0));
2485
4414
  const barWidth = cell.widthPt * Math.max(0, Math.min(1, cell.dataBar.fraction));
2486
- if (barWidth > 0) out.push({
2487
- type: "fill",
2488
- x: pt(cellX + cell.widthPt * start),
2489
- y: pt(pageHeight - rowBottom - row.heightPt),
2490
- width: pt(barWidth),
2491
- height: pt(row.heightPt),
2492
- fillColorHex: cell.dataBar.colorHex
2493
- });
4415
+ if (barWidth > 0) {
4416
+ const barX = cellX + cell.widthPt * start;
4417
+ const barBottomYUp = rowBottom;
4418
+ out.push({
4419
+ type: "shape",
4420
+ shape: {
4421
+ paths: [rectAtPath(0, 0, barWidth, row.heightPt)],
4422
+ fillColorHex: cell.dataBar.colorHex,
4423
+ fillGradient: {
4424
+ kind: "linear",
4425
+ angle: cell.dataBar.negative ? 180 : 0,
4426
+ stops: [{
4427
+ offset: 0,
4428
+ colorHex: cell.dataBar.colorHex
4429
+ }, {
4430
+ offset: 1,
4431
+ colorHex: "FFFFFF"
4432
+ }]
4433
+ },
4434
+ transform: flipTransform([
4435
+ 1,
4436
+ 0,
4437
+ 0,
4438
+ 1,
4439
+ barX,
4440
+ barBottomYUp
4441
+ ], pageHeight)
4442
+ }
4443
+ });
4444
+ }
2494
4445
  }
2495
4446
  if (cell.icon && cell.mergeRole !== "middle" && cell.mergeRole !== "end") {
2496
4447
  const iconSize = Math.min(CF_ICON_SIZE_PT, Math.max(0, row.heightPt - 1));
@@ -2592,27 +4543,49 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
2592
4543
  if (diagUp) pushDiagonal(diagUp, 0, h);
2593
4544
  }
2594
4545
  if (cell.mergeRole === "middle" || cell.mergeRole === "end") continue;
2595
- let textY = rowTop - cell.padTopPt;
2596
- for (const line of cell.lines) {
4546
+ const boxHeightPt = mergedHeights?.[i] ?? row.heightPt;
4547
+ const slack = cell.verticalAlign !== void 0 ? boxHeightPt - cell.padTopPt - cell.contentHeightPt - cell.padBottomPt : 0;
4548
+ const vOffset = slack <= 0 ? 0 : cell.verticalAlign === "bottom" ? slack : cell.verticalAlign === "center" ? slack / 2 : 0;
4549
+ let textY = rowTop - cell.padTopPt - vOffset;
4550
+ const nestedX = cellX + cell.padLeftPt;
4551
+ const drawNestedBefore = (n) => {
4552
+ for (const nt of cell.nestedTables ?? []) {
4553
+ if (nt.afterLine !== n) continue;
4554
+ for (const nrow of nt.block.rows) {
4555
+ const nestedIds = structId !== void 0 ? nrow.cells.map(() => structId) : void 0;
4556
+ emitRowChunk(out, nrow, nestedX, textY, pageHeight, nt.block.colCount, nestedIds);
4557
+ textY -= nrow.heightPt;
4558
+ }
4559
+ }
4560
+ for (const cs of cell.shapes ?? []) {
4561
+ if (cs.afterLine !== n) continue;
4562
+ const offset = alignmentOffset(cs.block.resolvedAlignment, cs.block.widthPt, cell.widthPt - cell.padLeftPt - cell.padRightPt);
4563
+ textY -= cs.block.heightPt;
4564
+ emitShapeItems(cs.block, nestedX + offset, textY, out, pageHeight, structId);
4565
+ }
4566
+ };
4567
+ for (const [lineIdx, line] of cell.lines.entries()) {
4568
+ drawNestedBefore(lineIdx);
4569
+ textY -= cell.lineGaps?.get(lineIdx) ?? 0;
2597
4570
  const h = computeLineHeight(line, line.resolved);
2598
4571
  textY -= h;
2599
4572
  const offset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, cell.widthPt - cell.padLeftPt - cell.padRightPt);
4573
+ const indentLeft = line.resolved.indentLeft + (line.firstLine ? line.resolved.indentFirstLine : 0);
2600
4574
  out.push({
2601
4575
  type: "line",
2602
4576
  line,
2603
- originX: pt(cellX + cell.padLeftPt + offset),
2604
- baselineY: pt(pageHeight - (textY + lineDescent(line))),
4577
+ originX: pt(cellX + cell.padLeftPt + indentLeft + offset),
4578
+ baselineY: pt(pageHeight - (textY + lineBaselineOffset(line, line.resolved))),
4579
+ ...cell.clipped ? { clip: {
4580
+ x: pt(cellX),
4581
+ y: pt(pageHeight - rowTop),
4582
+ width: pt(cell.widthPt),
4583
+ height: pt(boxHeightPt)
4584
+ } } : {},
2605
4585
  ...structId !== void 0 ? { structId } : {}
2606
4586
  });
2607
4587
  }
2608
- if (cell.nestedTables) {
2609
- const nestedX = cellX + cell.padLeftPt;
2610
- for (const nt of cell.nestedTables) for (const nrow of nt.rows) {
2611
- const nestedIds = structId !== void 0 ? nrow.cells.map(() => structId) : void 0;
2612
- emitRowChunk(out, nrow, nestedX, textY, pageHeight, nt.colCount, nestedIds);
2613
- textY -= nrow.heightPt;
2614
- }
2615
- }
4588
+ drawNestedBefore(cell.lines.length);
2616
4589
  }
2617
4590
  }
2618
4591
  function splitRowIntoChunks(row, capacity) {
@@ -2697,6 +4670,7 @@ function splitRowIntoChunks(row, capacity) {
2697
4670
  }
2698
4671
  return out.length > 0 ? out : [row];
2699
4672
  }
4673
+ var FILL_SEAM_PT = .07;
2700
4674
  function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx, rowCount, colCount) {
2701
4675
  const pushSide = (side, border) => {
2702
4676
  if (!border || border.style === "none") return;
@@ -2706,10 +4680,11 @@ function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx,
2706
4680
  side,
2707
4681
  x: pt(cellX),
2708
4682
  y: pt(pageHeight - cellY - rowHeight),
2709
- width: pt(cell.widthPt),
4683
+ width: pt(cell.paintWidthPt ?? cell.widthPt),
2710
4684
  height: pt(rowHeight),
2711
4685
  borderSizePt: sz,
2712
- borderColorHex: border.colorHex ?? "000000"
4686
+ borderColorHex: border.colorHex ?? "000000",
4687
+ ...border.style !== "single" ? { borderStyle: border.style } : {}
2713
4688
  });
2714
4689
  };
2715
4690
  pushSide("top", cell.borders.top);
@@ -2717,5 +4692,101 @@ function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx,
2717
4692
  if (rowIdx === rowCount - 1) pushSide("bottom", cell.borders.bottom);
2718
4693
  if (cell.colStart + cell.colSpan - 1 === colCount - 1) pushSide("right", cell.borders.right);
2719
4694
  }
4695
+ var NO_WRAP_MEASURE_WIDTH = 1e6;
4696
+ /**
4697
+ * One line, trimmed to the glyph that STRADDLES `widthPt` — what a non-wrapping
4698
+ * spreadsheet cell shows. Excel and LibreOffice paint that glyph and cut it
4699
+ * through the middle, and so do we now: the caller marks the cell and the
4700
+ * emitters clip the line to its box (TextLineItem.clip). Stopping at the last
4701
+ * glyph that wholly fits is what dropped a character off every cut cell.
4702
+ */
4703
+ function clipLineToWidth(lines, widthPt, hashOnOverflow = false) {
4704
+ const line = lines[0];
4705
+ if (!line) return [];
4706
+ if (line.contentWidthPt <= widthPt) return [line];
4707
+ if (hashOnOverflow) return [hashFill(line, widthPt)];
4708
+ const tokens = [];
4709
+ let used = 0;
4710
+ for (const token of line.tokens) {
4711
+ const advance = token.widthPt;
4712
+ if (used + advance <= widthPt) {
4713
+ tokens.push(token);
4714
+ used += advance;
4715
+ continue;
4716
+ }
4717
+ if (token.kind === "text") {
4718
+ const room = widthPt - used;
4719
+ let text = "";
4720
+ let textWidth = 0;
4721
+ for (const ch of token.text) {
4722
+ if (textWidth >= room) break;
4723
+ text += ch;
4724
+ textWidth = token.font.measure.textWidthPt(text, token.fontSizePt);
4725
+ }
4726
+ if (text.length > 0) {
4727
+ tokens.push({
4728
+ ...token,
4729
+ text,
4730
+ widthPt: textWidth
4731
+ });
4732
+ used += textWidth;
4733
+ }
4734
+ }
4735
+ break;
4736
+ }
4737
+ return [{
4738
+ ...line,
4739
+ tokens,
4740
+ contentWidthPt: used
4741
+ }];
4742
+ }
4743
+ /**
4744
+ * The line as a row of `#` filling `widthPt` — what Excel and LibreOffice put in
4745
+ * a cell whose number does not fit. At least one, so the cell is never blank:
4746
+ * a column too narrow even for a single `#` still has to say something.
4747
+ */
4748
+ function hashFill(line, widthPt) {
4749
+ const first = line.tokens.find((t) => t.kind === "text");
4750
+ if (!first) return line;
4751
+ const one = first.font.measure.textWidthPt("#", first.fontSizePt);
4752
+ const count = one > 0 ? Math.max(1, Math.floor(widthPt / one)) : 1;
4753
+ const text = "#".repeat(count);
4754
+ const token = {
4755
+ ...first,
4756
+ text,
4757
+ widthPt: first.font.measure.textWidthPt(text, first.fontSizePt)
4758
+ };
4759
+ return {
4760
+ ...line,
4761
+ tokens: [token],
4762
+ contentWidthPt: token.widthPt
4763
+ };
4764
+ }
4765
+ /**
4766
+ * The bottom edge of a row that ends a page — the rule the row below it owed it
4767
+ * and drew on the next page instead. See the call site: a table that spills over
4768
+ * a break is closed on both sides of it, the way Excel and Word print one.
4769
+ *
4770
+ * A cell whose bottom is suppressed (a vertical merge continuing past the break)
4771
+ * stays open; inventing an edge there would cut the merge in half.
4772
+ */
4773
+ function emitRowBottomEdge(out, row, marginLeft, rowBottom, pageHeight) {
4774
+ for (let i = 0; i < row.cells.length; i++) {
4775
+ const cell = row.cells[i];
4776
+ const border = cell.borders.bottom;
4777
+ if (!border || border.style === "none") continue;
4778
+ out.push({
4779
+ type: "border",
4780
+ side: "bottom",
4781
+ x: pt(marginLeft + (row.columnXOffsets[i] ?? 0)),
4782
+ y: pt(pageHeight - rowBottom - row.heightPt),
4783
+ width: pt(cell.widthPt),
4784
+ height: pt(row.heightPt),
4785
+ borderSizePt: border.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT,
4786
+ borderColorHex: border.colorHex ?? "000000",
4787
+ ...border.style !== "single" ? { borderStyle: border.style } : {}
4788
+ });
4789
+ }
4790
+ }
2720
4791
  //#endregion
2721
- export { layoutStyledDocument };
4792
+ export { GLUE_SHRINK_RATIO, layoutStyledDocument };