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.
@@ -18,8 +18,10 @@
18
18
  export const DEFAULT_PAGE_WIDTH = 612;
19
19
  export const DEFAULT_PAGE_HEIGHT = 792;
20
20
  export const DEFAULT_MARGIN = 72; // 1 inch
21
- /** Default header/footer distance (0.5 inch). */
22
- export const DEFAULT_HEADER_FOOTER_HEIGHT = 36;
21
+ /** Default header/footer distance from the paper edge (0.5 inch). */
22
+ export const DEFAULT_HEADER_FOOTER_DISTANCE = 36;
23
+ /** @deprecated Misnamed alias of {@link DEFAULT_HEADER_FOOTER_DISTANCE}. */
24
+ export const DEFAULT_HEADER_FOOTER_HEIGHT = DEFAULT_HEADER_FOOTER_DISTANCE;
23
25
  /** Converts pixels to points (assuming 96 DPI screen). */
24
26
  export function pxToPt(px) {
25
27
  return px * 0.75; // 72 points / 96 pixels
@@ -38,8 +40,10 @@ export function parseSectionDimensions(section) {
38
40
  const marginRight = parseFloat(section.dataset.marginRight || "") || DEFAULT_MARGIN;
39
41
  const marginBottom = parseFloat(section.dataset.marginBottom || "") || DEFAULT_MARGIN;
40
42
  const marginLeft = parseFloat(section.dataset.marginLeft || "") || DEFAULT_MARGIN;
41
- const headerHeight = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_HEIGHT;
42
- const footerHeight = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_HEIGHT;
43
+ // `data-header-height`/`data-footer-height` carry `w:pgMar/@w:header` and `@w:footer`, which
44
+ // are distances from the paper edge — the attribute names predate that being understood.
45
+ const headerDistance = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
46
+ const footerDistance = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
43
47
  return {
44
48
  pageWidth,
45
49
  pageHeight,
@@ -49,8 +53,61 @@ export function parseSectionDimensions(section) {
49
53
  marginRight,
50
54
  marginBottom,
51
55
  marginLeft,
52
- headerHeight,
53
- footerHeight,
56
+ headerDistance,
57
+ footerDistance,
58
+ headerHeight: headerDistance,
59
+ footerHeight: footerDistance,
60
+ };
61
+ }
62
+ /**
63
+ * A body band this short cannot hold a single line, and a page that can hold nothing sends
64
+ * every block down the oversized-block path. Running content may push the body — Word does
65
+ * that — but it may not delete it, so a pathological header/footer gives this much back.
66
+ */
67
+ export const MIN_BODY_BAND_PT = 12;
68
+ /**
69
+ * Word's page band model.
70
+ *
71
+ * `w:header`/`w:footer` are DISTANCES from the paper edge to the running content, and
72
+ * `w:top`/`w:bottom` are distances from the paper edge to the body text — four independent
73
+ * numbers, not two nested boxes. The header is laid out from `headerDistance` downward and
74
+ * the footer from `footerDistance` upward; the body then starts at the top margin unless the
75
+ * header has already run past it, and ends at the bottom margin unless the footer has
76
+ * already climbed above it.
77
+ *
78
+ * Anchoring the bands to the margins instead — header bottom-aligned to `marginTop`, footer
79
+ * top-aligned to `pageHeight - marginBottom` — collapses both toward the body by exactly
80
+ * `margin - distance`, which is what this function replaced.
81
+ */
82
+ export function resolvePageBands(dims, headerContentHeight, footerContentHeight) {
83
+ const headerTop = dims.headerDistance;
84
+ const footerBottom = dims.pageHeight - dims.footerDistance;
85
+ // A page with NO running story has nothing below the paper edge to push the body: the
86
+ // distance alone reserves no space. (Word's own empty header is still a story — one empty
87
+ // paragraph with a real line height — so zero here means absent, not blank.)
88
+ const headerReach = headerContentHeight > 0 ? headerTop + headerContentHeight : 0;
89
+ const footerReach = footerContentHeight > 0 ? footerBottom - footerContentHeight : dims.pageHeight;
90
+ let bodyTop = Math.max(dims.marginTop, headerReach);
91
+ let bodyBottom = Math.min(dims.pageHeight - dims.marginBottom, footerReach);
92
+ // Hand back whatever the bands over-claimed, in proportion to how far each grew past its
93
+ // own margin, so a tall header does not silently consume the whole page.
94
+ const shortfall = MIN_BODY_BAND_PT - (bodyBottom - bodyTop);
95
+ if (shortfall > 0) {
96
+ const headerGrowth = bodyTop - dims.marginTop;
97
+ const footerGrowth = dims.pageHeight - dims.marginBottom - bodyBottom;
98
+ const growth = headerGrowth + footerGrowth;
99
+ if (growth > 0) {
100
+ bodyTop -= (shortfall * headerGrowth) / growth;
101
+ bodyBottom += (shortfall * footerGrowth) / growth;
102
+ }
103
+ }
104
+ return {
105
+ headerTop,
106
+ headerHeight: Math.max(0, Math.min(headerContentHeight, bodyTop - headerTop)),
107
+ bodyTop,
108
+ bodyHeight: Math.max(0, bodyBottom - bodyTop),
109
+ footerTop: footerBottom - Math.max(0, Math.min(footerContentHeight, footerBottom - bodyBottom)),
110
+ footerHeight: Math.max(0, Math.min(footerContentHeight, footerBottom - bodyBottom)),
54
111
  };
55
112
  }
56
113
  /** Every section wrapper under `root`, or `root` itself when the render stamped none. */
@@ -1 +1 @@
1
- {"version":3,"file":"page-geometry.js","sourceRoot":"","sources":["../src/page-geometry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"page-geometry.js","sourceRoot":"","sources":["../src/page-geometry.ts"],"names":[],"mappings":"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"}
@@ -63,7 +63,7 @@ var DocxodusPagination = (() => {
63
63
  var DEFAULT_PAGE_WIDTH = 612;
64
64
  var DEFAULT_PAGE_HEIGHT = 792;
65
65
  var DEFAULT_MARGIN = 72;
66
- var DEFAULT_HEADER_FOOTER_HEIGHT = 36;
66
+ var DEFAULT_HEADER_FOOTER_DISTANCE = 36;
67
67
  function pxToPt(px) {
68
68
  return px * 0.75;
69
69
  }
@@ -79,8 +79,8 @@ var DocxodusPagination = (() => {
79
79
  const marginRight = parseFloat(section.dataset.marginRight || "") || DEFAULT_MARGIN;
80
80
  const marginBottom = parseFloat(section.dataset.marginBottom || "") || DEFAULT_MARGIN;
81
81
  const marginLeft = parseFloat(section.dataset.marginLeft || "") || DEFAULT_MARGIN;
82
- const headerHeight = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_HEIGHT;
83
- const footerHeight = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_HEIGHT;
82
+ const headerDistance = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
83
+ const footerDistance = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
84
84
  return {
85
85
  pageWidth,
86
86
  pageHeight,
@@ -90,8 +90,37 @@ var DocxodusPagination = (() => {
90
90
  marginRight,
91
91
  marginBottom,
92
92
  marginLeft,
93
- headerHeight,
94
- footerHeight
93
+ headerDistance,
94
+ footerDistance,
95
+ headerHeight: headerDistance,
96
+ footerHeight: footerDistance
97
+ };
98
+ }
99
+ var MIN_BODY_BAND_PT = 12;
100
+ function resolvePageBands(dims, headerContentHeight, footerContentHeight) {
101
+ const headerTop = dims.headerDistance;
102
+ const footerBottom = dims.pageHeight - dims.footerDistance;
103
+ const headerReach = headerContentHeight > 0 ? headerTop + headerContentHeight : 0;
104
+ const footerReach = footerContentHeight > 0 ? footerBottom - footerContentHeight : dims.pageHeight;
105
+ let bodyTop = Math.max(dims.marginTop, headerReach);
106
+ let bodyBottom = Math.min(dims.pageHeight - dims.marginBottom, footerReach);
107
+ const shortfall = MIN_BODY_BAND_PT - (bodyBottom - bodyTop);
108
+ if (shortfall > 0) {
109
+ const headerGrowth = bodyTop - dims.marginTop;
110
+ const footerGrowth = dims.pageHeight - dims.marginBottom - bodyBottom;
111
+ const growth = headerGrowth + footerGrowth;
112
+ if (growth > 0) {
113
+ bodyTop -= shortfall * headerGrowth / growth;
114
+ bodyBottom += shortfall * footerGrowth / growth;
115
+ }
116
+ }
117
+ return {
118
+ headerTop,
119
+ headerHeight: Math.max(0, Math.min(headerContentHeight, bodyTop - headerTop)),
120
+ bodyTop,
121
+ bodyHeight: Math.max(0, bodyBottom - bodyTop),
122
+ footerTop: footerBottom - Math.max(0, Math.min(footerContentHeight, footerBottom - bodyBottom)),
123
+ footerHeight: Math.max(0, Math.min(footerContentHeight, footerBottom - bodyBottom))
95
124
  };
96
125
  }
97
126
 
@@ -336,9 +365,9 @@ var DocxodusPagination = (() => {
336
365
  */
337
366
  smallestEffectiveContentHeight(dims, sectionIndex) {
338
367
  return Math.min(
339
- this.getEffectiveHeights(dims, sectionIndex, 1, 1).contentHeight,
340
- this.getEffectiveHeights(dims, sectionIndex, 2, 1).contentHeight,
341
- this.getEffectiveHeights(dims, sectionIndex, 2, 2).contentHeight
368
+ this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
369
+ this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight,
370
+ this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight
342
371
  );
343
372
  }
344
373
  /**
@@ -862,7 +891,7 @@ var DocxodusPagination = (() => {
862
891
  /**
863
892
  * Adds footnotes to a page container, including continuation content.
864
893
  */
865
- addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes) {
894
+ addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes) {
866
895
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
867
896
  if (footnoteIds.length === 0 && !hasContinuation) {
868
897
  return;
@@ -873,12 +902,12 @@ var DocxodusPagination = (() => {
873
902
  const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
874
903
  const maxFootnoteHeight = Math.min(
875
904
  footnoteHeight,
876
- dims.contentHeight * MAX_FOOTNOTE_AREA_RATIO
905
+ bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
877
906
  );
878
907
  const footnotesDiv = document.createElement("div");
879
908
  footnotesDiv.className = `${this.cssPrefix}footnotes`;
880
909
  footnotesDiv.style.position = "absolute";
881
- footnotesDiv.style.bottom = `${dims.marginBottom}pt`;
910
+ footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
882
911
  footnotesDiv.style.left = `${dims.marginLeft}pt`;
883
912
  footnotesDiv.style.width = `${dims.contentWidth}pt`;
884
913
  footnotesDiv.style.boxSizing = "border-box";
@@ -952,50 +981,51 @@ var DocxodusPagination = (() => {
952
981
  return sectionHf.footerDefault;
953
982
  }
954
983
  /**
955
- * Computes effective header, footer, and content heights for a specific page position.
956
- * Uses pre-measured header/footer heights from the registry.
957
- * This method is deterministic - same inputs always produce same outputs.
958
- * This enables lazy loading compatibility since available height can be computed
959
- * for any page position without knowing the page's content.
984
+ * The header, body, and footer bands for one page position.
985
+ *
986
+ * The single owner of "where does anything sit vertically on this page" — placement in
987
+ * {@link createPage}, the body budget the flow loop spends, and the note area's anchor all
988
+ * read it, so those three cannot disagree about where the body ends.
989
+ *
990
+ * Deterministic: it depends only on the section's page setup and the registry's pre-measured
991
+ * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
960
992
  */
961
- getEffectiveHeights(dims, sectionIndex, pageInSection, globalPageNumber) {
993
+ getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
962
994
  const sectionHf = this.hfRegistry.get(sectionIndex);
963
- let headerHeight = dims.marginTop;
964
- if (sectionHf) {
965
- let measuredHeaderHeight;
966
- if (pageInSection === 1 && sectionHf.headerFirstHeight != null) {
967
- measuredHeaderHeight = sectionHf.headerFirstHeight;
968
- } else if (globalPageNumber % 2 === 0 && sectionHf.headerEvenHeight != null) {
969
- measuredHeaderHeight = sectionHf.headerEvenHeight;
970
- } else if (sectionHf.headerDefaultHeight != null) {
971
- measuredHeaderHeight = sectionHf.headerDefaultHeight;
972
- }
973
- if (measuredHeaderHeight != null) {
974
- headerHeight = Math.max(dims.marginTop, measuredHeaderHeight);
975
- }
976
- }
977
- let footerHeight = dims.marginBottom;
978
- if (sectionHf) {
979
- let measuredFooterHeight;
980
- if (pageInSection === 1 && sectionHf.footerFirstHeight != null) {
981
- measuredFooterHeight = sectionHf.footerFirstHeight;
982
- } else if (globalPageNumber % 2 === 0 && sectionHf.footerEvenHeight != null) {
983
- measuredFooterHeight = sectionHf.footerEvenHeight;
984
- } else if (sectionHf.footerDefaultHeight != null) {
985
- measuredFooterHeight = sectionHf.footerDefaultHeight;
986
- }
987
- if (measuredFooterHeight != null) {
988
- footerHeight = Math.max(dims.marginBottom, measuredFooterHeight);
989
- }
990
- }
991
- const headerExpansion = headerHeight - dims.marginTop;
992
- const footerExpansion = footerHeight - dims.marginBottom;
993
- const contentHeight = dims.contentHeight - headerExpansion - footerExpansion;
994
- return { headerHeight, footerHeight, contentHeight };
995
+ return resolvePageBands(
996
+ dims,
997
+ this.selectStoryHeight(
998
+ sectionHf?.headerFirstHeight,
999
+ sectionHf?.headerEvenHeight,
1000
+ sectionHf?.headerDefaultHeight,
1001
+ pageInSection,
1002
+ globalPageNumber
1003
+ ),
1004
+ this.selectStoryHeight(
1005
+ sectionHf?.footerFirstHeight,
1006
+ sectionHf?.footerEvenHeight,
1007
+ sectionHf?.footerDefaultHeight,
1008
+ pageInSection,
1009
+ globalPageNumber
1010
+ )
1011
+ );
1012
+ }
1013
+ /**
1014
+ * The measured height of the running story this page position selects, mirroring
1015
+ * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
1016
+ */
1017
+ selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
1018
+ if (pageInSection === 1 && first != null) return first;
1019
+ if (globalPageNumber % 2 === 0 && even != null) return even;
1020
+ return fallback ?? 0;
995
1021
  }
996
1022
  /**
997
1023
  * Measures the content height of a header or footer element.
998
- * This is needed because headers/footers can contain more content than fits in the margin area.
1024
+ *
1025
+ * This is what tells {@link resolvePageBands} whether the story stays inside its margin or
1026
+ * pushes the body, so it must measure the story ALONE — any padding added here would have to
1027
+ * be added to the rendered band too, and the two drifting apart is exactly how a header
1028
+ * silently starts overlapping body text.
999
1029
  */
1000
1030
  measureHeaderFooterHeight(source, contentWidth) {
1001
1031
  const measureContainer = document.createElement("div");
@@ -1003,7 +1033,6 @@ var DocxodusPagination = (() => {
1003
1033
  measureContainer.style.visibility = "hidden";
1004
1034
  measureContainer.style.width = `${contentWidth}pt`;
1005
1035
  measureContainer.style.left = "-9999px";
1006
- measureContainer.style.paddingBottom = "4pt";
1007
1036
  for (const child of Array.from(source.childNodes)) {
1008
1037
  measureContainer.appendChild(child.cloneNode(true));
1009
1038
  }
@@ -1023,7 +1052,7 @@ var DocxodusPagination = (() => {
1023
1052
  let currentContent = [];
1024
1053
  let pageNumber = startPageNumber;
1025
1054
  let pageInSection = 1;
1026
- let { contentHeight: effectiveContentHeight } = this.getEffectiveHeights(
1055
+ let { bodyHeight: effectiveContentHeight } = this.getPageBands(
1027
1056
  dims,
1028
1057
  sectionIndex,
1029
1058
  pageInSection,
@@ -1058,8 +1087,8 @@ var DocxodusPagination = (() => {
1058
1087
  pageNumber++;
1059
1088
  pageInSection++;
1060
1089
  currentContent = [];
1061
- const newHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
1062
- effectiveContentHeight = newHeights.contentHeight;
1090
+ const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1091
+ effectiveContentHeight = newBands.bodyHeight;
1063
1092
  remainingHeight = effectiveContentHeight;
1064
1093
  prevMarginBottomPt = 0;
1065
1094
  currentFootnoteIds = [];
@@ -1121,7 +1150,7 @@ var DocxodusPagination = (() => {
1121
1150
  ) + additionalChainFootnoteHeight;
1122
1151
  const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
1123
1152
  if (currentChainHeight > currentAvailableHeight) {
1124
- const nextPageHeights = this.getEffectiveHeights(
1153
+ const nextPageBands = this.getPageBands(
1125
1154
  dims,
1126
1155
  sectionIndex,
1127
1156
  pageInSection + 1,
@@ -1137,7 +1166,7 @@ var DocxodusPagination = (() => {
1137
1166
  dims.contentWidth,
1138
1167
  nextPageContinuation
1139
1168
  );
1140
- if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageHeights.contentHeight) {
1169
+ if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
1141
1170
  finishPage();
1142
1171
  }
1143
1172
  }
@@ -1306,6 +1335,161 @@ var DocxodusPagination = (() => {
1306
1335
  if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
1307
1336
  }
1308
1337
  }
1338
+ /**
1339
+ * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
1340
+ *
1341
+ * The converter deliberately carries the OOXML bases and offsets as data instead of flattening
1342
+ * them into CSS: page/margin/column bases belong to the page, while paragraph/line/character
1343
+ * bases belong to the laid-out anchor. Once both coordinate systems exist, promote the object
1344
+ * from the clipped text column into the page box and position it in one shared point space.
1345
+ */
1346
+ positionDrawingAnchors(pageBox, contentArea, dims, pageNumber) {
1347
+ const anchors = Array.from(
1348
+ contentArea.querySelectorAll('[data-docx-drawing-anchor="true"]')
1349
+ );
1350
+ if (anchors.length === 0) return;
1351
+ const pageRect = pageBox.getBoundingClientRect();
1352
+ const pixelsPerPoint = pageRect.width / dims.pageWidth;
1353
+ if (!(pixelsPerPoint > 0)) return;
1354
+ for (const anchor of anchors) {
1355
+ const staticRect = anchor.getBoundingClientRect();
1356
+ const paragraph = anchor.closest("p, h1, h2, h3, h4, h5, h6");
1357
+ const paragraphRect = paragraph?.getBoundingClientRect() ?? staticRect;
1358
+ const toPageX = (x) => (x - pageRect.left) / pixelsPerPoint;
1359
+ const toPageY = (y) => (y - pageRect.top) / pixelsPerPoint;
1360
+ const context = {
1361
+ staticLeft: toPageX(staticRect.left),
1362
+ staticTop: toPageY(staticRect.top),
1363
+ lineHeight: paragraph ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
1364
+ paragraphLeft: toPageX(paragraphRect.left),
1365
+ paragraphTop: toPageY(paragraphRect.top),
1366
+ paragraphWidth: paragraphRect.width / pixelsPerPoint,
1367
+ paragraphHeight: paragraphRect.height / pixelsPerPoint
1368
+ };
1369
+ const widthReference = this.horizontalAnchorReference(
1370
+ anchor.getAttribute("data-docx-anchor-width-relative") ?? "margin",
1371
+ dims,
1372
+ pageNumber,
1373
+ context
1374
+ );
1375
+ const widthPercent = this.anchorNumber(anchor, "width-percent");
1376
+ if (widthPercent !== void 0) {
1377
+ anchor.style.width = `${widthReference.size * widthPercent / 100}pt`;
1378
+ }
1379
+ const heightReference = this.verticalAnchorReference(
1380
+ anchor.getAttribute("data-docx-anchor-height-relative") ?? "margin",
1381
+ dims,
1382
+ pageNumber,
1383
+ context
1384
+ );
1385
+ const heightPercent = this.anchorNumber(anchor, "height-percent");
1386
+ if (heightPercent !== void 0 && anchor.dataset.docxAnchorAutofit !== "true") {
1387
+ anchor.style.height = `${heightReference.size * heightPercent / 100}pt`;
1388
+ }
1389
+ const sizedRect = anchor.getBoundingClientRect();
1390
+ const width = sizedRect.width / pixelsPerPoint;
1391
+ const height = sizedRect.height / pixelsPerPoint;
1392
+ const horizontal = this.horizontalAnchorReference(
1393
+ anchor.getAttribute("data-docx-anchor-h-relative") ?? "column",
1394
+ dims,
1395
+ pageNumber,
1396
+ context
1397
+ );
1398
+ const vertical = this.verticalAnchorReference(
1399
+ anchor.getAttribute("data-docx-anchor-v-relative") ?? "paragraph",
1400
+ dims,
1401
+ pageNumber,
1402
+ context
1403
+ );
1404
+ const left = this.resolveAnchorAxis(
1405
+ horizontal,
1406
+ width,
1407
+ anchor.getAttribute("data-docx-anchor-h-align"),
1408
+ this.anchorNumber(anchor, "h-offset"),
1409
+ pageNumber
1410
+ );
1411
+ const top = this.resolveAnchorAxis(
1412
+ vertical,
1413
+ height,
1414
+ anchor.getAttribute("data-docx-anchor-v-align"),
1415
+ this.anchorNumber(anchor, "v-offset"),
1416
+ pageNumber
1417
+ );
1418
+ pageBox.appendChild(anchor);
1419
+ anchor.style.position = "absolute";
1420
+ anchor.style.left = `${left}pt`;
1421
+ anchor.style.top = `${top}pt`;
1422
+ anchor.style.margin = "0";
1423
+ }
1424
+ }
1425
+ anchorNumber(anchor, suffix) {
1426
+ const raw = anchor.getAttribute(`data-docx-anchor-${suffix}`);
1427
+ if (raw === null) return void 0;
1428
+ const value = Number(raw);
1429
+ return Number.isFinite(value) ? value : void 0;
1430
+ }
1431
+ horizontalAnchorReference(relativeFrom, dims, pageNumber, context) {
1432
+ const leftMargin = { start: 0, size: dims.marginLeft };
1433
+ const rightMargin = {
1434
+ start: dims.marginLeft + dims.contentWidth,
1435
+ size: dims.marginRight
1436
+ };
1437
+ switch (relativeFrom) {
1438
+ case "page":
1439
+ return { start: 0, size: dims.pageWidth };
1440
+ case "character":
1441
+ return { start: context.staticLeft, size: 0 };
1442
+ case "leftMargin":
1443
+ return leftMargin;
1444
+ case "rightMargin":
1445
+ return rightMargin;
1446
+ case "insideMargin":
1447
+ return pageNumber % 2 === 1 ? leftMargin : rightMargin;
1448
+ case "outsideMargin":
1449
+ return pageNumber % 2 === 1 ? rightMargin : leftMargin;
1450
+ case "paragraph":
1451
+ return { start: context.paragraphLeft, size: context.paragraphWidth };
1452
+ case "column":
1453
+ case "margin":
1454
+ default:
1455
+ return { start: dims.marginLeft, size: dims.contentWidth };
1456
+ }
1457
+ }
1458
+ verticalAnchorReference(relativeFrom, dims, pageNumber, context) {
1459
+ const topMargin = { start: 0, size: dims.marginTop };
1460
+ const bottomMargin = {
1461
+ start: dims.marginTop + dims.contentHeight,
1462
+ size: dims.marginBottom
1463
+ };
1464
+ switch (relativeFrom) {
1465
+ case "page":
1466
+ return { start: 0, size: dims.pageHeight };
1467
+ case "paragraph":
1468
+ return { start: context.paragraphTop, size: context.paragraphHeight };
1469
+ case "line":
1470
+ return { start: context.staticTop, size: context.lineHeight };
1471
+ case "topMargin":
1472
+ return topMargin;
1473
+ case "bottomMargin":
1474
+ return bottomMargin;
1475
+ case "insideMargin":
1476
+ return pageNumber % 2 === 1 ? topMargin : bottomMargin;
1477
+ case "outsideMargin":
1478
+ return pageNumber % 2 === 1 ? bottomMargin : topMargin;
1479
+ case "margin":
1480
+ default:
1481
+ return { start: dims.marginTop, size: dims.contentHeight };
1482
+ }
1483
+ }
1484
+ resolveAnchorAxis(reference, objectSize, alignment, offset, pageNumber) {
1485
+ if (offset !== void 0) return reference.start + offset;
1486
+ let fraction = 0;
1487
+ if (alignment === "center") fraction = 0.5;
1488
+ else if (alignment === "right" || alignment === "bottom") fraction = 1;
1489
+ else if (alignment === "inside") fraction = pageNumber % 2 === 1 ? 0 : 1;
1490
+ else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
1491
+ return reference.start + (reference.size - objectSize) * fraction;
1492
+ }
1309
1493
  /**
1310
1494
  * Creates a page container element.
1311
1495
  */
@@ -1332,22 +1516,22 @@ var DocxodusPagination = (() => {
1332
1516
  pageBox.dataset.pageNumber = String(pageNumber);
1333
1517
  pageBox.dataset.sectionIndex = String(sectionIndex);
1334
1518
  pageBox.dataset.pageInSection = String(pageInSection);
1335
- const effectiveHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
1519
+ const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1336
1520
  const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
1337
1521
  if (headerSource) {
1338
1522
  const headerDiv = document.createElement("div");
1339
1523
  headerDiv.className = `${this.cssPrefix}header`;
1340
1524
  headerDiv.style.position = "absolute";
1341
- headerDiv.style.top = "0";
1525
+ headerDiv.style.top = `${bands.headerTop}pt`;
1526
+ headerDiv.style.bottom = "auto";
1342
1527
  headerDiv.style.left = `${dims.marginLeft}pt`;
1343
1528
  headerDiv.style.width = `${dims.contentWidth}pt`;
1344
- headerDiv.style.height = `${effectiveHeights.headerHeight}pt`;
1529
+ headerDiv.style.height = `${bands.headerHeight}pt`;
1345
1530
  headerDiv.style.overflow = "hidden";
1346
1531
  headerDiv.style.boxSizing = "border-box";
1347
1532
  headerDiv.style.display = "flex";
1348
1533
  headerDiv.style.flexDirection = "column";
1349
- headerDiv.style.justifyContent = "flex-end";
1350
- headerDiv.style.paddingBottom = "4pt";
1534
+ headerDiv.style.justifyContent = "flex-start";
1351
1535
  for (const child of Array.from(headerSource.childNodes)) {
1352
1536
  const clonedheaderDiv = child.cloneNode(true);
1353
1537
  if (clonedheaderDiv.nodeType === 1) this.makeClonedStoryInert(clonedheaderDiv);
@@ -1355,8 +1539,8 @@ var DocxodusPagination = (() => {
1355
1539
  }
1356
1540
  pageBox.appendChild(headerDiv);
1357
1541
  }
1358
- const contentAreaTop = effectiveHeights.headerHeight;
1359
- const contentAreaHeight = effectiveHeights.contentHeight;
1542
+ const contentAreaTop = bands.bodyTop;
1543
+ const contentAreaHeight = bands.bodyHeight;
1360
1544
  const contentArea = document.createElement("div");
1361
1545
  contentArea.className = `${this.cssPrefix}content`;
1362
1546
  contentArea.style.position = "absolute";
@@ -1371,23 +1555,23 @@ var DocxodusPagination = (() => {
1371
1555
  pageBox.appendChild(contentArea);
1372
1556
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
1373
1557
  if (footnoteIds.length > 0 || hasContinuation) {
1374
- this.addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes);
1558
+ this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
1375
1559
  }
1376
1560
  const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
1377
1561
  if (footerSource) {
1378
1562
  const footerDiv = document.createElement("div");
1379
1563
  footerDiv.className = `${this.cssPrefix}footer`;
1380
1564
  footerDiv.style.position = "absolute";
1381
- footerDiv.style.bottom = "0";
1565
+ footerDiv.style.top = "auto";
1566
+ footerDiv.style.bottom = `${dims.footerDistance}pt`;
1382
1567
  footerDiv.style.left = `${dims.marginLeft}pt`;
1383
1568
  footerDiv.style.width = `${dims.contentWidth}pt`;
1384
- footerDiv.style.height = `${effectiveHeights.footerHeight}pt`;
1569
+ footerDiv.style.height = `${bands.footerHeight}pt`;
1385
1570
  footerDiv.style.overflow = "hidden";
1386
1571
  footerDiv.style.boxSizing = "border-box";
1387
1572
  footerDiv.style.display = "flex";
1388
1573
  footerDiv.style.flexDirection = "column";
1389
- footerDiv.style.justifyContent = "flex-start";
1390
- footerDiv.style.paddingTop = "4pt";
1574
+ footerDiv.style.justifyContent = "flex-end";
1391
1575
  for (const child of Array.from(footerSource.childNodes)) {
1392
1576
  const clonedfooterDiv = child.cloneNode(true);
1393
1577
  if (clonedfooterDiv.nodeType === 1) this.makeClonedStoryInert(clonedfooterDiv);
@@ -1402,6 +1586,7 @@ var DocxodusPagination = (() => {
1402
1586
  pageBox.appendChild(pageNum);
1403
1587
  }
1404
1588
  this.containerElement.appendChild(pageBox);
1589
+ this.positionDrawingAnchors(pageBox, contentArea, dims, pageNumber);
1405
1590
  const notesEl = pageBox.querySelector(`.${this.cssPrefix}footnotes`);
1406
1591
  if (notesEl) {
1407
1592
  const notesHeightPt = pxToPt(notesEl.getBoundingClientRect().height);
@@ -5,7 +5,7 @@
5
5
  * and flows it across fixed-size page containers based on document dimensions.
6
6
  */
7
7
  import type { PageDimensions } from "./page-geometry.js";
8
- export type { PageDimensions } from "./page-geometry.js";
8
+ export type { PageBands, PageDimensions } from "./page-geometry.js";
9
9
  /**
10
10
  * Headers and footers for a specific section.
11
11
  */
@@ -283,16 +283,28 @@ export declare class PaginationEngine {
283
283
  */
284
284
  private selectFooter;
285
285
  /**
286
- * Computes effective header, footer, and content heights for a specific page position.
287
- * Uses pre-measured header/footer heights from the registry.
288
- * This method is deterministic - same inputs always produce same outputs.
289
- * This enables lazy loading compatibility since available height can be computed
290
- * for any page position without knowing the page's content.
286
+ * The header, body, and footer bands for one page position.
287
+ *
288
+ * The single owner of "where does anything sit vertically on this page" — placement in
289
+ * {@link createPage}, the body budget the flow loop spends, and the note area's anchor all
290
+ * read it, so those three cannot disagree about where the body ends.
291
+ *
292
+ * Deterministic: it depends only on the section's page setup and the registry's pre-measured
293
+ * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
294
+ */
295
+ private getPageBands;
296
+ /**
297
+ * The measured height of the running story this page position selects, mirroring
298
+ * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
291
299
  */
292
- private getEffectiveHeights;
300
+ private selectStoryHeight;
293
301
  /**
294
302
  * Measures the content height of a header or footer element.
295
- * This is needed because headers/footers can contain more content than fits in the margin area.
303
+ *
304
+ * This is what tells {@link resolvePageBands} whether the story stays inside its margin or
305
+ * pushes the body, so it must measure the story ALONE — any padding added here would have to
306
+ * be added to the rendered band too, and the two drifting apart is exactly how a header
307
+ * silently starts overlapping body text.
296
308
  */
297
309
  private measureHeaderFooterHeight;
298
310
  /**
@@ -315,6 +327,19 @@ export declare class PaginationEngine {
315
327
  * cloned node cannot be uniquely addressed.
316
328
  */
317
329
  private makeClonedStoryInert;
330
+ /**
331
+ * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
332
+ *
333
+ * The converter deliberately carries the OOXML bases and offsets as data instead of flattening
334
+ * them into CSS: page/margin/column bases belong to the page, while paragraph/line/character
335
+ * bases belong to the laid-out anchor. Once both coordinate systems exist, promote the object
336
+ * from the clipped text column into the page box and position it in one shared point space.
337
+ */
338
+ private positionDrawingAnchors;
339
+ private anchorNumber;
340
+ private horizontalAnchorReference;
341
+ private verticalAnchorReference;
342
+ private resolveAnchorAxis;
318
343
  /**
319
344
  * Creates a page container element.
320
345
  */