From 9c3723f2239c797ff72d6a0d816f9693228248e6 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Tue, 22 Sep 2026 22:46:29 +0100 Subject: [PATCH] fix(docx): give a table cell the padding the document laid it out with MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 14 ++ .../examples/word-export-companion.docx | Bin 9215 -> 9298 bytes docs/recipes/docx-export.md | 2 +- .../semantic/docx/DocxSemanticBackend.java | 78 ++++++++++- .../semantic/docx/DocxCellPaddingTest.java | 125 ++++++++++++++++++ .../semantic/docx/DocxComposedCellTest.java | 5 +- .../docx/DocxInlineBackgroundTest.java | 2 +- .../semantic/docx/DocxTablePaintTest.java | 8 +- 8 files changed, 221 insertions(+), 13 deletions(-) create mode 100644 render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxCellPaddingTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a8fb0316e..ee77fbc10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A table cell keeps the space the document put inside its edges.** It was never written, + 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 table's 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 — is now + written as `w:tcMar`, and the same table's rows land within 0.1pt of the page. +

+ All four sides are written, and written even when they are zero, because Word's default is + not zero: 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 no padding at all is written with + the padding the engine lays it out with rather than with Word's, and a test pins the two + together so the export cannot quietly draw rows of a height nothing asked for. + - **A gap between two blocks in an exported Word file is one gap.** It was written from both sides — `w:after` on the block above and `w:before` on the one below — which is the same distance only in an editor that adds them. LibreOffice takes the larger: measured on the diff --git a/assets/readme/examples/word-export-companion.docx b/assets/readme/examples/word-export-companion.docx index 2facc29b103d4257db1d55df28b19ec2e176f7b8..52c96b0bd6fd36ede3d4c4ec538dec83bfc587f9 100644 GIT binary patch delta 2615 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 mmXbVBlv=9<A table's rows came out shorter than the page draws them: measured on the probe + * corpus, every row of a five-row table was 8.1pt short, because the cell padding the + * engine lays out with was never written and Word used its own — 5.4pt at each side and + * nothing above or below. Word holds this natively as {@code w:tcMar}, so it + * is a mapping rather than an approximation.

+ * + *

All four sides are written, and written even when they are zero, because Word's + * default is not zero: a table that asked for no padding would otherwise export with + * Word's side margins and read wider than it is. The vertical pair is what a reader + * sees as the row's height, since Word grows a row to fit its content and this is part + * of that content's box.

+ */ + private static void applyCellPadding(XWPFTableCell cell, DocumentInsets padding) { + CTTcPr properties = cellProperties(cell); + CTTcMar margins = properties.isSetTcMar() ? properties.getTcMar() : properties.addNewTcMar(); + setCellMargin(margins.isSetTop() ? margins.getTop() : margins.addNewTop(), padding.top()); + setCellMargin(margins.isSetBottom() ? margins.getBottom() : margins.addNewBottom(), + padding.bottom()); + setCellMargin(margins.isSetLeft() ? margins.getLeft() : margins.addNewLeft(), padding.left()); + setCellMargin(margins.isSetRight() ? margins.getRight() : margins.addNewRight(), + padding.right()); + } + + /** + * The padding a cell resolves to, most specific wins, with the engine's own default + * underneath. + * + *

The default is the last step of the same cascade the layout pipeline runs, where + * {@code TableCellLayoutStyle.DEFAULT} sits under the authored styles — so a table that + * states no padding is laid out with 4pt and has to be written with 4pt. + * {@code DocxCellPaddingTest} pins the two together, because the engine's copy is + * internal and cannot be read from here.

+ */ + private DocumentInsets resolveCellPadding(TableNode node, TableGrid.Placement placement) { + DocumentInsets authored = resolveCellValue(node, placement, DocumentTableStyle::padding); + return authored != null ? authored : DocumentInsets.of(ENGINE_DEFAULT_CELL_PADDING_POINTS); + } + + /** What the engine lays a cell out with when nothing states otherwise. */ + static final double ENGINE_DEFAULT_CELL_PADDING_POINTS = 4.0; + + /** The left and right margins written on a cell, or Word's own default for an unwritten one. */ + private static double horizontalMarginsOf(XWPFTableCell cell) { + CTTcPr properties = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : null; + CTTcMar margins = properties != null && properties.isSetTcMar() ? properties.getTcMar() : null; + if (margins == null) { + return 2 * WORD_DEFAULT_CELL_MARGIN_POINTS; + } + return marginPoints(margins.isSetLeft() ? margins.getLeft() : null) + + marginPoints(margins.isSetRight() ? margins.getRight() : null); + } + + 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; + } + + /** Word keeps this much clear inside every cell edge unless a table says otherwise. */ + private static final double WORD_DEFAULT_CELL_MARGIN_POINTS = 5.4; + private static CTTcPr cellProperties(XWPFTableCell cell) { return cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() @@ -2687,14 +2753,12 @@ private static boolean isSemanticallyTransparent(DocumentNode node) { * @param columns column count of the first row * @return the created table, already attached where it belongs */ - /** Word keeps this much clear inside every cell edge unless a table says otherwise. */ - private static final double DEFAULT_CELL_MARGIN_POINTS = 5.4; - /** - * How wide content can be inside one cell: the columns it spans, less Word's margins. + * How wide content can be inside one cell: the columns it spans, less its own margins. * - *

Read back from the grid this export just wrote rather than recomputed, so a cell - * cannot disagree with the table it is in.

+ *

Both are read back from what this export just wrote — the grid for the width and + * {@code w:tcMar} for the margins — so a cell cannot disagree with the table it is in, + * and a table whose padding the document stated is not measured against Word's.

* * @return the usable width in points, or {@code NaN} when the table has no written grid */ @@ -2708,7 +2772,7 @@ private static double usableWidthOf(XWPFTableCell cell, TableGrid.Placement plac for (int index = placement.column(); index < last; index++) { twips += Long.parseLong(String.valueOf(grid.getGridColArray(index).getW())); } - double points = twips / POINT_TO_TWIP - 2 * DEFAULT_CELL_MARGIN_POINTS; + double points = twips / POINT_TO_TWIP - horizontalMarginsOf(cell); return points > 0 ? points : Double.NaN; } diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxCellPaddingTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxCellPaddingTest.java new file mode 100644 index 000000000..f90a8174d --- /dev/null +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxCellPaddingTest.java @@ -0,0 +1,125 @@ +package com.demcha.compose.document.backend.semantic.docx; + +import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.table.DocumentTableCell; +import com.demcha.compose.document.table.DocumentTableColumn; +import com.demcha.compose.document.table.DocumentTableStyle; +import com.demcha.compose.engine.components.content.table.TableCellLayoutStyle; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.apache.poi.xwpf.usermodel.XWPFTable; +import org.apache.poi.xwpf.usermodel.XWPFTableCell; +import org.junit.jupiter.api.Test; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcMar; + +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A table cell keeps the space the document put inside its edges. + * + *

It was never written, so Word used its own — 5.4pt at each side and nothing + * above or below. Measured on the probe corpus, every row of a five-row table came out + * 8.1pt short of the page, because a row's height is its content's box and the padding is + * part of that box.

+ * + *

Word holds this as {@code w:tcMar}, so it is a mapping. All four sides are written, + * and written even when they are zero: Word's default is not zero, so a table that asked + * for no padding would otherwise export with Word's side margins.

+ * + * @author Artem Demchyshyn + */ +class DocxCellPaddingTest { + + private static final double TWIPS_PER_POINT = 20.0; + + @Test + void aStatedPaddingReachesAllFourSides() throws Exception { + XWPFTableCell cell = firstCell(page -> page.addTable(t -> t + .columns(DocumentTableColumn.auto()) + .defaultCellStyle(DocumentTableStyle.builder() + .padding(new DocumentInsets(9, 7, 5, 3)) + .build()) + .row("Padded"))); + + assertThat(margins(cell)).containsExactly(180L, 140L, 100L, 60L); + } + + @Test + void aTableThatStatesNothingIsWrittenWithTheEnginesOwnDefault() throws Exception { + // Not Word's 5.4pt a side and nothing above: the page is laid out with 4pt all + // round, and the file has to say the same or the rows come out shorter than drawn. + XWPFTableCell cell = firstCell(page -> page.addTable(t -> t + .columns(DocumentTableColumn.auto()) + .row("Plain"))); + + long four = Math.round(4 * TWIPS_PER_POINT); + assertThat(margins(cell)).containsExactly(four, four, four, four); + } + + @Test + void theDefaultIsTheOneTheEngineLaysOutWith() throws Exception { + // The engine's copy is internal and cannot be read from the backend, so the two + // are pinned together here: if the layout's default moves, this fails rather than + // the export quietly drawing rows of a height nothing asked for. + assertThat(DocxSemanticBackend.ENGINE_DEFAULT_CELL_PADDING_POINTS) + .isEqualTo(TableCellLayoutStyle.DEFAULT.padding().top()) + .isEqualTo(TableCellLayoutStyle.DEFAULT.padding().bottom()) + .isEqualTo(TableCellLayoutStyle.DEFAULT.padding().left()) + .isEqualTo(TableCellLayoutStyle.DEFAULT.padding().right()); + } + + @Test + void aCellsOwnPaddingBeatsTheRowsAndTheTablesTest() throws Exception { + // The same cascade the layout pipeline merges in, applied per field. + XWPFTable table = firstTable(page -> page.addTable(t -> t + .columns(DocumentTableColumn.auto(), DocumentTableColumn.auto()) + .defaultCellStyle(DocumentTableStyle.builder() + .padding(DocumentInsets.of(2)) + .build()) + .rowStyle(0, DocumentTableStyle.builder().padding(DocumentInsets.of(6)).build()) + .rowCells(DocumentTableCell.text("row wins"), + DocumentTableCell.text("cell wins").withStyle( + DocumentTableStyle.builder().padding(DocumentInsets.of(11)).build())))); + + assertThat(margins(table.getRow(0).getCell(0))[0]).isEqualTo(Math.round(6 * TWIPS_PER_POINT)); + assertThat(margins(table.getRow(0).getCell(1))[0]).isEqualTo(Math.round(11 * TWIPS_PER_POINT)); + } + + @Test + void aTableThatAsksForNoPaddingGetsNoneRatherThanWords() throws Exception { + XWPFTableCell cell = firstCell(page -> page.addTable(t -> t + .columns(DocumentTableColumn.auto()) + .defaultCellStyle(DocumentTableStyle.builder() + .padding(DocumentInsets.zero()) + .build()) + .row("Tight"))); + + assertThat(margins(cell)).containsExactly(0L, 0L, 0L, 0L); + } + + /** Top, right, bottom, left in twips. */ + private static long[] margins(XWPFTableCell cell) { + CTTcMar mar = cell.getCTTc().getTcPr().getTcMar(); + return new long[]{width(mar.getTop()), width(mar.getRight()), + width(mar.getBottom()), width(mar.getLeft())}; + } + + private static long width(CTTblWidth margin) { + return margin == null || margin.getW() == null + ? -1 + : Long.parseLong(String.valueOf(margin.getW())); + } + + private static XWPFTableCell firstCell(Consumer content) throws Exception { + return firstTable(content).getRow(0).getCell(0); + } + + private static XWPFTable firstTable(Consumer content) throws Exception { + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, content)) { + return document.getTables().get(0); + } + } +} diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java index 0f5a9c176..f344bfdec 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java @@ -109,8 +109,9 @@ void aNestedTableIsGivenTheWidthOfTheColumnItSitsIn() throws Exception { assertThat(cell.getTables().get(0).getCTTbl().getTblPr().getTblW().getType().toString()) .as("stated, not left to Word") .isEqualTo("dxa"); - // The column less the margins Word keeps inside every cell edge: 5.4pt a side. - assertThat(nested).isEqualTo(column - 2 * 108); + // The column less the margins the cell itself states: the engine's own default + // cell padding, 4pt a side, which this export writes as w:tcMar. + assertThat(nested).isEqualTo(column - 2 * 80); } @Test diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxInlineBackgroundTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxInlineBackgroundTest.java index 43e5f8f61..a8e4d38e4 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxInlineBackgroundTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxInlineBackgroundTest.java @@ -119,7 +119,7 @@ void readingWhatIsUnderAChipPaintsNothing() throws Exception { .as("nothing underneath it, so the page's white") .isEqualTo("EFF1F3"); assertThat(document.getTables().get(0).getRow(0).getTableCells().stream() - .noneMatch(cell -> cell.getCTTc().isSetTcPr())) + .noneMatch(cell -> cell.getCTTc().getTcPr().isSetShd())) .as("a cell nobody painted stays unpainted") .isTrue(); } diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTablePaintTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTablePaintTest.java index 5b5acc60c..ccce9e1a2 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTablePaintTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTablePaintTest.java @@ -47,8 +47,12 @@ void aCellFillReachesWordAsShading() throws Exception { .build()); assertThat(shadingFill(table.getRow(0).getCell(0))).isEqualToIgnoringCase("14505F"); - // A cell nothing painted keeps Word's default rather than being filled with black. - assertThat(table.getRow(0).getCell(1).getCTTc().getTcPr()).isNull(); + // A cell nothing painted is not painted. It still carries properties — every cell + // states its own margins — so the question is whether it has shading, not whether + // it has a w:tcPr. + assertThat(table.getRow(0).getCell(1).getCTTc().getTcPr().isSetShd()) + .as("a cell nothing painted keeps Word's default rather than being filled with black") + .isFalse(); } @Test