Skip to content

feat(docx): carry whatever a composed table cell is built from - #706

Merged
DemchaAV merged 2 commits into
2.5-devfrom
feature/docx-composed-cells
Sep 22, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
feature/docx-composed-cells

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

DocumentTableCell.node(...) lets a cell hold anything the document can hold, and the
export wrote paragraphs out of it and nothing else. A cell built from an image, a list or a
table came out empty — not wrong, empty: content the page draws simply missing from the
file, with one line in a log to say so. A table is where a document keeps the things a
reader counts, which makes it the worst place to lose something quietly.

What changed

The cell is a destination, not a place with its own writer. It had a dispatcher that had
learned about paragraphs and the wrappers a paragraph sits in, and that is exactly the set
it could carry. newBodyParagraph now points at the cell being filled and writeNode does
the rest, so everything writable anywhere is writable there. Nested tables work without
anything in the cell path knowing how a table is written.

  • A nested table is a real w:tbl, registered through XWPFTableCell.insertTable so the
    model and the XML agree — getTables() is unmodifiable and throws rather than letting
    them drift apart — and followed by the paragraph Word requires a cell to end with. A cell
    ending in a table is malformed and Word refuses the file rather than showing it.
  • The destination is restored rather than cleared after a cell, because a cell can hold a
    table whose cells hold content of their own.

A defect in reading columns back from the layout, uncovered by the above. A table whose
cell is built from another table emits that inner table's rows under the owner's path,
so the fragments at one path are not all one table's. DocxLayoutMetrics.tableColumns
merged their boundaries: a two-column table came out with a three-column grid — 45.3 / 32.7
/ 76.9 where 78.0 / 76.9 belonged — and Word placed every edge exactly where it was told.
Only rows that span the table's own placed width are read now, and the derived count must
equal the one TableGrid resolves; a wrong grid is worse than none.

A nested table is given the width of the column it sits in, less the 5.4pt margins Word
keeps inside each cell edge. That is not the width the page gives it — the layout reports a
composed cell's content under the owner's path, so which measured row belongs to which
nested table cannot be told apart there. The choice was not between exact and approximate
but between approximate and unreadable: with no width at all Word squeezes a nested table to
about one character a line, confirmed by opening the export in Word 16.0 before and after.

Verification

./mvnw -B -ntp clean verify over the seven-module gate → BUILD SUCCESS, 1784 tests.
./mvnw -B -ntp test -f examples/pom.xml93 tests, BUILD SUCCESS; no committed preview
drifted, which matters here because the column-reading fix applies to every table, not only
to the ones with composed cells.

render-docx goes from 175 to 182 tests. DocxComposedCellTest covers an image, a list, a
nested table, the paragraph that has to follow it, a transparent wrapper inside the cell, the
nested table's width, and the destination returning to the body afterwards.

Checked by opening the export in Word 16.0: the nested table renders as a table with its rows
and the list as bullets, where the same document previously had two empty cells.

Lane: canonical — document.backend.semantic.docx only; no engine, layout or pagination
internals are touched.

DocumentTableCell.node(...) lets a cell hold anything the document can
hold, and the export wrote paragraphs out of it and nothing else: a cell
built from an image, a list or a table came out empty. Not wrong --
empty, with content the page draws simply missing from the file and one
line in a log to say so. A table is where a document keeps the things a
reader counts, which makes it the worst place to lose something quietly.

The fix is not a second writer that knows about cells. The cell is a
destination now: newBodyParagraph points at it and writeNode does the
rest, so everything that can be written anywhere is written here too. A
nested table is a real w:tbl followed by the paragraph Word requires a
cell to end with, registered through insertTable so the model and the
XML agree -- getTables() is unmodifiable, and adding to it throws rather
than letting them drift apart.

This uncovered a defect in reading columns back from the layout. A table
whose cell is built from another table emits that inner table's rows
under the OWNER's path, so the fragments at one path are not all one
table's: a two-column table came out with a three-column grid, 45.3 /
32.7 / 76.9 where 78.0 / 76.9 belonged, and Word placed every edge
exactly where it was told. Only rows that span the table are read now,
and the derived count must match the one the table resolves -- a wrong
grid is worse than none.

A nested table is given the width of the column it sits in, less the
margins Word keeps inside a cell. That is not the width the page gives
it: the layout reports a composed cell's content under the owner's path,
so which measured row belongs to which nested table cannot be told apart
there. The choice was not between exact and approximate but between
approximate and unreadable -- with no width at all, Word squeezes a
nested table to about one character a line, confirmed by opening the
export in Word 16.0 before and after.

Tests: seven over the exported package -- an image, a list, a nested
table, the paragraph that has to follow it, a transparent wrapper, the
width, and the destination returning to the body afterwards.

Verification: ./mvnw -B -ntp clean verify over the seven-module gate,
1784 tests, BUILD SUCCESS; render-docx 175 to 182; the examples module
separately, 93 tests, BUILD SUCCESS, no committed preview drifted.
The capability matrix said a composed cell writes paragraphs only and one built from an image or a list lands empty, which is no longer what happens. The recipe listed tables and said nothing about what a cell can be built from.
double twips = 0;
int last = Math.min(placement.column() + placement.colSpan(), grid.sizeOfGridColArray());
for (int index = placement.column(); index < last; index++) {
twips += Long.parseLong(String.valueOf(grid.getGridColArray(index).getW()));
@DemchaAV
DemchaAV merged commit 160d9d5 into 2.5-dev Sep 22, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the feature/docx-composed-cells branch September 22, 2026 13:14
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