Skip to content

fix(docx): align table cells where the page aligns them - #735

Merged
DemchaAV merged 2 commits into
2.5-devfrom
feature/docx-cell-vertical-align
Sep 23, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
feature/docx-cell-vertical-align

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Why

A table cell's textAnchor says where the page places its content in the cell's box, and the DOCX export read neither half of it.

  • Horizontal. 23 template and example files align columns with it: 38 CENTER, 18 CENTER_RIGHT, 20 TOP_LEFT and others. Every amount, quantity and total column the page right-aligns or centres came out flush left in Word.
  • Vertical. Word's default is the top of the cell, and the engine's is the vertical middle (TableCellLayoutStyle.DEFAULTAnchor.centerLeft()). A single line beside a taller neighbour sat at the top of its row.

What changed

  • resolveCellAnchor resolves the anchor through the same cascade the layout merges: table default, column, row, cell. With none stated it uses the engine's default:
    • CENTER_LEFT;
    • CENTER_RIGHT for a right-to-left cell, as TableLayoutSupport.answerDirection gives it.
  • applyVerticalAnchor writes w:vAlign on every cell:
    • CENTER_*center;
    • BOTTOM_*bottom;
    • TOP_* → not written, since it is Word's default;
    • DEFAULTbottom. The engine maps DocumentTableTextAnchor.DEFAULT to Anchor.defaultAnchor(), and PdfTableRowFragmentRenderHandler.resolveTextLines and the composed-cell placement both put its vertical half at the bottom.
  • A text cell's paragraph gets w:jc from the horizontal half, through the existing toAlignment. toAlignment swaps left and right for a w:bidi paragraph, because Word reads them as the flow's start and end. The left (the start) is not written.
  • A composed cell takes the vertical half only. NodeDefinitionSupport places a composed child vertically by the anchor and never across, since the child is laid out at the cell's full inner width. The paragraph keeps its own alignment.
  • An AUTO cell's direction is read from the text as the layout reads it. TableLayoutSupport flattens a break inside a line to a space before it looks for the first strong character, and the export read the raw line. A cell reading "123\nשלום" was drawn right to left and written left to right, and its default alignment followed.
  • Committed preview updated. word-export-companion.docx gains w:vAlign="center" on its 30 cells. Its document.xml differs by those lines and nothing else, and the package holds the same parts.
  • The recipe's Tables row, the capability matrix table row and the v2.5.0 CHANGELOG describe the mapping.

Verification

In an editor. A probe with three columns, rendered by the engine and converted with LibreOffice:

  • fixed widths;
  • a centred Qty column and a CENTER_RIGHT Amount column;
  • a three-line item beside single-line cells;
  • a BOTTOM_RIGHT cell and a two-line right-aligned cell;
  • a right-to-left table with one default cell and one CENTER_LEFT cell.

The quantity and the amount sit centred beside the three-line cell, and "Bot" sits bottom-right, as on the page. The right-to-left default starts on the right, and the authored left lands on the left. The engine draws Hebrew in Helvetica as ?, but its placement is the same.

Tests. ./mvnw -B -ntp test -pl :graph-compose-render-docx354 run, 0 failures.

New DocxCellAnchorTest (9):

  • a cell with no anchor → vAlign center, no jc;
  • all eight anchors → their vAlign/jc;
  • the cell's anchor beats the column's, and the row's beats the column's;
  • a spanning cell's anchor on every row it covers;
  • right to left: the default writes no jc, an authored left writes jc right, and DEFAULT sits bottom left;
  • an AUTO cell whose line opens on digits then Hebrew is right to left;
  • a composed cell → vAlign bottom for BOTTOM_RIGHT, and its paragraph stays left.

Each of these was sabotaged and fails its test:

  • dropping vAlign (3 tests);
  • dropping jc (3 tests);
  • a left default for right-to-left cells;
  • mapping DEFAULT to top;
  • putting the column after the row in the cascade;
  • vAlign on the span's first row only;
  • reading the raw line for the direction.

Gate. Full reactor gate (clean verify over core, render-pdf/docx/pptx, templates, testing, qa, coverage) → BUILD SUCCESS (qa 1788 green). Examples after install: 93 green, with word-export-companion.docx regenerated by WordExportExample (its document.xml gains the 30 vAlign lines and nothing else).

Not in this change: the engine sizes a multi-line cell's row with lineSpacing between the lines, but the PDF draws them stacked without it. The DOCX follows the PDF. The mismatch is inside the engine and is left as a separate fix.

Lane: shared-engine (render-docx). No public API change.

The export read neither half of a cell's textAnchor. Every column a
template right-aligns or centres came out flush left, and since Word's
default is the top of the cell while the engine's is the vertical middle,
a single line beside a taller neighbour sat at the top of its row.

The anchor, cascaded as the layout cascades it, is now w:vAlign on every
cell and w:jc on a text cell's paragraph, with the engine's default when
none is stated: middle left, or middle right for a right-to-left cell,
and DEFAULT at the bottom left as the renderer draws it. A composed cell
takes the vertical half only, as the engine places it.

The committed word-export-companion.docx gains w:vAlign="center" on its
30 cells and nothing else.
The layout flattens a break inside a cell's line to a space before it
looks for the first strong character; the export read the raw line. A
cell reading "123\nשלום" under TextDirection.AUTO was drawn right to left
on the page and written left to right in Word, and its default alignment
followed. The export now reads the lines the same way.

Adds coverage for a row's anchor beating its column's, an anchor on every
row a spanning cell covers, a right-to-left cell anchored at DEFAULT, and
the auto direction above. Splits the recipe's Tables sentence in two.
@DemchaAV
DemchaAV merged commit 65c2231 into 2.5-dev Sep 23, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the feature/docx-cell-vertical-align branch September 23, 2026 20:27
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.

1 participant