docxodus 9.7.0 → 9.8.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.
@@ -1 +1 @@
1
- {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,7 @@
5
5
  * and flows it across fixed-size page containers based on document dimensions.
6
6
  */
7
7
  import { formatPageNumber } from "./page-number-format.js";
8
- import { DEFAULT_MARGIN, DEFAULT_PAGE_WIDTH, parseSectionDimensions, ptToPx, pxToPt, } from "./page-geometry.js";
8
+ import { DEFAULT_MARGIN, DEFAULT_PAGE_WIDTH, parseSectionDimensions, ptToPx, pxToPt, resolvePageBands, } from "./page-geometry.js";
9
9
  // Default letter size in points (612 x 792 = 8.5" x 11")
10
10
  // Maximum percentage of content height that footnotes can occupy
11
11
  // This allows footnotes to expand upward into body content space when needed
@@ -276,7 +276,7 @@ export class PaginationEngine {
276
276
  * send it through the oversized-block fallback again.
277
277
  */
278
278
  smallestEffectiveContentHeight(dims, sectionIndex) {
279
- return Math.min(this.getEffectiveHeights(dims, sectionIndex, 1, 1).contentHeight, this.getEffectiveHeights(dims, sectionIndex, 2, 1).contentHeight, this.getEffectiveHeights(dims, sectionIndex, 2, 2).contentHeight);
279
+ return Math.min(this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight, this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight, this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight);
280
280
  }
281
281
  /**
282
282
  * Builds a clone of a simple table wrapper containing a contiguous run of rows.
@@ -724,7 +724,7 @@ export class PaginationEngine {
724
724
  return 0;
725
725
  }
726
726
  // Measure in the SAME styling context the notes render in: `.page-footnotes` carries
727
- // font-size 0.85em and line-height 1.4, so measuring without the class sizes the note
727
+ // its own font-size and line-height, so measuring without the class sizes the note
728
728
  // block against body type and the reserve can never match what is drawn.
729
729
  // Create a temporary measurement container
730
730
  const measureContainer = document.createElement("div");
@@ -884,7 +884,7 @@ export class PaginationEngine {
884
884
  /**
885
885
  * Adds footnotes to a page container, including continuation content.
886
886
  */
887
- addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes) {
887
+ addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes) {
888
888
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
889
889
  if (footnoteIds.length === 0 && !hasContinuation) {
890
890
  return;
@@ -895,11 +895,13 @@ export class PaginationEngine {
895
895
  // Create a set of partial footnote IDs for quick lookup
896
896
  const partialFootnoteIds = new Set(partialFootnotes?.map(p => p.footnoteId) || []);
897
897
  // Calculate max height for footnotes area (content height minus margin for body content)
898
- const maxFootnoteHeight = Math.min(footnoteHeight, dims.contentHeight * MAX_FOOTNOTE_AREA_RATIO);
898
+ const maxFootnoteHeight = Math.min(footnoteHeight, bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO);
899
899
  const footnotesDiv = document.createElement("div");
900
900
  footnotesDiv.className = `${this.cssPrefix}footnotes`;
901
901
  footnotesDiv.style.position = "absolute";
902
- footnotesDiv.style.bottom = `${dims.marginBottom}pt`; // Above footer area
902
+ // Notes sit at the FOOT OF THE BODY BAND, not at the bottom margin: a footer taller than
903
+ // its margin raises that edge, and anchoring to the raw margin would draw notes over it.
904
+ footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
903
905
  footnotesDiv.style.left = `${dims.marginLeft}pt`;
904
906
  footnotesDiv.style.width = `${dims.contentWidth}pt`;
905
907
  footnotesDiv.style.boxSizing = "border-box";
@@ -991,63 +993,37 @@ export class PaginationEngine {
991
993
  return sectionHf.footerDefault;
992
994
  }
993
995
  /**
994
- * Computes effective header, footer, and content heights for a specific page position.
995
- * Uses pre-measured header/footer heights from the registry.
996
- * This method is deterministic - same inputs always produce same outputs.
997
- * This enables lazy loading compatibility since available height can be computed
998
- * for any page position without knowing the page's content.
996
+ * The header, body, and footer bands for one page position.
997
+ *
998
+ * The single owner of "where does anything sit vertically on this page" — placement in
999
+ * {@link createPage}, the body budget the flow loop spends, and the note area's anchor all
1000
+ * read it, so those three cannot disagree about where the body ends.
1001
+ *
1002
+ * Deterministic: it depends only on the section's page setup and the registry's pre-measured
1003
+ * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
999
1004
  */
1000
- getEffectiveHeights(dims, sectionIndex, pageInSection, globalPageNumber) {
1005
+ getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
1001
1006
  const sectionHf = this.hfRegistry.get(sectionIndex);
1002
- // Determine effective header height
1003
- let headerHeight = dims.marginTop;
1004
- if (sectionHf) {
1005
- let measuredHeaderHeight;
1006
- // Select the appropriate header height based on page position
1007
- if (pageInSection === 1 && sectionHf.headerFirstHeight != null) {
1008
- measuredHeaderHeight = sectionHf.headerFirstHeight;
1009
- }
1010
- else if (globalPageNumber % 2 === 0 && sectionHf.headerEvenHeight != null) {
1011
- measuredHeaderHeight = sectionHf.headerEvenHeight;
1012
- }
1013
- else if (sectionHf.headerDefaultHeight != null) {
1014
- measuredHeaderHeight = sectionHf.headerDefaultHeight;
1015
- }
1016
- if (measuredHeaderHeight != null) {
1017
- // Use the larger of margin height or measured content height
1018
- headerHeight = Math.max(dims.marginTop, measuredHeaderHeight);
1019
- }
1020
- }
1021
- // Determine effective footer height
1022
- let footerHeight = dims.marginBottom;
1023
- if (sectionHf) {
1024
- let measuredFooterHeight;
1025
- // Select the appropriate footer height based on page position
1026
- if (pageInSection === 1 && sectionHf.footerFirstHeight != null) {
1027
- measuredFooterHeight = sectionHf.footerFirstHeight;
1028
- }
1029
- else if (globalPageNumber % 2 === 0 && sectionHf.footerEvenHeight != null) {
1030
- measuredFooterHeight = sectionHf.footerEvenHeight;
1031
- }
1032
- else if (sectionHf.footerDefaultHeight != null) {
1033
- measuredFooterHeight = sectionHf.footerDefaultHeight;
1034
- }
1035
- if (measuredFooterHeight != null) {
1036
- // Use the larger of margin height or measured content height
1037
- footerHeight = Math.max(dims.marginBottom, measuredFooterHeight);
1038
- }
1039
- }
1040
- // Calculate effective content height
1041
- // contentHeight from dims is: pageHeight - marginTop - marginBottom
1042
- // We need to adjust for any header/footer expansion beyond margins
1043
- const headerExpansion = headerHeight - dims.marginTop;
1044
- const footerExpansion = footerHeight - dims.marginBottom;
1045
- const contentHeight = dims.contentHeight - headerExpansion - footerExpansion;
1046
- return { headerHeight, footerHeight, contentHeight };
1007
+ return resolvePageBands(dims, this.selectStoryHeight(sectionHf?.headerFirstHeight, sectionHf?.headerEvenHeight, sectionHf?.headerDefaultHeight, pageInSection, globalPageNumber), this.selectStoryHeight(sectionHf?.footerFirstHeight, sectionHf?.footerEvenHeight, sectionHf?.footerDefaultHeight, pageInSection, globalPageNumber));
1008
+ }
1009
+ /**
1010
+ * The measured height of the running story this page position selects, mirroring
1011
+ * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
1012
+ */
1013
+ selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
1014
+ if (pageInSection === 1 && first != null)
1015
+ return first;
1016
+ if (globalPageNumber % 2 === 0 && even != null)
1017
+ return even;
1018
+ return fallback ?? 0;
1047
1019
  }
1048
1020
  /**
1049
1021
  * Measures the content height of a header or footer element.
1050
- * This is needed because headers/footers can contain more content than fits in the margin area.
1022
+ *
1023
+ * This is what tells {@link resolvePageBands} whether the story stays inside its margin or
1024
+ * pushes the body, so it must measure the story ALONE — any padding added here would have to
1025
+ * be added to the rendered band too, and the two drifting apart is exactly how a header
1026
+ * silently starts overlapping body text.
1051
1027
  */
1052
1028
  measureHeaderFooterHeight(source, contentWidth) {
1053
1029
  // Create a temporary measurement container
@@ -1056,8 +1032,6 @@ export class PaginationEngine {
1056
1032
  measureContainer.style.visibility = "hidden";
1057
1033
  measureContainer.style.width = `${contentWidth}pt`;
1058
1034
  measureContainer.style.left = "-9999px";
1059
- // Add padding to match the actual rendering
1060
- measureContainer.style.paddingBottom = "4pt";
1061
1035
  // Clone and add the header/footer content
1062
1036
  for (const child of Array.from(source.childNodes)) {
1063
1037
  measureContainer.appendChild(child.cloneNode(true));
@@ -1083,7 +1057,7 @@ export class PaginationEngine {
1083
1057
  // Track page number within this section for first-page header/footer selection
1084
1058
  let pageInSection = 1;
1085
1059
  // Get effective content height for first page (accounts for header/footer sizes)
1086
- let { contentHeight: effectiveContentHeight } = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
1060
+ let { bodyHeight: effectiveContentHeight } = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1087
1061
  let remainingHeight = effectiveContentHeight;
1088
1062
  // Track the previous block's bottom margin for margin collapsing
1089
1063
  let prevMarginBottomPt = 0;
@@ -1123,8 +1097,8 @@ export class PaginationEngine {
1123
1097
  pageInSection++;
1124
1098
  currentContent = [];
1125
1099
  // Get effective content height for new page position
1126
- const newHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
1127
- effectiveContentHeight = newHeights.contentHeight;
1100
+ const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1101
+ effectiveContentHeight = newBands.bodyHeight;
1128
1102
  remainingHeight = effectiveContentHeight;
1129
1103
  prevMarginBottomPt = 0; // Reset margin tracking for new page
1130
1104
  currentFootnoteIds = []; // Reset footnotes for new page
@@ -1188,14 +1162,14 @@ export class PaginationEngine {
1188
1162
  additionalChainFootnoteHeight;
1189
1163
  const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
1190
1164
  if (currentChainHeight > currentAvailableHeight) {
1191
- const nextPageHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection + 1, pageNumber + 1);
1165
+ const nextPageBands = this.getPageBands(dims, sectionIndex, pageInSection + 1, pageNumber + 1);
1192
1166
  const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(keepChain, 0, true);
1193
1167
  // finishPage transfers this continuation to the new page's
1194
1168
  // currentContinuation state, so include it in the destination
1195
1169
  // page's footnote reservation before deciding to move the chain.
1196
1170
  const freshChainFootnoteHeight = this.measureFootnotesHeight(newChainFootnoteIds, dims.contentWidth, nextPageContinuation);
1197
1171
  if (freshChainBodyHeight + freshChainFootnoteHeight <=
1198
- nextPageHeights.contentHeight) {
1172
+ nextPageBands.bodyHeight) {
1199
1173
  finishPage();
1200
1174
  }
1201
1175
  }
@@ -1425,6 +1399,144 @@ export class PaginationEngine {
1425
1399
  el.setAttribute("contenteditable", "false");
1426
1400
  }
1427
1401
  }
1402
+ /**
1403
+ * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
1404
+ *
1405
+ * The converter deliberately carries the OOXML bases and offsets as data instead of flattening
1406
+ * them into CSS: page/margin/column bases belong to the page, while paragraph/line/character
1407
+ * bases belong to the laid-out anchor. Once both coordinate systems exist, promote the object
1408
+ * from the clipped text column into the page box and position it in one shared point space.
1409
+ */
1410
+ positionDrawingAnchors(pageBox, contentArea, dims, pageNumber) {
1411
+ const anchors = Array.from(contentArea.querySelectorAll('[data-docx-drawing-anchor="true"]'));
1412
+ if (anchors.length === 0)
1413
+ return;
1414
+ const pageRect = pageBox.getBoundingClientRect();
1415
+ const pixelsPerPoint = pageRect.width / dims.pageWidth;
1416
+ if (!(pixelsPerPoint > 0))
1417
+ return;
1418
+ for (const anchor of anchors) {
1419
+ const staticRect = anchor.getBoundingClientRect();
1420
+ const paragraph = anchor.closest('p, h1, h2, h3, h4, h5, h6');
1421
+ const paragraphRect = paragraph?.getBoundingClientRect() ?? staticRect;
1422
+ const toPageX = (x) => (x - pageRect.left) / pixelsPerPoint;
1423
+ const toPageY = (y) => (y - pageRect.top) / pixelsPerPoint;
1424
+ const context = {
1425
+ staticLeft: toPageX(staticRect.left),
1426
+ staticTop: toPageY(staticRect.top),
1427
+ lineHeight: paragraph
1428
+ ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint
1429
+ : staticRect.height / pixelsPerPoint,
1430
+ paragraphLeft: toPageX(paragraphRect.left),
1431
+ paragraphTop: toPageY(paragraphRect.top),
1432
+ paragraphWidth: paragraphRect.width / pixelsPerPoint,
1433
+ paragraphHeight: paragraphRect.height / pixelsPerPoint,
1434
+ };
1435
+ const widthReference = this.horizontalAnchorReference(anchor.getAttribute('data-docx-anchor-width-relative') ?? 'margin', dims, pageNumber, context);
1436
+ const widthPercent = this.anchorNumber(anchor, 'width-percent');
1437
+ if (widthPercent !== undefined) {
1438
+ anchor.style.width = `${widthReference.size * widthPercent / 100}pt`;
1439
+ }
1440
+ const heightReference = this.verticalAnchorReference(anchor.getAttribute('data-docx-anchor-height-relative') ?? 'margin', dims, pageNumber, context);
1441
+ const heightPercent = this.anchorNumber(anchor, 'height-percent');
1442
+ if (heightPercent !== undefined && anchor.dataset.docxAnchorAutofit !== 'true') {
1443
+ anchor.style.height = `${heightReference.size * heightPercent / 100}pt`;
1444
+ }
1445
+ // Read the final border-box size after relative sizing and before reparenting. The stored
1446
+ // wp:extent remains the fallback; wps:bodyPr insets stay inside it through border-box sizing.
1447
+ const sizedRect = anchor.getBoundingClientRect();
1448
+ const width = sizedRect.width / pixelsPerPoint;
1449
+ const height = sizedRect.height / pixelsPerPoint;
1450
+ const horizontal = this.horizontalAnchorReference(anchor.getAttribute('data-docx-anchor-h-relative') ?? 'column', dims, pageNumber, context);
1451
+ const vertical = this.verticalAnchorReference(anchor.getAttribute('data-docx-anchor-v-relative') ?? 'paragraph', dims, pageNumber, context);
1452
+ const left = this.resolveAnchorAxis(horizontal, width, anchor.getAttribute('data-docx-anchor-h-align'), this.anchorNumber(anchor, 'h-offset'), pageNumber);
1453
+ const top = this.resolveAnchorAxis(vertical, height, anchor.getAttribute('data-docx-anchor-v-align'), this.anchorNumber(anchor, 'v-offset'), pageNumber);
1454
+ pageBox.appendChild(anchor);
1455
+ anchor.style.position = 'absolute';
1456
+ anchor.style.left = `${left}pt`;
1457
+ anchor.style.top = `${top}pt`;
1458
+ anchor.style.margin = '0';
1459
+ }
1460
+ }
1461
+ anchorNumber(anchor, suffix) {
1462
+ const raw = anchor.getAttribute(`data-docx-anchor-${suffix}`);
1463
+ if (raw === null)
1464
+ return undefined;
1465
+ const value = Number(raw);
1466
+ return Number.isFinite(value) ? value : undefined;
1467
+ }
1468
+ horizontalAnchorReference(relativeFrom, dims, pageNumber, context) {
1469
+ const leftMargin = { start: 0, size: dims.marginLeft };
1470
+ const rightMargin = {
1471
+ start: dims.marginLeft + dims.contentWidth,
1472
+ size: dims.marginRight,
1473
+ };
1474
+ switch (relativeFrom) {
1475
+ case 'page':
1476
+ return { start: 0, size: dims.pageWidth };
1477
+ case 'character':
1478
+ return { start: context.staticLeft, size: 0 };
1479
+ case 'leftMargin':
1480
+ return leftMargin;
1481
+ case 'rightMargin':
1482
+ return rightMargin;
1483
+ case 'insideMargin':
1484
+ return pageNumber % 2 === 1 ? leftMargin : rightMargin;
1485
+ case 'outsideMargin':
1486
+ return pageNumber % 2 === 1 ? rightMargin : leftMargin;
1487
+ case 'paragraph':
1488
+ // Not part of ST_RelFromH, but accepting it gives malformed/legacy producers the
1489
+ // intuitive base without conflating it with the whole text column.
1490
+ return { start: context.paragraphLeft, size: context.paragraphWidth };
1491
+ case 'column':
1492
+ case 'margin':
1493
+ default:
1494
+ // The paginator currently lays out one text column per section, so the column and page
1495
+ // margin boxes coincide. Keeping the names distinct here leaves multi-column geometry
1496
+ // localized to this resolver when that layout support is added.
1497
+ return { start: dims.marginLeft, size: dims.contentWidth };
1498
+ }
1499
+ }
1500
+ verticalAnchorReference(relativeFrom, dims, pageNumber, context) {
1501
+ const topMargin = { start: 0, size: dims.marginTop };
1502
+ const bottomMargin = {
1503
+ start: dims.marginTop + dims.contentHeight,
1504
+ size: dims.marginBottom,
1505
+ };
1506
+ switch (relativeFrom) {
1507
+ case 'page':
1508
+ return { start: 0, size: dims.pageHeight };
1509
+ case 'paragraph':
1510
+ return { start: context.paragraphTop, size: context.paragraphHeight };
1511
+ case 'line':
1512
+ return { start: context.staticTop, size: context.lineHeight };
1513
+ case 'topMargin':
1514
+ return topMargin;
1515
+ case 'bottomMargin':
1516
+ return bottomMargin;
1517
+ case 'insideMargin':
1518
+ return pageNumber % 2 === 1 ? topMargin : bottomMargin;
1519
+ case 'outsideMargin':
1520
+ return pageNumber % 2 === 1 ? bottomMargin : topMargin;
1521
+ case 'margin':
1522
+ default:
1523
+ return { start: dims.marginTop, size: dims.contentHeight };
1524
+ }
1525
+ }
1526
+ resolveAnchorAxis(reference, objectSize, alignment, offset, pageNumber) {
1527
+ if (offset !== undefined)
1528
+ return reference.start + offset;
1529
+ let fraction = 0;
1530
+ if (alignment === 'center')
1531
+ fraction = 0.5;
1532
+ else if (alignment === 'right' || alignment === 'bottom')
1533
+ fraction = 1;
1534
+ else if (alignment === 'inside')
1535
+ fraction = pageNumber % 2 === 1 ? 0 : 1;
1536
+ else if (alignment === 'outside')
1537
+ fraction = pageNumber % 2 === 1 ? 1 : 0;
1538
+ return reference.start + (reference.size - objectSize) * fraction;
1539
+ }
1428
1540
  /**
1429
1541
  * Creates a page container element.
1430
1542
  */
@@ -1464,24 +1576,27 @@ export class PaginationEngine {
1464
1576
  // Needed by substitutePageNumberFields: a section that restarts numbering counts from its own
1465
1577
  // first page, not from the document's.
1466
1578
  pageBox.dataset.pageInSection = String(pageInSection);
1467
- // Get pre-computed effective heights for this page position (no re-measurement needed)
1468
- const effectiveHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
1579
+ // Where the three bands sit on this page (no re-measurement needed)
1580
+ const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1469
1581
  // Add header if available for this section/page
1470
1582
  const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
1471
1583
  if (headerSource) {
1472
1584
  const headerDiv = document.createElement("div");
1473
1585
  headerDiv.className = `${this.cssPrefix}header`;
1474
1586
  headerDiv.style.position = "absolute";
1475
- headerDiv.style.top = "0"; // Start at page top
1587
+ // `w:header` is the distance to the TOP of the story, and the story grows downward from
1588
+ // there; `flex-start` therefore pins the edge the OOXML actually declares, and content
1589
+ // taller than the measurement clips at the bottom rather than sliding up the page.
1590
+ headerDiv.style.top = `${bands.headerTop}pt`;
1591
+ headerDiv.style.bottom = "auto";
1476
1592
  headerDiv.style.left = `${dims.marginLeft}pt`;
1477
1593
  headerDiv.style.width = `${dims.contentWidth}pt`;
1478
- headerDiv.style.height = `${effectiveHeights.headerHeight}pt`; // Use pre-computed effective height
1594
+ headerDiv.style.height = `${bands.headerHeight}pt`;
1479
1595
  headerDiv.style.overflow = "hidden";
1480
1596
  headerDiv.style.boxSizing = "border-box";
1481
1597
  headerDiv.style.display = "flex";
1482
1598
  headerDiv.style.flexDirection = "column";
1483
- headerDiv.style.justifyContent = "flex-end"; // Align content to bottom of header area
1484
- headerDiv.style.paddingBottom = "4pt"; // Small gap before content area
1599
+ headerDiv.style.justifyContent = "flex-start";
1485
1600
  // Clone the header content (skip the wrapper div's data attributes)
1486
1601
  for (const child of Array.from(headerSource.childNodes)) {
1487
1602
  const clonedheaderDiv = child.cloneNode(true);
@@ -1492,8 +1607,8 @@ export class PaginationEngine {
1492
1607
  pageBox.appendChild(headerDiv);
1493
1608
  }
1494
1609
  // Create content area using pre-computed effective heights
1495
- const contentAreaTop = effectiveHeights.headerHeight;
1496
- const contentAreaHeight = effectiveHeights.contentHeight;
1610
+ const contentAreaTop = bands.bodyTop;
1611
+ const contentAreaHeight = bands.bodyHeight;
1497
1612
  const contentArea = document.createElement("div");
1498
1613
  contentArea.className = `${this.cssPrefix}content`;
1499
1614
  contentArea.style.position = "absolute";
@@ -1510,7 +1625,7 @@ export class PaginationEngine {
1510
1625
  // Add footnotes if any references appear on this page (or continuation from previous)
1511
1626
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
1512
1627
  if (footnoteIds.length > 0 || hasContinuation) {
1513
- this.addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes);
1628
+ this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
1514
1629
  }
1515
1630
  // Add footer if available for this section/page
1516
1631
  const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
@@ -1518,16 +1633,18 @@ export class PaginationEngine {
1518
1633
  const footerDiv = document.createElement("div");
1519
1634
  footerDiv.className = `${this.cssPrefix}footer`;
1520
1635
  footerDiv.style.position = "absolute";
1521
- footerDiv.style.bottom = "0"; // Start at page bottom
1636
+ // `w:footer` is the distance to the BOTTOM of the story, and the story grows upward from
1637
+ // there — the mirror of the header, so `flex-end` and a bottom anchor.
1638
+ footerDiv.style.top = "auto";
1639
+ footerDiv.style.bottom = `${dims.footerDistance}pt`;
1522
1640
  footerDiv.style.left = `${dims.marginLeft}pt`;
1523
1641
  footerDiv.style.width = `${dims.contentWidth}pt`;
1524
- footerDiv.style.height = `${effectiveHeights.footerHeight}pt`; // Use pre-computed effective height
1642
+ footerDiv.style.height = `${bands.footerHeight}pt`;
1525
1643
  footerDiv.style.overflow = "hidden";
1526
1644
  footerDiv.style.boxSizing = "border-box";
1527
1645
  footerDiv.style.display = "flex";
1528
1646
  footerDiv.style.flexDirection = "column";
1529
- footerDiv.style.justifyContent = "flex-start"; // Align content to top of footer area
1530
- footerDiv.style.paddingTop = "4pt"; // Small gap after content area
1647
+ footerDiv.style.justifyContent = "flex-end";
1531
1648
  // Clone the footer content (skip the wrapper div's data attributes)
1532
1649
  for (const child of Array.from(footerSource.childNodes)) {
1533
1650
  const clonedfooterDiv = child.cloneNode(true);
@@ -1546,6 +1663,9 @@ export class PaginationEngine {
1546
1663
  }
1547
1664
  // Add to container
1548
1665
  this.containerElement.appendChild(pageBox);
1666
+ // Floating objects need the final page and anchor-paragraph boxes. Resolve them only after
1667
+ // append, when getBoundingClientRect() is meaningful, and lift them out of content clipping.
1668
+ this.positionDrawingAnchors(pageBox, contentArea, dims, pageNumber);
1549
1669
  // Body and notes must occupy DISJOINT bands. The note block is absolutely positioned against
1550
1670
  // the page bottom and grows upward, while the content area spans the whole text height — so
1551
1671
  // nothing but the layout loop's arithmetic keeps them apart, and any disagreement between the