Skip to content

fix(docx): set a row and an alignment composed in a table cell where the page does - #909

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-composed-row-align
Oct 16, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-composed-row-align

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Why

Content composed in a table cell (DocumentTableCell.node(...)) has no place of its own in the layout. Its paragraphs' lines are laid out under the table's path, so the writers that read a node's placement found nothing, and two things went silently wrong.

  • Rows. Some rows are divided by what their children measure: an arrangement other than START, a spacer that grows, an auto column. Such a row was written as a nested one-row table with no grid, so Word sized its columns to their text. In a row SPACE_BETWEEN of "Total" and "$120", the page puts "$120" at x 284.86pt, against the cell's right edge. Word put it at 170.57pt, the middle of the cell. An auto column's value wrapped onto a second line in Word, while the page keeps it on one.
  • Alignments. A block in addAligned(...) was written from the cell's left. A right-aligned "Total due" sits at 257.63pt on the page and at 24pt in Word. A centred 40pt picture sits at 150pt on the page and at 24pt in Word.

The report said nothing in either case.

What changed

  • composedRowStarts finds where each child of such a row starts. A paragraph starts where its lines start (DocxLayoutMetrics.firstTextBox, the lines the table laid out for it). In a row with no stated columns, a spacer or an empty paragraph stands where the page spreads the room between its neighbours: between two children it knows, the run takes the room they leave, less its widths and the gaps, in equal shares, as an arrangement spreads it. A growing spacer takes all of that room. A run at the row's edge is set from the row's padding: its columns hold nothing either way. In a row of stated columns, a spacer has its column's slot, which no line reports. Such a row is written as before and named. Everything is measured from the row's left: the cell's box, plus the margin written for the cell, plus the containers' insets. The row is then written by the same placed-column path a placed row takes (placedColumns, the auto column's editor point, takeHang).
  • placeComposedAcross holds an aligned block in through holdIn, the same call the body uses. holdIn now takes the box's x and width.
    • A paragraph is held to its lines.
    • A picture is held to where the alignment sets a box of the width it is written at: resolveImageDimensions in the width the alignment leaves it (its padding comes off inside the sizing), as writeImage sizes it.
    • The lines must stand in the cell being written: on its page, crossing it, and with the first baseline inside it. A line a container pulls up past the cell's top still counts. The matcher pairs a paragraph with the first line of its text in the table, and a plain "Total" can take the line a **Total** in another cell lays out. Such lines are not taken.
    • An aligned drawing is skipped: its table already draws it where the page draws it, and a rule takes its place from that drawing.
  • Fail-closed notes (APPROXIMATED). These cases keep the old output and are now named in the report:
    • a row holding a child the lines do not place (a picture, a shape, or a spacer in a row of stated columns);
    • an aligned block of any other kind (a section, say);
    • a paragraph whose lines stand in another cell;
    • a row or an alignment written inside a panel or another table in the cell, which is a cell of its own.
  • No layout behind the export. Nothing is placed there, and the export already says so once for the document, so no alignment or row gets a note of its own.
  • Docs. The recipe's Rows row and its addAligned(...) passage cover composed cells; the CHANGELOG has an entry under 2.5.0 Public API.

Verification

  • Word 16, measured with PyMuPDF. Before → after, against the page:

    Case Before After Page
    SPACE_BETWEEN: "$120" 170.57 284.95 284.86
    growing spacer: "Right" 219.19 283.39 283.32
    addAligned(RIGHT): "Total due" 24.00 257.71 257.63
    addAligned(CENTER) picture 24.00 150.00 150.00

    The auto column's value went from two lines at 170.57 to one line at 63.14 (page 62.14). The extra point is the auto column's editor slack, as in a placed row.

  • DocxComposedCellGeometryTest, 25 tests:

    • Grids for SPACE_BETWEEN, CENTER, an auto column, a growing spacer with spacing, a fixed spacer in SPACE_BETWEEN and in SPACE_AROUND, a padded row with margins, and an empty paragraph. Each expected grid is read off the session's own layout fragments.
    • A note for a spacer in a row of stated columns. No note for an aligned row. A line pulled up past its cell's top is held in, not named.
    • A stated-weights row unchanged.
    • Notes for a row holding a picture and for a row in a panel.
    • Indents for a right-aligned paragraph and a centred picture.
    • Notes for an aligned section and for an alignment in a panel.
    • No note for a drawing aligned in a panel, none for a left alignment, and none for an alignment with no layout.
    • Notes for an aligned paragraph and a row whose lines stand in another cell, both across and a table row apart.
    • A padded, unsized picture wider than its cell keeps the page's width: 292pt less 20.
  • Sabotage. 20 sabotages each undo one part of the change, and every one fails a test.

  • Template corpus (62 DOCX) against 2.5-dev. All 62 are byte-identical and the report notes stay at 1244 → 1244: no corpus template composes such a row or alignment in a cell. The aligned SVG icons in panels inside cells in invoice-payments and invoice-workspace are drawings, drawn as before, with no note.

  • ./mvnw -B -ntp clean verify → BUILD SUCCESS (render-docx 1653, qa 1820).

Lane: shared-engine (render-docx semantic DOCX backend), no public API change.

…the page does

Content composed in a table cell has no place of its own in the layout.
A row the page divides by what its children measure (an arrangement
other than START, a growing spacer, an auto column) had its columns left
to Word, which sized them to their text: an amount set at the cell's
right edge stood at its middle. A block in addAligned(...) was written
from the cell's left: a total aligned right and a centred picture both
stood at the left.

The paragraphs' lines are laid out under the table's path and matched
to them, so composedRowStarts reads where each child of such a row
starts (a spacer where the page spreads the room between its
neighbours) and the row is written by the placed-column path; lines
found in another cell are not taken. placeComposedAcross holds an aligned
paragraph in to its lines and a picture to where the alignment sets one
of its written width. A row holding a child the lines do not place, an
aligned block of any other kind or whose lines stand in another cell,
and either inside a panel or another table in the cell are written as
before and named in the report; an aligned drawing is drawn by its
table where the page draws it.
@DemchaAV
DemchaAV marked this pull request as draft October 15, 2026 22:19
…d a pulled-up line as its cell's

A row of stated columns gives each child its column's slot, a spacer
among them, and the room an arrangement spreads is not there to share:
placed by that share, the spacer made the column before it wider than
the page's slot, and Word could wrap its text elsewhere. Such a row is
now written as before and named.

A paragraph with no text in a composed row stands in a column of its
own, as a spacer does, rather than sending the row back to Word. A line
a container pulls up past its cell's top, or out at a side, is read as
the cell's: its first baseline stands in the cell, and its box crosses
it. An alignment of a row, which is as wide as the width it is set in,
or of an empty paragraph says nothing.

Tests cover a row centred and spread around in its cell, a padded row
with margins, an empty paragraph, a spacer in stated columns, an
aligned row and a pulled-up line; the test's row spacing uses
spacing(double) rather than the deprecated gap(double).
@DemchaAV
DemchaAV marked this pull request as ready for review October 15, 2026 22:55
@DemchaAV
DemchaAV merged commit 155cb09 into 2.5-dev Oct 16, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-composed-row-align branch October 16, 2026 08:22
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