Skip to content

fix(docx): keep a table's and a row's own margins - #831

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-violet-grid
Oct 3, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-violet-grid

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Why

The DOCX export dropped a table's and a row's own margins.

  • Left margin. indentTable writes w:tblInd from insetLeft alone, and nothing added the table's or the row's own margin.left to it. Every table or row with a left margin started that far left in Word: PaymentsInvoice's bank details stood 36.7pt left of the page's.
  • Top margin under a band. VioletGrid sets each education entry in a table held 51.3pt clear of a badge drawn beside it, and 2.3pt down. A table opening a band's layer never took the band's resumed space, so newTable's own resumeHere() later overwrote the 2.3pt the table owed. At the top of a cell nothing held that space either. In Word the lines started under the badge and stood 2.4pt high.

What changed

The changes are in DocxSemanticBackend.writeTableWithItsOwnSpacing, except the last one:

  • Side margins. A table or a row adds its own margin.left / margin.right to insetLeft / insetRight while it is written, restored in a finally block, so indentTable carries the margin into w:tblInd.
    • The one exception is a block a row placed as one of its columns (leftMarginInCell). The column already starts past that margin, which is also why writeParagraph and writeList skip it.
  • Top margin under a band. A TableNode written while a band's or a column layer's resumeSpacing is pending takes it first (resumeHere()), then owes its own top margin.
    • At the top of a cell that space is held by a hairline line (holdTheSpaceAboveATable), as it already was for a row.
    • A RowNode does not take the resume: the band measures it to the first block inside the row, and the row's cells write what stands above that block. Letting rows take it moved the whole of PaymentsInvoice 10pt down.
    • A table opening a cell outside a band still holds nothing above it. Holding that space moved ObsidianInvoice's line items 6pt.
  • Width in a cell. nestedTableWidth() is the cell's width less what the table is now held in by. It was the whole cell, so a nested table indented by its margin, sized from it or given the editor's slack in it, ran that margin past the cell's right edge.
  • The Javadoc, docs/recipes/docx-export.md and the CHANGELOG state both rules.

Verification

  • Word corpus. scripts/docx-visual/word-fidelity.ps1 (Word 16.0.20430) → BUILD SUCCESS, with the baseline rewritten by -Update.
    • Only VioletGrid's three education lines moved vertically: −2.47 / −2.43 / −2.44pt → −0.07 / −0.03 / −0.04pt. Its lines more than 2pt off fall from 5 to 2.
    • The 2 lines left are a wrap difference ("sitemaps, user flows, card / sorting…", +7.5pt) and are not touched here.
  • Horizontal check. The corpus gate measures vertical drift only, so I compared the Word PDFs word by word before and after the change:
    • PaymentsInvoice bank details (the CardFields row, left margin px(65) = 36.7pt): 29 words moved, all of them nearer the PDF (−36.7pt → +0.1pt).
    • VioletGrid education lines (BADGE_COLUMN = 51.3pt): 19 words moved, all nearer (−51.4pt → +0.3pt at most).
    • PlatformInvoice footer (the FooterIdentity row, left margin px(1) = 0.56pt): 13 words moved, all 0.6pt nearer.
    • No other corpus DOCX changed.
  • LibreOffice (Windows). -Dgraphcompose.docxFidelity=libreoffice failed only on the same three VioletGrid lines, now +2.4pt further: 19.08 → 21.48pt. LibreOffice already sets that CV on two pages, about 19pt off, so the right 2.4pt adds to an older offset. I rewrote the baseline with update=true. The Linux baseline is this PR's CI artifact docx-fidelity-37139222719, which moved the same three lines and nothing else.
  • New DocxTableMarginsTest, 7 tests:
    • a table's and a row's left margin become w:tblInd;
    • a table beside a badge in a band keeps its indent and its 2.3pt above;
    • the same table opening a row's cell gets a hairline carrying 2.3pt above it;
    • a table a hand-built RowNode places as a column is not indented twice (RowBuilder refuses a table, the node API does not);
    • a table indented in a cell ends inside the cell;
    • a table opening a cell outside a band holds no hairline — this one guards the resumed condition from widening.
    • Sabotage: dropping the margin inset fails 4, dropping the resumeHere() call fails both band tests, dropping the hold for a resumed table fails 1 (and widening it to every table fails the no-hairline test), dropping the column guard fails 1, and the old nestedTableWidth() fails the width test.
  • 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 → BUILD SUCCESS.
  • Examples. CommittedAssetDriftTest in examples → 3/3 green, so no committed preview moved.

Known limits

  • An empty table opening a band's layer takes the resumed space and may leave a hairline with no table under it; the layout measures the gap to that table, so the space is right.
  • Negative left margin in a cell. A table with a negative left margin in a cell is still held at the cell's edge by indentTable, while a row in the same place hangs through writeRow.

Lane: shared-engine (render-docx) — DOCX export geometry only. No public API change.

@DemchaAV
DemchaAV merged commit 265e2e8 into 2.5-dev Oct 3, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-violet-grid branch October 3, 2026 18:11
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.

2 participants