Skip to content

fix(docx): give a table cell the padding the document laid it out with - #711

Merged
DemchaAV merged 1 commit into
2.5-devfrom
feature/docx-cell-padding
Sep 22, 2026
Merged

DemchaAV merged 1 commit into
2.5-devfrom
feature/docx-cell-padding

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

A table cell never stated its padding, so Word used its own: 5.4pt at each side and
nothing above or below. A row's height in Word is its content's box, so every row came
out shorter than the page draws it.

Measured on the probe corpus through LibreOffice, against the reference PDF — each row of
the five-row billing table 8.1pt short, and the table's last row 40pt above where the page
puts it:

317.0 → 262.2  −9.7  Item
337.9 → 275.0  −8.1  Platform subscription
358.9 → 287.9  −8.1  Priority support
379.8 → 300.7  −8.1  Onboarding workshop
400.8 → 313.6  −8.1  Additional storage

What changed

The padding a cell resolves to is written as w:tcMar — the table's default, then the
column's, then the row's, then the cell's own, through the same resolveCellValue cascade
the fill and the stroke already use.

All four sides, and written even when zero. Word's default is not zero, so a table that
asked for no padding would otherwise export with Word's side margins and read wider than it
is.

A table that states nothing is written with the padding the engine lays it out with
(4pt), not Word's. The engine's own default is internal and cannot be read from the backend,
so DocxCellPaddingTest pins the two together: if the layout's default moves, that test
fails rather than the export quietly drawing rows of a height nothing asked for.

A nested table's usable width is measured against the margins the cell now states, read
back from the file the same way the column widths already are, rather than against Word's
5.4pt.

Verification

./mvnw -B -ntp clean verify over the eight-module gate → BUILD SUCCESS (core 815,
render-pdf 337, render-docx 222, testing 5, render-pptx 140, templates 127, qa 1784).
./mvnw -B -ntp test -f examples/pom.xml → 93 tests, BUILD SUCCESS.

Measured again on the same corpus, same editor, after the change:

294.2 → 265.1  +0.0  Billing              (the gap fix from #710, in the editor)
317.0 → 282.2  −5.7  Item
337.9 → 303.0  −0.1  Platform subscription
358.9 → 323.9  −0.1  Priority support
379.8 → 344.7  −0.1  Onboarding workshop
400.8 → 365.6  −0.1  Additional storage

Row pitch is 20.9pt against the page's 21.0. Accumulated drift at the foot of the table goes
from −87.2 to −35.2.

render-docx goes from 217 to 222 tests. DocxCellPaddingTest covers a stated padding on
all four sides, the engine default for a table that states nothing, the guard tying that
default to the engine's own, the cascade (a row's padding beating the table's, a cell's
beating the row's), and a table asking for no padding getting none rather than Word's.

Three existing tests changed, each because the behaviour under them did:
DocxTablePaintTest and DocxInlineBackgroundTest asserted that an unpainted cell has no
w:tcPr — every cell now carries one, because every cell states its margins, so they ask
whether it has shading instead. DocxComposedCellTest pinned a nested table's width as
"the column less 2 × 108 twips" (Word's 5.4pt); it is now "less 2 × 80" (the cell's own 4pt).

The committed word-export-companion.docx preview is regenerated — the export writes cell
margins now, so its bytes moved.

Lane: canonical — document.backend.semantic.docx only. No public API change.

Still open on the same page, each its own change: a row loses its own vertical padding
(−14, twice — a row is a table, and Word has no space above one), and the table's header row
is 5.7pt short where its body rows are 0.1.

A cell never stated its padding, so Word used its own: 5.4pt at each side
and nothing above or below. A row's height in Word is its content's box,
so every row came out shorter than the page draws it — measured on the
probe corpus through LibreOffice, each row of a five-row table sat 8.1pt
short and the last row 40pt above where the page puts it.

The padding a cell resolves to — the table's default, then the column's,
then the row's, then the cell's own, the same cascade the paint already
used — is written as w:tcMar. The same table's rows now land within 0.1pt
of the page.

All four sides are written, and written even when zero, because Word's
default is not zero: a table asking for no padding would otherwise export
with Word's side margins. A table that states nothing is written with the
padding the engine lays it out with, and DocxCellPaddingTest pins that
constant to the engine's own so it cannot drift unnoticed.

A nested table's usable width is measured against the margins the cell
now states rather than against Word's default, read back from the file
the same way the column widths are.
private static double marginPoints(CTTblWidth margin) {
return margin == null || margin.getW() == null
? WORD_DEFAULT_CELL_MARGIN_POINTS
: Long.parseLong(String.valueOf(margin.getW())) / POINT_TO_TWIP;
private static long width(CTTblWidth margin) {
return margin == null || margin.getW() == null
? -1
: Long.parseLong(String.valueOf(margin.getW()));
@DemchaAV
DemchaAV merged commit 0dee58e into 2.5-dev Sep 22, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the feature/docx-cell-padding branch September 22, 2026 22:41
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