Skip to content

fix(docx): write a row's fill, outline and side borders, as a panel holding its columns - #871

Merged
DemchaAV merged 4 commits into
2.5-devfrom
fix/docx-write-row-paint
Oct 8, 2026
Merged

DemchaAV merged 4 commits into
2.5-devfrom
fix/docx-write-row-paint

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Why

A row paints its box as a container does — RowBuilder.fillColor, stroke, borders, cornerRadius (RowDefinition emits the same decoration as ContainerDefinition). The DOCX export wrote the row's columns as a table with no shading or borders and named the paint as lost (row paint, DROPPED). Word holds a box's fill and borders on a table cell, as the export already does for a painted container.

What changed

  • A row in the flow that paints its box is written as a panel holding its columns (writePaintedRow). It goes through the panel writer containers use (writePanelPiece):

    • a table of one cell carrying the fill as its shading, and the outline and side borders as its borders;
    • its margins the row's padding, as a panel's are;
    • the columns a table nested in the cell.
  • writePanelPiece takes what its cell holds (CellContent): a container's children as before, or a row's columns. It also takes whether the layout moves the panel to a new page, and then holds the space the page keeps above it on that page (holdTheSpaceAboveOnItsPage). Before that, writePaintedRow leaves on the page above what the row's table left there (leaveThePageAbove, now shared with writeTableWithItsOwnSpacing). Containers pass false and are written as before.

  • The columns in the row's own panel are laid out without the padding (writeRow(…, inItsPanel), applyRowGeometry, withoutTheRowsPadding). The padding comes off the first and last columns, on the layout's placed columns and on the row's own arithmetic alike, each keeping a child's margin and the gap. The columns are not held to a height of their own: the panel holds the row's.

  • Whether a row is its panel is decided once, before anything of it is written. It paints, stands tall enough to be painted, and has a place of its own in the layout or no layout at all (paintsAPanelOfItsOwn). It also needs nothing to keep it from its panel (keptFromItsPanel):

    • a padding below zero, or a first or last column whose margin hangs into it, which a cell's margins do not hold;
    • a margin below zero above or below it, which its columns' table nets against its padding and a panel does not, or at a side in a cell, where Word starts a table no further left than the cell's text. In the body a row bleeding past the margin is its panel;
    • a band of overlapping layers, a pending resume, or a block under it that closes what a band or a stack's column measures from (closesAMeasure). There the space round the row is measured to its text, and the panel's margins would hold the padding again.

    A row kept from its panel is written as its columns alone; one composed in a table cell is too, and one in a page zone's line is runs on that line. reportUnwrittenRowPaint names each's paint with the reason (row paint) — APPROXIMATED where a cell's table drew it as a shape. The note's last arm always names something.

  • What Word draws differently is named on the row's note (RowNode, APPROXIMATED):

    • its borders above and below, which Word draws outside the panel's row: where no space round the row takes them, what follows stands up to their width lower;
    • a top border wider than its padding, under which Word starts the columns: they stand up to the difference lower where the space above does not take it.
  • paintOf reads a row's paint as a container's: per-side borders win, a uniform stroke stands in for all four. warnContainerRadiusDropped names a row's squared corners (corner radius). A translucent fill or border is flattened against what the page paints under it and named, as a panel's is. What stands on the row — a chip, a rule — is flattened against the panel's shading as written (surfaceBehind).

  • DocxLayoutMetrics.colourUnder still leaves a row's own fill out. What stands on the fill is written in the row's panel and reads its shading. What the page lays over the row from outside it is written before or after the panel, where the fill is not under it in Word.

Verification

  • ./mvnw -B -ntp install -pl :graph-compose-render-docx → BUILD SUCCESS, 1239 tests, 0 failures, 1 skipped.
  • New DocxRowPaintTest (14):
    • a filled row's panel: its shading; margins equal to its padding; the columns nested, unshaded and not held; their grid the row's width less its padding; the first column starting and the last ending at the cell's text, with the gap between them; no note; the panel kept whole;
    • an outline as four borders, its borders' note and its columns under the unpadded top border named; padded, the columns not named; a bottom border alone, with its note;
    • a rounded row keeping its fill, its corners named;
    • a translucent fill flattened against the page and named, and a translucent panel in a filled row flattened against the row's shading;
    • a filled row in a band of layers, opening it and in the middle of a layer, written as its columns alone, its paint named, and a panel laid over it flattened against white, as Word shows it;
    • in a stack's columns, a filled row opening a later layer and one closing a layer another resumes after, each named;
    • a row whose first column hangs into its padding, and one with a padding below zero, each named;
    • a row pulled up and down by its margin, and one bleeding to its card's edge, each named; one bleeding past the page margin in the body, its panel;
    • a row the layout moves to a new page, its top margin held by a line kept with the panel, the space below the paragraph before it left on the page above;
    • an empty row it paints, a panel of its padding;
    • with no layout, the columns the row's own arithmetic less its padding;
    • a row of no height, not written;
    • a row that paints nothing, its columns alone as before.
  • DocxReportedLossesTest: a row in the flow not named; a row composed in a table cell and one in a page zone still named. DocxTranslucencyTest: a chip and a rule in a navy row are flattened against the navy.
  • Word 16 and LibreOffice, on a page of painted rows (filled, outlined, bottom-bordered, centred, in a filled section, empty), against the page:
    • a filled row's shading covers the box the page fills to a tenth of a point in both editors, and its text stands where the page sets it;
    • before, Word showed no fill, outline or border, and a row in a dark section came out as dark text on its dark fill.
  • Bordered rows one under the other, Word 16 and LibreOffice: five rows with a 1pt bottom border set what follows 5.4pt low. That is their borders' 5pt, plus a tenth of a point for each of the four hairline paragraphs Word needs between two tables. Five containers with that border do the same (5.4pt).
  • 29 sabotages, each reverting a single decision in the change, are each caught by a test. They cover:
    • the routing, and each reason a row is kept from its panel on its own: the band, the pending resume, the closing block, the padding, the hanging column, the margin above or below, the side bleed in a cell, and the body bleed left a panel;
    • each step of the padding taken off, and the columns not held;
    • the new-page space, the kept panel, the corners, the borders' note and the columns' note;
    • the cell and zero-height exceptions, the note's reason, and the colour under.
  • Core doc guards: 166/0. qa doc and DOCX guards: 50/0.
  • DOCX fidelity corpus: no row in it paints its box, and all 62 documents are byte-identical with 2.5-dev. Report notes: 954, unchanged.

Known limits

  • A panel's borders above and below. Word draws them outside its row: where no space round it takes them, what follows stands up to their width lower. This holds for a row's panel and a container's. It is named on the row; a container's is not yet.
  • Rows still written as their columns alone, their paint named: in a band of overlapping layers, or where a stack's column measures the space round the row to its text. A painted container there counts its padding twice, measured to its text and held again by its panel's margins. Rows are kept out of that until panels are measured by their edges.

Lane: render-docx (DOCX semantic backend) — no public API change.

…olding its columns

A row paints its box as a container does, and the export wrote its columns
as a table with no shading or borders, naming the paint as lost.

A row that paints, stands tall enough and has a place of its own is
written through the panel writer containers use (writePanelPiece, which
now takes what its cell holds): one cell carrying the fill and borders,
its margins the row's padding, and the columns nested in it, laid out
without that padding (withoutTheRowsPadding). Its corners are named where
they round; a translucent paint is flattened and named, as a panel's.
colourUnder counts the row's fill, which Word now shows under what sits
on it. A row composed in a table cell or in a page zone keeps its note.
…hat the page sets

A row is kept from its panel, its columns written alone and its paint
named with the reason, where the panel's margins cannot hold what the
page sets:
- a padding below zero, or a first or last column whose margin hangs
  into it;
- a band of overlapping layers, a pending resume, or a block under it
  that closes what a band or a stack's column measures from, where the
  space round the row is measured to its text.

The decision is taken once, before anything of the row is written.

The columns in a row's own panel are laid out without its padding on
the layout's placed columns and on the row's own arithmetic alike, and
are not held to a height of their own. A row the layout moves to a new
page keeps the space above it there. Borders above and below are named,
Word drawing them outside the panel's row.

colourUnder keeps leaving a row's own fill out: what stands on it is
written in the panel and reads its shading; what is laid over the row
from outside it is written before or after the panel.
…ame its columns under the top border

A painted row whose margin is below zero above or below it, or at a side
in a cell, is written as its columns alone and its paint named: its
columns' table nets the margin against its padding, where a panel owes
the space round it on its own and Word starts a table no further left
than its cell's text. In the body a row bleeding past the margin stays
its panel.

A top border wider than the row's padding stands its columns that much
lower in Word, where the space above does not take it; the row's note
now says so beside its borders above and below.

Whether a row is its panel is read once: what keeps it from its panel,
then whether it paints a panel of its own. The new-page step a row's
panel and a table share is one helper (leaveThePageAbove).
@DemchaAV
DemchaAV merged commit 2de7152 into 2.5-dev Oct 8, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-write-row-paint branch October 8, 2026 07:32
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