From e553a828b11074bb0adb45717ee88f550bccf14e Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sat, 3 Oct 2026 18:05:08 +0100
Subject: [PATCH 1/2] fix(docx): keep a table's and a row's own margins
---
CHANGELOG.md | 7 +
docs/recipes/docx-export.md | 7 +-
.../libreoffice-windows-lines.tsv | 6 +-
.../docx-fidelity/libreoffice-windows.tsv | 2 +-
.../docx-fidelity/word-windows-lines.tsv | 6 +-
.../resources/docx-fidelity/word-windows.tsv | 2 +-
.../semantic/docx/DocxSemanticBackend.java | 37 +++-
.../semantic/docx/DocxTableMarginsTest.java | 165 ++++++++++++++++++
8 files changed, 215 insertions(+), 17 deletions(-)
create mode 100644 render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTableMarginsTest.java
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7104f735..3150ba822 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,13 @@ follow semantic versioning; release dates are ISO 8601.
### Public API
+- **A table or a row keeps its own margins in DOCX.** Its left margin is now written into its
+ indent (`w:tblInd`), as a paragraph's is: it was dropped everywhere, and `PaymentsInvoice`'s
+ bank details stood 36.7pt left of the page's in Word. A table that opens a layer of a stack
+ written as a band starts where the band resumes and keeps its top margin below that, held by
+ a hairline line at the top of a cell. `VioletGrid`'s education lines, a table beside a badge
+ held 51.3pt clear of it and 2.3pt down, started under the badge and stood 2.4pt high; they
+ are 0.05pt off now. Lines more than 2pt off in Word fall from 5 to 2 for `VioletGrid`.
- **Text hanging past a block takes its place out of the space above a stack that follows in
DOCX.** A stack written as a band opens at the space the page measures above its first block,
and that block forgot what the text above it already hung into that space: a band's or a
diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md
index c9b37abf8..25f65d6e6 100644
--- a/docs/recipes/docx-export.md
+++ b/docs/recipes/docx-export.md
@@ -252,8 +252,11 @@ paragraph a point tall after it: Word writes one there in any case, and its own
text tall.
The horizontal half is carried as an indent: outside any panel, every paragraph by each enclosing container's
-margin and padding, a row or a table by the same amount as `w:tblInd` — see "What a panel
-keeps and loses".
+margin and padding, a row or a table by the same amount as `w:tblInd`, its own left margin
+added unless it is a row's column, whose cell already starts past it — see "What a panel
+keeps and loses". A table that opens a layer of a stack written as a band, or of a column
+layer, starts where the layer resumes, its own top margin below that; at the top of a cell an
+empty hairline line above it holds that space, as it does for a row.
A right-to-left paragraph has its two indents written the other way round. In a `w:bidi`
paragraph Word and LibreOffice read `w:ind`'s `left` and `right` as the start and end of
diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv
index 697cb65ca..b14091dfd 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv
@@ -1685,9 +1685,9 @@ cv-violet_grid 0:999ddb26bed6 21.16 redesignedkeypatientflowsincludi
cv-violet_grid 0:073d15633c0b 21.16 systemandcomponentlibraryusedacr
cv-violet_grid 0:68bf0109c6f8 21.41 education
cv-violet_grid 0:6cb574fa10b6 21.41 languages
-cv-violet_grid 0:0cf7506cf94a 19.08 bacheloroffineartsingraphicdesig
-cv-violet_grid 0:00ab7a6aa5f1 19.09 californiacollegeofthearts
-cv-violet_grid 0:7e2c21bb41db 19.08 sanfrancisco,ca|2014-2018
+cv-violet_grid 0:0cf7506cf94a 21.48 bacheloroffineartsingraphicdesig
+cv-violet_grid 0:00ab7a6aa5f1 21.49 californiacollegeofthearts
+cv-violet_grid 0:7e2c21bb41db 21.48 sanfrancisco,ca|2014-2018
cv-violet_grid 0:b6234d2ea0d6 20.73 english
cv-violet_grid 0:bef32d2c315a 37.47 native
cv-violet_grid 0:bef32d2c315a#1 5.21 native
diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv
index f84de8629..b94487410 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv
@@ -25,7 +25,7 @@ cv-slate_orange 1 1 87 86 3.27 3.87 86
cv-teal_pulse 1 1 57 57 0.22 0.64 0
cv-terracotta_rail 1 1 102 102 0.44 0.94 0
cv-timeline_minimal 2 2 89 86 0.45 0.50 0
-cv-violet_grid 1 2 80 76 5.21 21.31 39
+cv-violet_grid 1 2 80 76 5.21 21.41 39
invoice-classic 1 1 56 56 0.17 0.32 0
invoice-classic_long 2 2 166 166 0.22 0.27 0
invoice-consulting 1 1 92 92 0.67 1.01 0
diff --git a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv
index 0cafde84c..6878c2617 100644
--- a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv
+++ b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv
@@ -1778,9 +1778,9 @@ cv-violet_grid 0:999ddb26bed6 -0.33 redesignedkeypatientflowsincludi
cv-violet_grid 0:073d15633c0b -0.29 systemandcomponentlibraryusedacr
cv-violet_grid 0:68bf0109c6f8 -0.07 education
cv-violet_grid 0:6cb574fa10b6 -0.07 languages
-cv-violet_grid 0:0cf7506cf94a -2.47 bacheloroffineartsingraphicdesig
-cv-violet_grid 0:00ab7a6aa5f1 -2.43 californiacollegeofthearts
-cv-violet_grid 0:7e2c21bb41db -2.44 sanfrancisco,ca|2014-2018
+cv-violet_grid 0:0cf7506cf94a -0.07 bacheloroffineartsingraphicdesig
+cv-violet_grid 0:00ab7a6aa5f1 -0.03 californiacollegeofthearts
+cv-violet_grid 0:7e2c21bb41db -0.04 sanfrancisco,ca|2014-2018
cv-violet_grid 0:b6234d2ea0d6 -1.38 english
cv-violet_grid 0:bef32d2c315a -1.37 native
cv-violet_grid 0:bef32d2c315a#1 -1.29 native
diff --git a/qa/src/test/resources/docx-fidelity/word-windows.tsv b/qa/src/test/resources/docx-fidelity/word-windows.tsv
index ca33ebb8e..df8f19b25 100644
--- a/qa/src/test/resources/docx-fidelity/word-windows.tsv
+++ b/qa/src/test/resources/docx-fidelity/word-windows.tsv
@@ -25,7 +25,7 @@ cv-slate_orange 1 1 87 85 0.53 1.18 0
cv-teal_pulse 1 1 57 57 0.11 0.38 0
cv-terracotta_rail 1 1 102 102 0.42 0.87 0
cv-timeline_minimal 2 2 89 86 0.36 0.48 0
-cv-violet_grid 1 1 80 77 0.23 1.30 5
+cv-violet_grid 1 1 80 77 0.22 1.09 2
invoice-classic 1 1 56 56 0.22 0.36 0
invoice-classic_long 2 2 166 166 0.27 0.34 0
invoice-consulting 1 1 92 92 0.70 1.02 0
diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java
index 94c825bdc..375a70043 100644
--- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java
+++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java
@@ -7366,8 +7366,9 @@ private static boolean startsWith(byte[] bytes, int... signature) {
* paragraph after. Both go through the debt every other gap goes through
* ({@link #owePendingSpacingAfter}), so a table between two paragraphs reads the same as
* two paragraphs with a gap between them. With nothing above it — opening the body, or a
- * row opening any cell but a table's — a paragraph a tenth of a point tall holds that edge
- * ({@link #holdTheSpaceAboveATable}); a table opening a cell still loses it.
+ * row opening any cell but a table's, or a table opening a cell where a band or column layer
+ * resumes — a paragraph a tenth of a point tall holds that edge
+ * ({@link #holdTheSpaceAboveATable}); any other table opening a cell still loses it.
*/
private void writeTableWithItsOwnSpacing(XWPFDocument document, DocumentNode node)
throws Exception {
@@ -7384,11 +7385,20 @@ private void writeTableWithItsOwnSpacing(XWPFDocument document, DocumentNode nod
borderBelow = 0;
forgetTheHang();
}
+ // A table a band's or a column's layer opens is that layer's first block, so it starts
+ // where the layer resumes, as a paragraph does, and its own top margin comes after: VioletGrid's education
+ // lines, beside a badge, lost their 2.3pt and stood 2.4pt high. A row is not a first block:
+ // the band measures to the first one inside it, and the row's cells write what stands
+ // above that block.
+ boolean resumed = node instanceof TableNode && !Double.isNaN(resumeSpacing);
+ if (resumed) {
+ resumeHere();
+ }
owePendingSpacingAfter(node.margin().top() + node.padding().top());
if (onANewPage) {
holdTheSpaceAboveOnItsPage(document);
}
- if (node instanceof RowNode && currentCell != null && !tablesCells.contains(currentCell.getCTTc())) {
+ if ((node instanceof RowNode || resumed) && currentCell != null && !tablesCells.contains(currentCell.getCTTc())) {
// At the top of a cell nothing above holds that space: MerchantInvoice's due-date
// row lost its 16.7pt of top padding and stood against the card's top edge, once the
// card held the page's height, and PaymentsInvoice's metadata grid the 6.2pt its
@@ -7398,10 +7408,23 @@ private void writeTableWithItsOwnSpacing(XWPFDocument document, DocumentNode nod
// 6pt below the page's.
holdTheSpaceAboveATable(document);
}
- if (node instanceof RowNode row) {
- writeRow(document, row);
- } else {
- writeTable(document, (TableNode) node);
+ // Its side margins hold it in from the edges it is written between, as a paragraph's do:
+ // VioletGrid's education lines, a table held 51.3pt clear of the badge beside them,
+ // started under the badge, and PaymentsInvoice's bank details 36.7pt left of the page's.
+ double outerLeft = insetLeft;
+ double outerRight = insetRight;
+ // A row's column already starts past the margin of the block the layout placed in it.
+ insetLeft += node == leftMarginInCell ? 0 : node.margin().left();
+ insetRight += node.margin().right();
+ try {
+ if (node instanceof RowNode row) {
+ writeRow(document, row);
+ } else {
+ writeTable(document, (TableNode) node);
+ }
+ } finally {
+ insetLeft = outerLeft;
+ insetRight = outerRight;
}
owePendingSpacingAfter(node.margin().bottom() + node.padding().bottom());
}
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTableMarginsTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTableMarginsTest.java
new file mode 100644
index 000000000..59a82d4ab
--- /dev/null
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTableMarginsTest.java
@@ -0,0 +1,165 @@
+package com.demcha.compose.document.backend.semantic.docx;
+
+import com.demcha.compose.document.dsl.PageFlowBuilder;
+import com.demcha.compose.document.dsl.ParagraphBuilder;
+import com.demcha.compose.document.dsl.SectionBuilder;
+import com.demcha.compose.document.dsl.ShapeContainerBuilder;
+import com.demcha.compose.document.dsl.TableBuilder;
+import com.demcha.compose.document.node.DocumentNode;
+import com.demcha.compose.document.node.LayerAlign;
+import com.demcha.compose.document.style.DocumentColor;
+import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.style.DocumentStroke;
+import com.demcha.compose.document.table.DocumentTableCell;
+import com.demcha.compose.document.table.DocumentTableColumn;
+import com.demcha.compose.document.table.DocumentTableStyle;
+import org.apache.poi.xwpf.usermodel.IBodyElement;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.apache.poi.xwpf.usermodel.XWPFParagraph;
+import org.apache.poi.xwpf.usermodel.XWPFTable;
+import org.apache.poi.xwpf.usermodel.XWPFTableCell;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A table keeps its own margins: its left margin holds it in from the edge it is written from,
+ * and its top margin stands above it, in a band's layer as anywhere else.
+ *
+ * VioletGrid sets each education entry's lines in a table held 51.3pt clear of a badge
+ * drawn beside them, and 2.3pt down. Neither margin was written: in Word the lines started
+ * under the badge and stood 2.4pt high. A table or a row was never indented by its left
+ * margin, anywhere — PaymentsInvoice's bank details stood 36.7pt left of the page's.
+ */
+class DocxTableMarginsTest {
+
+ private static final double INSET = 51.3;
+ private static final double DOWN = 2.3;
+
+ @Test
+ void aTablesLeftMarginIndentsIt() throws Exception {
+ try (XWPFDocument document = DocxExports.withLayout(500, 600, 20, page -> page
+ .addParagraph("Above")
+ .add(lines()))) {
+ assertThat(indentOf(document.getTables().get(0))).as("the table stands its margin in")
+ .isEqualTo(Math.round(INSET * 20));
+ }
+ }
+
+ @Test
+ void aRowsLeftMarginIndentsIt() throws Exception {
+ try (XWPFDocument document = DocxExports.withLayout(500, 600, 20, page -> page
+ .addParagraph("Above")
+ .addRow(row -> row.name("Split").margin(new DocumentInsets(0, 0, 0, 30))
+ .addSection("Left", left -> left.addParagraph("Left"))
+ .addSection("Right", right -> right.addParagraph("Right"))))) {
+ assertThat(indentOf(document.getTables().get(0))).as("the row stands its margin in")
+ .isEqualTo(600);
+ }
+ }
+
+ @Test
+ void aTableBesideABadgeKeepsItsMarginsInTheBand() throws Exception {
+ try (XWPFDocument document = DocxExports.withLayout(500, 600, 20, page -> {
+ page.addParagraph("Above");
+ body(page);
+ })) {
+ XWPFTable lines = document.getTables().get(0);
+ assertThat(lines.getText()).contains("Degree");
+ assertThat(indentOf(lines)).as("clear of the badge, as the page sets it")
+ .isEqualTo(Math.round(INSET * 20));
+ XWPFParagraph above = document.getParagraphs().get(0);
+ assertThat(above.getText()).isEqualTo("Above");
+ assertThat(above.getSpacingAfter()).as("and its top margin stands above it")
+ .isEqualTo((int) Math.round(DOWN * 20));
+ }
+ }
+
+ @Test
+ void aTableOpeningACellBesideABadgeKeepsItsTopMarginOverIt() throws Exception {
+ try (XWPFDocument document = DocxExports.withLayout(500, 600, 20, page -> page
+ .addParagraph("Above")
+ .addRow(row -> row.name("Columns")
+ .addSection("Education", this::bodyIn)
+ .addSection("Other", other -> other.addParagraph("Other"))))) {
+ XWPFTableCell cell = document.getTables().get(0).getRow(0).getCell(0);
+ List elements = cell.getBodyElements();
+ assertThat(elements.get(0)).as("nothing above the table in its cell holds the space")
+ .isInstanceOf(XWPFParagraph.class);
+ XWPFParagraph hairline = (XWPFParagraph) elements.get(0);
+ assertThat(hairline.getText()).isEmpty();
+ assertThat(hairline.getSpacingBefore()).as("so a hairline over it holds its top margin")
+ .isEqualTo((int) Math.round(DOWN * 20));
+ XWPFTable lines = (XWPFTable) elements.get(1);
+ assertThat(indentOf(lines)).isEqualTo(Math.round(INSET * 20));
+ }
+ }
+
+ @Test
+ void aTableARowPlacesAsAColumnIsNotIndentedTwice() throws Exception {
+ // The row's cell starts where the layout placed the table, past its margin already.
+ // RowBuilder refuses a table, and a row node built by hand holds one.
+ DocumentNode row = new com.demcha.compose.document.node.RowNode("Columns",
+ List.of(lines(), new SectionBuilder().name("Other").addParagraph("Other").build()),
+ List.of(1.0, 1.0), 0, DocumentInsets.zero(), DocumentInsets.zero(), null, null,
+ com.demcha.compose.document.style.DocumentCornerRadius.ZERO, null, List.of());
+ try (XWPFDocument document = DocxExports.withLayout(500, 600, 20, page -> page.add(row))) {
+ XWPFTableCell cell = document.getTables().get(0).getRow(0).getCell(0);
+ XWPFTable lines = cell.getTables().get(0);
+ assertThat(lines.getText()).contains("Degree");
+ assertThat(indentOf(lines)).as("the column holds the margin, not the table").isZero();
+ }
+ }
+
+ @Test
+ void aTableOpeningACellOutsideABandHoldsNoHairline() throws Exception {
+ try (XWPFDocument document = DocxExports.withLayout(500, 600, 20, page -> page
+ .addRow(row -> row.name("Columns")
+ .addSection("Education", section -> section.add(lines()))
+ .addSection("Other", other -> other.addParagraph("Other"))))) {
+ XWPFTableCell cell = document.getTables().get(0).getRow(0).getCell(0);
+ assertThat(cell.getBodyElements().get(0)).as("nothing resumes there, so nothing is held")
+ .isInstanceOf(XWPFTable.class);
+ }
+ }
+
+ private void bodyIn(SectionBuilder section) {
+ section.addLayerStack(stack -> stack.name("Body")
+ .layer(lines(), LayerAlign.TOP_LEFT, 0)
+ .position(badge(), 0, 0, LayerAlign.TOP_LEFT, 1));
+ }
+
+ private void body(PageFlowBuilder page) {
+ page.addSection("Education", this::bodyIn);
+ }
+
+ private static DocumentNode lines() {
+ return new TableBuilder().name("Lines")
+ .margin(new DocumentInsets(DOWN, 0, 0, INSET)).width(150)
+ .columns(DocumentTableColumn.fixed(150))
+ .defaultCellStyle(DocumentTableStyle.builder().padding(DocumentInsets.zero())
+ .stroke(DocumentStroke.of(DocumentColor.WHITE, 0)).build())
+ .rowCells(DocumentTableCell.node(new ParagraphBuilder().text("Degree").build()))
+ .rowCells(DocumentTableCell.node(new ParagraphBuilder().text("School").build()))
+ .build();
+ }
+
+ private static DocumentNode badge() {
+ return new ShapeContainerBuilder().name("Badge").circle(40)
+ .fillColor(DocumentColor.rgb(200, 180, 240))
+ .center(new SectionBuilder().name("Glyph").spacer(10, 10).build())
+ .build();
+ }
+
+ private static long indentOf(XWPFTable table) {
+ var properties = table.getCTTbl().getTblPr();
+ if (properties == null || !properties.isSetTblInd()) {
+ return 0;
+ }
+ Object width = properties.getTblInd().getW();
+ return width instanceof BigInteger value ? value.longValue() : Long.parseLong(String.valueOf(width));
+ }
+}
From bad988c1c4da13b5a629baf3197ed50c98352222 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sat, 3 Oct 2026 19:01:53 +0100
Subject: [PATCH 2/2] fix(docx): take a cell table's margin out of its width
and the Linux LibreOffice baseline from CI
---
CHANGELOG.md | 9 +++----
.../docx-fidelity/libreoffice-linux-lines.tsv | 6 ++---
.../docx-fidelity/libreoffice-linux.tsv | 2 +-
.../semantic/docx/DocxSemanticBackend.java | 14 ++++++-----
.../semantic/docx/DocxTableMarginsTest.java | 24 ++++++++++++++++---
5 files changed, 38 insertions(+), 17 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3150ba822..8dad04d20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,11 +10,12 @@ follow semantic versioning; release dates are ISO 8601.
- **A table or a row keeps its own margins in DOCX.** Its left margin is now written into its
indent (`w:tblInd`), as a paragraph's is: it was dropped everywhere, and `PaymentsInvoice`'s
- bank details stood 36.7pt left of the page's in Word. A table that opens a layer of a stack
- written as a band starts where the band resumes and keeps its top margin below that, held by
- a hairline line at the top of a cell. `VioletGrid`'s education lines, a table beside a badge
+ bank details stood 36.7pt left of the page's in Word. A table indented in a cell takes that
+ margin out of the width it is given there, rather than running past the cell. A table that
+ opens a layer of a stack written as a band starts where the band resumes and keeps its top
+ margin below that, held by a hairline line at the top of a cell. `VioletGrid`'s education lines, a table beside a badge
held 51.3pt clear of it and 2.3pt down, started under the badge and stood 2.4pt high; they
- are 0.05pt off now. Lines more than 2pt off in Word fall from 5 to 2 for `VioletGrid`.
+ are at most 0.07pt off now. Lines more than 2pt off in Word fall from 5 to 2 for `VioletGrid`.
- **Text hanging past a block takes its place out of the space above a stack that follows in
DOCX.** A stack written as a band opens at the space the page measures above its first block,
and that block forgot what the text above it already hung into that space: a band's or a
diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv
index 2e1447fb2..39a9b350b 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv
@@ -1779,9 +1779,9 @@ cv-violet_grid 0:999ddb26bed6 21.16 redesignedkeypatientflowsincludi
cv-violet_grid 0:073d15633c0b 21.16 systemandcomponentlibraryusedacr
cv-violet_grid 0:68bf0109c6f8 21.41 education
cv-violet_grid 0:6cb574fa10b6 21.41 languages
-cv-violet_grid 0:0cf7506cf94a 19.08 bacheloroffineartsingraphicdesig
-cv-violet_grid 0:00ab7a6aa5f1 19.09 californiacollegeofthearts
-cv-violet_grid 0:7e2c21bb41db 19.08 sanfrancisco,ca|2014-2018
+cv-violet_grid 0:0cf7506cf94a 21.48 bacheloroffineartsingraphicdesig
+cv-violet_grid 0:00ab7a6aa5f1 21.49 californiacollegeofthearts
+cv-violet_grid 0:7e2c21bb41db 21.48 sanfrancisco,ca|2014-2018
cv-violet_grid 0:b6234d2ea0d6 20.73 english
cv-violet_grid 0:bef32d2c315a 37.47 native
cv-violet_grid 0:bef32d2c315a#1 5.21 native
diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
index 8130fb274..3f048ec40 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
@@ -25,7 +25,7 @@ cv-slate_orange 1 1 87 86 3.27 3.87 86
cv-teal_pulse 1 1 57 57 0.22 0.64 0
cv-terracotta_rail 1 1 102 102 0.44 0.94 0
cv-timeline_minimal 2 2 89 86 0.45 0.50 0
-cv-violet_grid 1 2 80 76 5.21 21.31 39
+cv-violet_grid 1 2 80 76 5.21 21.41 39
invoice-classic 1 1 56 56 0.12 0.30 0
invoice-classic_long 2 2 166 166 0.20 0.27 0
invoice-consulting 1 1 92 92 0.67 0.96 0
diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java
index 375a70043..8566239d9 100644
--- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java
+++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java
@@ -7386,10 +7386,10 @@ private void writeTableWithItsOwnSpacing(XWPFDocument document, DocumentNode nod
forgetTheHang();
}
// A table a band's or a column's layer opens is that layer's first block, so it starts
- // where the layer resumes, as a paragraph does, and its own top margin comes after: VioletGrid's education
- // lines, beside a badge, lost their 2.3pt and stood 2.4pt high. A row is not a first block:
- // the band measures to the first one inside it, and the row's cells write what stands
- // above that block.
+ // where the layer resumes, as a paragraph does, and its own top margin comes after:
+ // VioletGrid's education lines, beside a badge, lost their 2.3pt and stood 2.4pt high.
+ // A row is not a first block: the band measures to the first one inside it, and the
+ // row's cells write what stands above that block.
boolean resumed = node instanceof TableNode && !Double.isNaN(resumeSpacing);
if (resumed) {
resumeHere();
@@ -7398,7 +7398,8 @@ private void writeTableWithItsOwnSpacing(XWPFDocument document, DocumentNode nod
if (onANewPage) {
holdTheSpaceAboveOnItsPage(document);
}
- if ((node instanceof RowNode || resumed) && currentCell != null && !tablesCells.contains(currentCell.getCTTc())) {
+ if ((node instanceof RowNode || resumed) && currentCell != null
+ && !tablesCells.contains(currentCell.getCTTc())) {
// At the top of a cell nothing above holds that space: MerchantInvoice's due-date
// row lost its 16.7pt of top padding and stood against the card's top edge, once the
// card held the page's height, and PaymentsInvoice's metadata grid the 6.2pt its
@@ -10085,7 +10086,8 @@ private static double usableWidthOf(XWPFTableCell cell, int column, int span) {
* Word to about one character per line, which is not a document anybody can read.
*/
private double nestedTableWidth() {
- return currentCellWidth;
+ // Less what the table is held in by, its own margins included (writeTableWithItsOwnSpacing).
+ return currentCellWidth - Math.max(0, insetLeft) - Math.max(0, insetRight);
}
private XWPFTable newTable(XWPFDocument document, int rows, int columns) {
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTableMarginsTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTableMarginsTest.java
index 59a82d4ab..7417cc3e9 100644
--- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTableMarginsTest.java
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxTableMarginsTest.java
@@ -20,7 +20,6 @@
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.junit.jupiter.api.Test;
-import java.math.BigInteger;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -114,6 +113,26 @@ void aTableARowPlacesAsAColumnIsNotIndentedTwice() throws Exception {
}
}
+ @Test
+ void aTableIndentedInACellStillEndsInsideIt() throws Exception {
+ try (XWPFDocument document = DocxExports.withLayout(500, 600, 20, page -> page
+ .addRow(row -> row.name("Columns")
+ .addSection("Left", section -> section.add(new TableBuilder().name("Auto")
+ .margin(new DocumentInsets(0, 0, 0, 20))
+ .columns(DocumentTableColumn.auto(), DocumentTableColumn.auto())
+ .row("A rather longer cell of text", "Another rather long cell")
+ .build()))
+ .addSection("Right", right -> right.addParagraph("Right"))))) {
+ XWPFTableCell cell = document.getTables().get(0).getRow(0).getCell(0);
+ XWPFTable nested = cell.getTables().get(0);
+ long cellWidth = ((Number) cell.getCTTc().getTcPr().getTcW().getW()).longValue();
+ long tableWidth = ((Number) nested.getCTTbl().getTblPr().getTblW().getW()).longValue();
+ assertThat(indentOf(nested)).isEqualTo(400);
+ assertThat(indentOf(nested) + tableWidth).as("its margin comes out of its width, not past the cell")
+ .isLessThanOrEqualTo(cellWidth);
+ }
+ }
+
@Test
void aTableOpeningACellOutsideABandHoldsNoHairline() throws Exception {
try (XWPFDocument document = DocxExports.withLayout(500, 600, 20, page -> page
@@ -159,7 +178,6 @@ private static long indentOf(XWPFTable table) {
if (properties == null || !properties.isSetTblInd()) {
return 0;
}
- Object width = properties.getTblInd().getW();
- return width instanceof BigInteger value ? value.longValue() : Long.parseLong(String.valueOf(width));
+ return ((Number) properties.getTblInd().getW()).longValue();
}
}