Skip to content

fix(docx): raise a cell's first line into its padding, and a row's out of the space above it - #882

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-panel-cell-seat
Oct 12, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-panel-cell-seat

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Why

Word and LibreOffice stand an exact line's baseline four fifths of the way down it. The page sets it its face's ascent below the line's top. A line Word sets lower is moved up into the space written above it, and that space is owed below it so what follows stays. Under half a point nothing else moves it: the position counts in half points.

A line opening a cell has no space above it of its own:

  • a panel's padding and a table cell's padding are the cell's margin;
  • a row's columns have their space above written above the row's table.

So those lines stayed as low as Word set them. By the export's own measure of Word's seat, 250 lines of the corpus stood so, a median 0.22pt low. Measured against its panel's bar, ClassicInvoice's "TOTAL DUE" stood 0.18pt low in Word and 0.2pt in LibreOffice.

What changed

In DocxSemanticBackend:

  • raiseIntoTheSpaceAbove takes what the space above cannot give from the cell's top padding (paddingAbove), under half a point only. The line still owes what it rose below it.
    • A panel gives its padding less what its top border's reach takes (panelsMarginKeeps). Its row is held as much taller: Word adds the margin left to the height a row is held at.
    • A table's composed cell gives its padding in a row whose margins even (cellsGivingTheirPadding). evenTheRowsMargins then gives the cells beside it the difference as space above their first lines.
    • A row's margins even only where every cell's content is known to open with a paragraph (opensWithAParagraph): text, a paragraph, a list, a picture, or a spacer keeping no other layer's place. It used to look for a table alone, but a row of columns, a painted container and a shape are each written as a table first too, and such a cell keeps its margin. Word gives every cell the row's largest margin, so a line beside such a cell rising into a smaller one would not rise, and would leave what it owed below.
    • Half a point and more stays the position's. Taken from the padding instead, CobaltRota's 3.4pt masthead raise, whose position LibreOffice overshoots, came out 0.5 to 0.7pt lower there.
  • A paragraph in a table's composed cell is raised into its own space above (inARowKeptWhole). The cell has no placement, so layout.onOnePage said no. Its row is kept on one page (rowsPlacedWhole, or cantSplit).
  • A row of columns' first row is lifted out of the space above its table (noteAFirstLineLow, raiseRows, liftEachCell).
    • The lift is the most any of the row's first lines calls for.
    • Each line rises by its own call, and the space under it takes that back (giveBackBelow): above the next paragraph, or after the line where none follows.
    • Not a row the layout moves to a new page (rowOpeningItsPage), a row held to a height, or a row whose column opens with a panel or a table, which would rise whole.
    • The paragraph above a lifted table counts as an empty separator only when it holds no picture either (takesRoom): its space above places the picture.

Docs:

  • CHANGELOG.md (v2.5.0) gets a new entry.
  • The DOCX recipe's line height row and the capability matrix's paragraph row describe the rule and its limits.
  • assets/readme/examples/word-export-companion.docx is re-rendered: its row of columns is lifted 4 twips (CommittedAssetDriftTest).

Verification

  • ./mvnw -B -ntp clean verify → BUILD SUCCESS: render-docx 1410 run, 0 failures, 1 skipped (+19); qa 1820, core 818, examples 93.

  • DocxFirstLineSeatTest, 19 new tests, one case each:

    • a panel's line rises into its padding, the row held taller; its top border keeps its reach;
    • a line half a point low and more keeps its position;
    • a composed cell's line rises into its padding, beside a picture too; the cell beside it keeps its place; a cell beside one opening with a panel keeps its padding;
    • a panel split over a page keeps its padding;
    • a line under the first in a composed cell rises into its own space above;
    • a row's lines rise out of the space above its table, and out of the foot of the row of columns above;
    • a picture above a row keeps its space above;
    • a cell calling for less starts that much lower, and one Word sets high by the whole lift;
    • the space given back goes above the next line;
    • a row under a tenth of a point is not lifted, nor one on a new page, one held to a height, or one whose column opens with a panel.
  • Tests that pin a panel's or a cell's top margin, or the space above a row, now count the raise.

    • DocxPanelTailTest, DocxContainerPaintTest, DocxContainerSpacingTest, DocxComposedCellTest and DocxVerticalSpacingTest count DocxExports.DEFAULT_LINE_RAISE.
    • In DocxSpaceOnANewPageTest the row takes back what the line above it owes for its raise.
    • DocxStandInMoveTest reads the rise off the strip's margin: the name rising out of the strip's padding owes the subtitle the space it rises into.
    • In DocxPanelTailTest, reachBelow reads the space the line above owes before the icon's line.
  • Twenty-three sabotages, each caught by a test:

    • no padding at all;
    • no panel padding; no table cell padding; no border reserve;
    • the panel not held taller;
    • the padding under the position too;
    • no whole row; placed rows not noted;
    • no lift;
    • lift whatever the row; lift a held row; lift a column opening with a panel; lift a row opening its page;
    • give back above; no line rises in the lift; lift by the least;
    • no lift threshold; give back to the line itself;
    • a table first the only thing that keeps a margin; a picture beside the line keeping its margin; a split panel gives its padding; a picture as a separator; no give-back under a lift from the foot.
  • DOCX fidelity corpus, against the committed baselines (by median):

    nearer further
    Word 12 0
    LibreOffice on Windows 10 1 (LumaStudioInvoice, 0.12 → 0.14pt)
    LibreOffice 24.2 (container) 9 1 (LumaStudioInvoice, 0.12 → 0.14pt)
    • Line by line, moved by 0.05pt or more: Word 224 nearer, 56 further; LibreOffice on Windows 194 and 25; LibreOffice 24.2 177 and 25.
    • On CI, against the same check's run for the previous change: LibreOffice 24.2 moves 10 documents nearer and 1 further by median (LumaStudioInvoice, 0.12 → 0.14pt), 194 lines nearer and 25 further. Its acceptance summary is unchanged.
    • ClassicInvoice's header, "BILL TO", "FROM" and "TOTAL DUE" rose 0.24 to 0.36pt in Word and 0.2 to 0.4pt in LibreOffice.
    • ConsultingInvoice's line items rose 0.24 to 0.36pt in Word and 0.35pt in LibreOffice.
    • ModernProfessional's entries rose 0.24pt in Word and 0.2pt in LibreOffice.
    • The Word and LibreOffice on Windows baselines are rewritten from the measured run. The base run reproduced the committed ones row for row, so only the documents whose lines moved change.
    • Report notes: 969, unchanged.

Notes for review

  • LumaStudioInvoice is further by median in LibreOffice. Its line-items table stands 0.13 to 0.16pt high there, at its header's fill and at each row's rule. Its first lines, set low, made up for that. In their rows the header and the item titles now stand within 0.11pt of the page's in Word and 0.04pt in LibreOffice, from 0.13 to 0.25pt low in Word. The rows' drift is its own change.
  • In Word, a text cell beside a composed one that rose can stand a step of its 0.12pt grid off, either way. Its share of the row's margin is written as space above it. 53 of Word's 56 lines further are LumaStudioInvoice's, and the other 3 a step of its grid each. LibreOffice moved only the raised lines.
  • ClassicInvoice's notes and payment terms still stand 0.2 to 0.26pt low in their rows. Their columns are ruled down their left side, so each is a panel in its cell with no padding above its line, and the row is not lifted.
  • Under LibreOffice 24.2, a panel or a table row held taller than its text comes out taller by the raise, under half a point. That is one with a drawing set where the page puts it: 24.2 counts the margins inside a row's height. No corpus document shows it.
  • A row set from the middle or the bottom takes the lift back as one set from the top does. In a probe of a centred and a bottom-aligned row, the cell calling for nothing got the lift as space above its line. It stood where it did without the lift in Word, LibreOffice on Windows and LibreOffice 24.2, and the line that rose stood on the page's baseline.
  • The LibreOffice 24.2 baseline is not rewritten. The DOCX Fidelity check holds each line within half a point, and this change moves none further than that.
  • A list item's line is not seated yet. That is its own change.

Lane: shared-engine (DOCX backend) — render-docx; no public API.

…t of the space above it

- A line opening a cell has no space above it of its own, and under
  half a point one Word sets low was left there: a panel's padding and
  a table cell's are the cell's margin, and a row's columns have their
  space above written above the row's table. ClassicInvoice's "TOTAL
  DUE" stood 0.18pt low in its panel in Word and 0.2pt in LibreOffice.
- Under half a point, what the line's space above cannot give comes out
  of the top padding of the cell it opens: a panel's, less what its top
  border takes, the panel's row held as much taller; a table's composed
  cell's, in a row whose margins even. The line owes as much below it,
  so what follows stays.
- A row of columns' first row is lifted out of the space above its
  table by the most its cells' first lines call for, each line rising
  by its own and the space under it taking that back. Not a row the
  layout moves to a new page, held to a height, or whose column opens
  with a panel or a table.
- A paragraph in a table's composed cell is raised into the space
  above it as anywhere else: its row is kept on one page.
- Half a point and more stays the position's.
- A row's margins even only where every cell opens with a paragraph:
  a row of columns, a painted container and a shape are written as a
  table first too, and keep their margin. The paragraph above a lifted
  table is an empty separator only where it holds no picture.
- Docs: the CHANGELOG entry, the recipe's line height row and the
  capability matrix's paragraph row.
- Fidelity: the Word and LibreOffice on Windows baselines rewritten for
  the documents whose lines moved.
- Examples: the Word export companion's committed DOCX re-rendered, its
  row of columns lifted 4 twips.
- Tests: DocxFirstLineSeatTest covers a panel's line and its top border,
  a line half a point low and more, a composed cell's line and the cell
  beside it, a line under the first in a composed cell, a row's lift, a
  cell calling for less, one Word sets high, the space given back above
  the next line, a call under a tenth, a lift out of the foot of the
  row above, a cell beside a badge, a split panel, a picture above a
  row, and rows on a new page, held or opening with a panel. Tests that pin a panel's or a cell's top margin,
  or the space above a row, count the default line's raise.
…e the held table row

- A cell opening with a picture, or a spacer keeping no other layer's
  place, writes a paragraph first: its row's margins even again, as
  before the first-line raise, and the gap above a wrapped first line
  is taken from the cell's padding beside it.
- Javadoc and docs: a table row is held at its height less the margins
  left, so LibreOffice 24.2 grows a table row held taller than its text
  by the raise, as it does a panel; the line height paragraph's rules
  for the panel's padding and for the space a line's position leaves
  now name the new sources; the row of columns' lift skips a line in
  any overlay.
- Tests: DocxFirstLineSeatTest covers a cell beside a picture.
@DemchaAV
DemchaAV merged commit 8b9cdc4 into 2.5-dev Oct 12, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-panel-cell-seat branch October 12, 2026 07:40
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