Skip to content

fix(docx): seat what a row's margins leave of a text cell by the height of its lines - #881

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

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-cell-line-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 a table cell's line its face's ascent below its top. moveTheRowsText closes the gap for a row of text cells by moving the row's top and bottom margins together, by the least any of its cells calls for. Some rows it does not move:

  • a row holding a composed cell, whose content the margins would move too;
  • a row whose cells call for opposite ways;
  • a row in a vertical merge.

What the margins leave was moved by the text's w:position from half a point up, and under half a point not at all.

CobaltRota's staff names sit beside the day cells its chips are composed in. Measured against their own row's rules, they stood:

Word LibreOffice on Windows LibreOffice 24.2
staff names (12) 0.25pt low 0.2pt low 0.19pt low
covers label 0.25pt low 0.2pt low 0.2pt low

What changed

In DocxSemanticBackend:

  • seatByTheLines moves what the margins leave, from a tenth of a point to a half, by the height of the cell's lines. Each exact line is written shorter and the space after it taller by as much. Where Word sets the text high, it goes the other way, as far as the space after holds.
    • The cell's content keeps its height, and each line its top.
    • Both editors stand the text four fifths of the difference higher or lower.
    • A line alone written shorter keeps its row its height in Word and on Windows. LibreOffice 24.2 holds a row to its cells' content: on a probe that wrote the lines alone shorter, the covers values' lines among them, the covers row, whose tallest content those lines were, came out 0.8pt shorter there. The covers values are a composed paragraph, which this change does not touch.
  • Where it applies.
    • Every paragraph of the cell is one exact line, with no line broken in it.
    • The cell is in a face whose letters are read (inkOf); a standard face the export does not embed is read through a stand-in, in that font's units.
    • A line is cut no nearer its letters' tops and feet than DocxStackedLines.INK_MARGIN, where Word cuts an exact line's letters on screen, as a composed chip's label is cut.
  • moveTheRowsText keeps the position from half a point up. Its order is now position, then line, then nothing.
  • The letters are read only for a cell whose call lands under half a point, from the cell the layout measured (textCellsMeasured), not for every text cell.

In DocxInk:

  • of(String, TextStyle, FontLibrary) and readInTheLayoutsUnits(TextStyle, FontLibrary) read a text set in one style. The ParagraphLine overloads now go through them, with the same result.

Docs:

  • CHANGELOG.md (v2.5.0) gets a new entry. It also corrects one sentence of the composed chip label entry: the chips' 0.25pt and 0.15pt offsets are the rows above them; in their own rows the chips stand within a tenth of a point.
  • The DOCX recipe's table paragraph and the capability matrix's table row describe the new rule.

Verification

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

  • DocxCellSeatTest, 23 tests (+9; two existing tests now also assert the line is left as it was). Each new test pins one case:

    • a cell beside a composed one, called for 0.42pt, trades its line for the space after it and stands on the page's baseline, with no position;
    • two lines in their own paragraphs are each cut alike, the space between them kept;
    • a cell Word sets high takes its taller line from the space after it, as far as that space holds;
    • what the margins of a text row leave of a call is moved by the line;
    • descenders near the foot of the line leave it as it was, and Poppins' Å near its top cuts it by less than its call;
    • lines broken in one paragraph, a standard face not embedded, and a call under a tenth of a point are left as they were.
  • Twelve sabotages, each caught by its own test:

    • no trade;
    • the line alone, without the space after;
    • no ink limit, no top guard, no foot guard;
    • breaks ignored;
    • a taller line unbounded by the space after;
    • a centred share of 0.3 in place of 0.8;
    • the first paragraph only;
    • no threshold;
    • a stand-in's letters read;
    • the cut's sign inverted.
  • Probe and corpus, measured against the rules of each line's own row:

    Word LibreOffice on Windows LibreOffice 24.2
    CobaltRota staff names (12), before → after 0.25 → −0.04 0.2 → −0.03 0.19 → −0.08
    covers label, before → after 0.25 → +0.04 0.2 → −0.06 0.2 → −0.06
    rows' pitch unchanged unchanged unchanged
  • DOCX fidelity corpus, against the committed baselines:

    • Word: 3 documents nearer, 0 further, by median; the other 59 DOCX are byte-identical.
      • CobaltRota median 0.17 → 0.15pt, p90 0.38 → 0.31.
      • LumaStudioInvoice 0.14 → 0.11.
      • its long version 0.21 → 0.17.
    • LibreOffice on Windows: 2 nearer, 0 further, by median.
      • CobaltRota 0.08 → 0.06, p90 0.30 → 0.22.
      • LumaStudioInvoice's long version 0.20 → 0.12.
    • Line by line, CobaltRota's lines all move nearer. 21 amounts of LumaStudioInvoice and its long version, 0.07–0.09pt low in LibreOffice on Windows, stand 0.11–0.13pt high there, and 6, 0.04pt low in Word, stand 0.08pt high, a step of its grid.
    • The Word and LibreOffice on Windows baselines are rewritten for those three documents only.
    • Report notes: 969, unchanged.

Notes for review

  • The position keeps what is half a point and more. Written by the line instead, ConsultingInvoice's amounts stood nearer the page in their rows in LibreOffice: in-row −0.18 → +0.07, Word unchanged at −0.02. But the median went from 0.19 to 0.40pt on Windows. Their rows stand 0.3pt low in both editors, and LibreOffice's overshoot of the position made up for it: it moves text by the position times its face's height to its em, 1.4 for Poppins. The rows' drift is its own change.
  • In LibreOffice 24.2 on CI, CobaltRota's median moves from 0.22 to 0.25pt, its p90 unchanged at 0.52, and LumaStudioInvoice's long version's from 0.20 to 0.12; no other document's median moves. 24.2 holds each staff row to its content, a day's block rounded down, so every row is a twip shorter than the page and the rows above the names stand higher. The names, now seated in their rows, no longer make up for that. Each line moves within the fidelity gate's 0.5pt slack, and the DOCX Fidelity check passed on this head, so the Linux baseline is left as it is.
  • CobaltRota's covers values are a composed paragraph, not a text cell, and still stand 0.2pt low in their rows in all three editors. Seating the first line of a composed cell is its own change.

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

…ht of its lines

- What a row's margins leave of a text cell's call, from a tenth of a
  point up to a half, is moved by the height of the cell's lines: each
  exact line written shorter and the space after it taller by as much,
  or the other way as far as that space holds. The cell's content keeps
  its height and each line its top, and Word and LibreOffice stand the
  text four fifths of the difference higher or lower. Under half a
  point nothing moved such a cell before: CobaltRota's staff names,
  beside the day cells its chips are composed in, stood 0.25pt low in
  their rows in Word and 0.2pt in LibreOffice.
- Only where every paragraph of the cell is one line, in a face whose
  letters are read, and no nearer the letters' tops and feet than three
  quarters of a point, where Word cuts an exact line's letters on
  screen. From half a point up the position moves it, as before.
- DocxInk reads the reach of a text set in one style, which the
  paragraph line's reading now goes through.
- Docs: the CHANGELOG entry and a wording fix in the composed chip
  label entry, the recipe's table paragraph and the capability
  matrix's table row.
- Fidelity: the Word and LibreOffice on Windows baselines rewritten for
  CobaltRota and LumaStudioInvoice, both nearer the page.
- Tests: DocxCellSeatTest covers a cell beside a composed one, several
  lines, a line made taller from the space after it, what the margins
  leave of a text row, the letters' foot and top, lines broken in one
  paragraph, a standard face not embedded and a call under a tenth.
…ove further

- The covers row that came out 0.8pt shorter in LibreOffice 24.2 was a
  probe writing the lines alone shorter, the covers values' composed
  lines among them; the CHANGELOG entry and seatByTheLines say so.
- The CHANGELOG entry counts its corpus at the median, and names the
  lines that move further: 21 amounts of LumaStudioInvoice and its long
  version in LibreOffice on Windows, from 0.07-0.09pt low to
  0.11-0.13pt high, and 6 in Word, a step of its grid.
- DocxCellSeatTest: the class Javadoc names the position and the line
  trade, the premises read the line as written, and the top-guard test
  is renamed.
@DemchaAV
DemchaAV merged commit 26be323 into 2.5-dev Oct 11, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-cell-line-seat branch October 11, 2026 20:16
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