Skip to content

fix(docx): take a panel's top border inside it when no space above can, and hold the body at a margin a band reaches past - #799

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-panel-border-height
Oct 1, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-panel-border-height

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Why

MerchantInvoice is one page in the PDF and two in Word, its footer row alone on the second page. The page sets that row down to its 3.4pt bottom margin, so any growth above it pushes the row over. Two things grew it.

  • The payment panel's top border. The panel is a section with a fill and a 0.875pt stroke, no padding, opening a cell of the settlement row. Word draws a cell's top border above its content, unless the cell's top margin is wider. The export takes that border from the space above a panel, but a panel opening a cell has none. So every line inside stood 0.9pt low, and the panel stood 0.8pt taller than the page.
  • The footer band. It reaches 9.8pt from the page's edge, past the 3.4pt margin. The page lets a band overlap the body; Word moves the body clear of a header or footer taller than its margin.

What changed

  • writePanelPiece / takeTheTopBorderInside.
    • topNotTaken is what of the top border the space above could not give.
    • Word starts the cell's content at the larger of the border and the cell's top margin, and the page starts it the padding below the edge. So the content stands topNotTaken − padding.top low: a padding as wide as the border holds it.
    • That amount comes out of the space above the panel's first paragraph, where it has some.
    • Where that amount is positive, the row's held height is also less the borders Word draws outside it: topNotTaken plus the bottom border, less the heavier of the two, which holdRowAtLeast already takes off for LibreOffice. A padding that holds the border leaves the height as it was: Word draws the border inside the margin.
    • A panel whose cell margin is wider than its border (CompactMono, 9pt of padding round a 0.375pt border) gives nothing: Word draws the border inside the margin.
  • placeBand / reachesPastTheMargin. A text band alone of its kind that reaches past the page margin, by more than a twentieth of a point, writes that margin negative. In OOXML a negative w:top/w:bottom holds the body at the margin whatever the header or footer reaches, which is what the page does. A margin of none is written as a twentieth of a point. A framed band (one sharing its kind with another band or a page zone) is left as it was. The report note says so, and that LibreOffice still moves the body clear. The grace before a band counts as past the margin is now a twentieth of a point rather than a point, so a band up to a point past it is written negative and noted too.
  • Docs. CHANGELOG.md, the panel paragraph and the header/footer paragraph of docs/recipes/docx-export.md, and the panel and header/footer rows of docs/architecture/backend-capability-matrix.md.

Verification

  • Full reactor gate: ./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-qa,:graph-compose-coverage -am gives BUILD SUCCESS (1791 + 127 tests).
    • render-docx runs 736 tests.
    • After install, examples run 93 green.
    • The knowledge checks pass.
  • DocxPanelHeightTest (+3).
    • A panel with a 1pt border opening a row's cell, against the same panel with no border. The space over its heading is 1pt less, and its held height 1pt less than the border allowance already in it.
    • A padded panel opening a cell keeps the space over its first line and its held height.
    • A panel ruled above only, opening a cell, gives up the space over its heading and keeps its held height, less only LibreOffice's allowance for the border.
  • DocxTextBandTest (+5).
    • A header reaching past the 30pt margin writes the top margin as −600 twips.
    • A footer reaching past it writes the bottom margin as −600 twips.
    • A footer within the margin leaves it at 600.
    • Two footers, framed, leave it at 600.
    • A header past a margin of none writes it as −1 twip.
  • Each rule fails its own test when reverted:
    • Not taking the border inside fails the bordered-panel test.
    • Ignoring the padding fails the padded-panel test.
    • Taking the height off where the padding holds the border fails the padded-panel test.
    • Taking the top border's part off the height regardless of the bottom border fails the ruled-above test.
    • Not writing the margin negative fails the header test.
    • Writing a margin of none as none fails the no-margin test.
    • Not skipping a framed band fails the framed test.
  • Template corpus (62 documents), against 2.5-dev: four documents change.
    • MerchantInvoice: one page in Word, from two. p90 drift in Word 1.64pt → 0.76; in LibreOffice 1.56pt → 0.67.
    • ObsidianInvoice: p90 drift in Word 1.59pt → 0.61. In LibreOffice, lines more than 2pt off 7 → 0.
    • PlatformInvoice: p90 drift in Word 0.65pt → 0.40.
    • SubscriptionInvoice: its footer reaches past its margin too. It stays one page, its drift unchanged.

Tried and dropped

  • A frame (w:framePr) round the footer band. Word still moved the body clear of the frame, and the document stayed two pages.
  • A footer band in a text box anchored to the page. It fits the page in Word too, but a negative margin says the same thing in one attribute.

Known limits

  • LibreOffice reads a negative page margin as positive. MerchantInvoice is still two pages there, as it was.
  • A panel whose height its held row sets stands its border's width shorter in LibreOffice. LibreOffice adds the border to a row's height once, Word twice.
  • A panel opening a cell with a table, or with a first line with no space above it, still stands its border's width low.
  • A page zone (DocumentPageZone) reaching past the margin is neither written negative nor reported, as before.

Lane: shared-engine (render-docx). No public API change.

…n, and hold the body at a margin a band reaches past
…it only where its padding cannot hold them, and leave a framed band's margin as it is
@DemchaAV
DemchaAV merged commit f424f8b into 2.5-dev Oct 1, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-panel-border-height branch October 1, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant