fix(docx): write a painted card as a one-cell table - #734
Merged
Merged
Conversation
A container with a fill, per-side borders or a uniform stroke was painted onto each paragraph inside it, and in an editor the card came apart: the accent bar broke beside rows and tables, the band had gaps, and the top and bottom padding lay outside it. It is now a table of one cell: the shading is the fill, the borders run the card's full height, and the padding is the cell's margins, less half of each border, with the table half a border wider on each side so text and border land where the page draws them. The table takes the width the layout placed the card at plus a point of editor slack. Blocks inside the card keep their anchors and keeps, a page break closes the card and opens it again after the break, the space above a card with nothing above it is held by a hairline paragraph, and the paragraph Word requires after a nested table is a hairline the next paragraph takes over. Unfilled table cells on a filled card or cell are written white, as the engine paints them.
DemchaAV
force-pushed
the
feature/docx-panel-as-cell
branch
from
September 23, 2026 19:21
f8f495a to
311e8d1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A painted container (a card with a fill, an accent bar or an outline) exported as shading and borders on each paragraph inside it. Word has no element that wraps a run of paragraphs, so in LibreOffice the card came apart:
What changed
A painted container is a table of one cell (
writeContainerChildren→writePanel), which is how a panel is built in Word by hand. The cell's shading is the fill behind everything inside, and its borders are the card's edges at full height. Its margins are the padding on all four sides. An unpainted container is unchanged: its children are written where it stood, indented by its insets.The content goes through the ordinary writers. Children are written by the same writers a composed cell uses (
writeCellNodebecamewriteCellNodes), so they stay paragraphs, lists, rows, tables and pictures a reader edits as usual.Geometry is translated from the page's. The engine centres a border on the panel's edge. LibreOffice keeps a cell's border inside the cell, and places a body table by its first cell's text, with the border centred on that margin line. Measured on a probe card:
insideTheBorders);w:tblIndis the text position less half the left border;w:tblIndis the margin less half the left border.Before the half-border terms, the card sat 3pt right with a 3pt accent. Before the nested rule, a nested panel sat its whole padding right.
Width is the layout's. The table takes
PlacedNode.placementWidth(newDocxLayoutMetrics.placedWidth). The engine sizes a card round short content, so a one-line card is narrow in Word as on the page. One point of slack is added, as auto columns already get, so an editor's substitute face does not wrap the longest line. Without it, the nested box's line wrapped in LibreOffice.Unfilled table cells on a filled card, or inside a filled cell, are white (
resolveCellFill,ENGINE_DEFAULT_CELL_FILL). The engine paints such cells white (TableCellLayoutStyle.DEFAULT), and an unshaded Word cell would show the colour behind it instead. A table straight on the page is unchanged: no shading is written there, as before.No empty line under a nested table. Word ends a cell with a paragraph, and the one after a nested table was a full line tall. That put a blank line under every table in a card and at a card's bottom. It is now a 0.1pt hairline (
holdToHairline). The next paragraph written in the cell takes it over; a second table written next makes it the separator between the two, withkeepNext.Pictures and nested content fit their cell.
availableWidth()uses the cell's width when a cell of known width is being filled.writeRownow sets that width for each row cell, where before a picture in a row's column was sized to the page's width.keepTogether()on a card the layout held on one page makes its roww:cantSplit.What moved into the cell keeps what it had in the body:
writeNodereads what a block wrote from the cell being filled, not the body. Before, it read the body, which does not change inside a card. So a card's inner section withanchor(...)lost its bookmark, and a page reference to it pointed at nothing. Keeps on inner blocks were dropped the same way.edgeParagraphdescends).A page break inside a card closes the table and opens the card again after the break. Word breaks no page inside a cell, and the break used to land after the whole card.
Space above a card with nothing above it is held by a 0.1pt paragraph (
holdTheSpaceAboveATable), where it used to be dropped:Edge cases:
tblIndcan be negative (half its border left of the cell), and is written signed.Double.MAX_VALUE.Removed the paragraph painter (
applyContainerPaint, the fill-coloured hairline edges, the 31ptw:spacecap, the list border spacing) and thecontainerPaint/cellPaintDepthstate.A translucent chip in an unshaded cell (a row's, inside a card) is flattened against the card's fill, not white (
colourUnder).The recipe's "What a panel keeps and loses", the Sections / Composed cells rows, the capability matrix row and the v2.5.0 CHANGELOG entries describe the table form.
Not carried: the corner radius (a cell is rectangular), warned once per export as before.
Known limits:
w:tblInd(body table by its text, nested table by its edge) was measured in LibreOffice only. Word has not been measured, and a nested card may sit a few points off there.Verification
In an editor. Probe card: 3pt accent and 14pt padding, holding a title, a row, a wrapped paragraph, a nested outlined box, a table, an unpainted padded section with a table, and a list. Converted with LibreOffice and measured against the engine's PNG at 96 dpi:
The band's right edge is 3px wider, the point of slack. The nested line no longer wraps, and there is no blank line under the tables.
Tests.
./mvnw -B -ntp test -pl :graph-compose-render-docx→ 345 run, 0 failures.New
DocxPanelEdgeCasesTest(11):tblInd;Each fix was sabotaged and fails its test.
DocxContainerPaintTestwas rewritten for the table form (18):NIL; a uniform stroke on all four sides;tcMar= padding, andtblInd= margin + padding;tblInd= margin − half border;cantSplitonly when kept together;Other test classes:
DocxPanelInsetTest(15): painted cases rewritten to cell margins; new picture-in-a-panel and picture-in-a-row cases.DocxInlineBackgroundTest: new chip-in-a-row-on-a-card case; its paragraph walk now descends into nested tables.DocxAdjacentTablesTest: the two panel-separator cases moved to the paint test.DocxVerticalSpacingTest: two spacing cases no longer paint their wrapper, since they test the unpainted path.Each of these was sabotaged and fails its test:
cantSplit;Gate. Full reactor gate (
clean verifyover core, render-pdf/docx/pptx, templates, testing, qa, coverage) → BUILD SUCCESS (qa 1788 green). Examples after install: 93 green; the committedword-export-companion.docxdid not move.Merge order: stacked on #733 (
fix/docx-panel-padding), which must merge first; this PR then retargets2.5-dev.Lane: shared-engine (render-docx). No public API change.