diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4ea8a44..2fb98dffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A bordered panel in a table cell shows its right border in Word.** Word draws a table's right + border outside its right edge — measured in its PDF, a table ending at 566.0pt had its right + border from 566.2 to 566.9 — and on screen cuts off what passes its cell's edge and draws the + cell's gridline there. `EditorialProposal`'s glance card, as wide as its column, lost its right + border on screen. A bordered panel reaching its cell's text edge now ends its whole right border + plus half a point short of that edge, the points taken from its right margin and no more than + that margin holds: its text stands where it did and wraps as it did, and its right border is + drawn on screen and on paper wherever its margin is wide enough. - **A ruled table's rows are as tall in Word as on the page, and a table that states no rule is ruled as the page rules it.** Word makes room in a table for its horizontal rules — a rule between two rows half in each, the rule above the table and the one below it whole in their diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 6f67d415b..3ec3fa26c 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -351,7 +351,11 @@ How it lands: text of a card land within a pixel of the page's. - **Nesting.** A panel inside a panel is a table inside its cell. So is a row, with no fill of its own, so the panel shows through it. A table keeps its own cell fills, and a cell - no style fills is written white, as the page draws it on the card. + no style fills is written white, as the page draws it on the card. Word draws a table's + right border outside its right edge and, on screen, cuts off what passes its cell's edge + and draws the cell's gridline there; a bordered panel reaching its cell's text edge ends its + whole right border plus half a point short of that edge, the points taken from its right + margin and no more than it holds, so its text keeps its place and width. - **Keeping together.** A `keepTogether()` panel the layout placed on one page is a row Word may not split. Anchors and keeps on the blocks inside a panel carry as they do anywhere else. 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 e76d6e50f..28190a583 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 @@ -2840,6 +2840,12 @@ private void writePanel(XWPFDocument document, DocumentNode node, ContainerPaint } } + /** + * The room a panel in a cell leaves past its right border, in points: Word draws the cell's + * gridline on screen at the cell's edge, over a border that meets it. + */ + private static final double CLEAR_OF_THE_GRIDLINE_POINTS = 0.5; + /** Writes one table of a panel: the whole panel, or the part of it between page breaks. */ private void writePanelPiece(XWPFDocument document, DocumentNode node, ContainerPaint paint, List children, boolean first, boolean last) throws Exception { @@ -2863,11 +2869,33 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container holdTheSpaceAboveATable(document); double width = panelWidth(node); + double edge = insetLeft + margin.left(); + double indent = currentCell == null ? edge + padding.left() - halfLeft : edge - halfLeft; + // In a cell, Word starts a nested table no further left than the cell's text, draws its + // right border outside the table's right edge — measured in its PDF, a table ending at + // 566.0pt had its right border from 566.2 to 566.9 — and on screen cuts off whatever + // passes the cell's edge and draws the cell's gridline over it: a panel as wide as its + // cell lost its right border, EditorialProposal's glance card. A panel with a right + // border that reaches its cell's text edge ends that border and a little more short of + // it, the points taken from its right margin and no more than it holds, so its text keeps + // its place and its width. + double shortOfTheEdge = 0; + double room = currentCell != null && Double.isFinite(currentCellWidth) && strokeWidth(borders.right()) > 0 + ? currentCellWidth - insetRight - Math.max(0, indent) - strokeWidth(borders.right()) + - CLEAR_OF_THE_GRIDLINE_POINTS + : Double.NaN; + XWPFTable table = newTable(document, 1, 1); hideTableGrid(table); XWPFTableCell cell = table.getRow(0).getCell(0); if (Double.isFinite(width) && width > 0) { double outer = width + halfLeft + halfRight; + if (room > 0 && outer > room) { + // No more than the right margin holds: past that the text would narrow and wrap, + // which is worse than a border the cell's edge covers. + shortOfTheEdge = Math.min(outer - room, insideTheBorders(padding, borders).right()); + outer -= shortOfTheEdge; + } setTableWidth(table, outer); writeGrid(table, new double[]{outer}); CTTcPr properties = cellProperties(cell); @@ -2877,7 +2905,9 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container } applyCellPaint(cell, paint.fill(), null); paintCellSides(cell, paint.borders()); - applyCellPadding(cell, insideTheBorders(padding, borders)); + DocumentInsets margins = insideTheBorders(padding, borders); + applyCellPadding(cell, new DocumentInsets(margins.top(), Math.max(0, margins.right() - shortOfTheEdge), + margins.bottom(), margins.left())); if (node.keepTogether() && layout.onOnePage(node)) { table.getRow(0).setCantSplitRow(true); } @@ -2916,15 +2946,14 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container holdRowAtLeast(table.getRow(0), placed.placementHeight()); } - double edge = insetLeft + margin.left(); - double indent = currentCell == null ? edge + padding.left() - halfLeft : edge - halfLeft; if (indent != 0) { CTTblPr tableProperties = table.getCTTbl().getTblPr(); CTTblWidth tableIndent = tableProperties.isSetTblInd() ? tableProperties.getTblInd() : tableProperties.addNewTblInd(); tableIndent.setType(STTblWidth.DXA); - // Signed: a nested panel with no margin starts half its border left of the cell. + // Signed: a nested panel with no margin starts half its border left of the cell, and + // Word starts it at the cell's text. tableIndent.setW(BigInteger.valueOf(Math.round(indent * POINT_TO_TWIP))); } if (last) { diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxPanelEdgeCasesTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxPanelEdgeCasesTest.java index 308b2e6b2..d97e9a06a 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxPanelEdgeCasesTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxPanelEdgeCasesTest.java @@ -162,6 +162,70 @@ void aNestedCardWithNoMarginStartsHalfItsBorderLeftOfTheCell() throws Exception } } + @Test + void aNestedCardWithTooThinAMarginGivesUpNoMoreThanItHolds() throws Exception { + // 1pt of padding against a 1.5pt border: the margin can give back 0.25pt, not the 3.5 + // the border and its clearance would need. The text keeps its width; the border moves in + // only by that quarter point. + try (XWPFDocument document = export(page -> page.addSection("Outer", outer -> outer + .fillColor(SURFACE) + .addSection("Inner", inner -> inner + .stroke(com.demcha.compose.document.style.DocumentStroke.of(ACCENT, 1.5)) + .padding(DocumentInsets.of(1)) + .addParagraph(p -> p.text("Inner")))))) { + XWPFTableCell host = card(document); + var hostProperties = host.getCTTc().getTcPr(); + long room = DocxTwips.of(hostProperties.getTcW().getW()) + - DocxTwips.of(hostProperties.getTcMar().getLeft().getW()) + - DocxTwips.of(hostProperties.getTcMar().getRight().getW()); + XWPFTable nested = host.getTables().get(0); + long width = DocxTwips.of(nested.getCTTbl().getTblPr().getTblW().getW()); + var margins = nested.getRow(0).getCell(0).getCTTc().getTcPr().getTcMar(); + + assertThat(DocxTwips.of(margins.getRight().getW())).as("all its right margin given up").isZero(); + // As wide as its cell, every border inside its margins: the room less 1pt of padding + // each side, and the four half borders the margins give back. + assertThat(width - DocxTwips.of(margins.getLeft().getW()) - DocxTwips.of(margins.getRight().getW())) + .as("its text as wide as on the page") + .isCloseTo(room - 40 + 60, org.assertj.core.data.Offset.offset(3L)); + } + } + + @Test + void aNestedCardAsWideAsItsCellEndsItsRightBorderInsideItAndKeepsItsTextsWidth() throws Exception { + // Word starts a nested table no further left than its cell's text, draws its right border + // outside its right edge and, on screen, cuts off what passes the cell's edge and draws + // the cell's gridline there: a card as wide as its cell lost its right border. The points + // it gives up come off its right margin. + try (XWPFDocument document = export(page -> page.addSection("Outer", outer -> outer + .fillColor(SURFACE) + .addSection("Inner", inner -> inner + .stroke(com.demcha.compose.document.style.DocumentStroke.of(ACCENT, 1.5)) + .padding(DocumentInsets.of(10)) + .addParagraph(p -> p.text("Inner")))))) { + XWPFTableCell host = card(document); + XWPFTable nested = host.getTables().get(0); + var hostProperties = host.getCTTc().getTcPr(); + long room = DocxTwips.of(hostProperties.getTcW().getW()) + - DocxTwips.of(hostProperties.getTcMar().getLeft().getW()) + - DocxTwips.of(hostProperties.getTcMar().getRight().getW()); + var tableProperties = nested.getCTTbl().getTblPr(); + long start = Math.max(0, tableProperties.isSetTblInd() ? DocxTwips.of(tableProperties.getTblInd().getW()) : 0); + long width = DocxTwips.of(tableProperties.getTblW().getW()); + var margins = nested.getRow(0).getCell(0).getCTTc().getTcPr().getTcMar(); + long text = width - DocxTwips.of(margins.getLeft().getW()) - DocxTwips.of(margins.getRight().getW()); + + // Word draws the right border, 1.5pt or 30 twips, outside the table's edge, and the + // cell's gridline half a point further. The text is as wide as the card written as + // wide as its cell, every border inside the cell's margins: its room less its 10pt + // padding each side, and the four half borders the margins give back. + assertThat(start + width + 30 + 10).as("its right border inside the cell, clear of its gridline") + .isLessThanOrEqualTo(room); + assertThat(text).as("its text as wide as on the page") + .isCloseTo(room - 400 + 60, org.assertj.core.data.Offset.offset(3L)); + } + } + @Test void aCardWithNoCanvasLeavesItsWidthToTheEditor() throws Exception { try (XWPFDocument document = DocxExports.withoutCanvas(page -> page.addSection("Card", card -> card