Skip to content

fix(docx): raise a line Word sets low into the space above it, where both editors keep it - #875

Merged
DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-exact-line-seat
Oct 8, 2026
Merged

DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-exact-line-seat

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Why

Word and LibreOffice stand an exact line's baseline four fifths of the way down it whatever the
face; the page sets it the face's ascent below the line's top. Measured on the fifteen faces the
templates use and JetBrains Mono, 8 to 36pt, one line per page, the 0.8 share held for every face
in both editors (Word within 0.04pt in median, inside its 0.12pt grid), and held in every context
of the DOCX fidelity corpus too — 3155 first lines in cells, after tables and in lists, their line
tops read from Word itself.

So the shift the export computes is right; how it applies it was not:

  • Half points. The text was moved by its w:position, which counts in half points, and only
    where the shift was half a point or more. A seat error of a quarter point — Helvetica's lines,
    which the page seats 0.78 of the way down, stood 0.12 to 0.38pt low in Word — is the worst case
    for that step: 0.2 rounds to nothing and 0.3 overshoots to 0.5, so both were left as Word set
    them. Across the corpus the seat left a median 0.22pt in almost every document.
  • LibreOffice scales the position. It moves raised text by the position times the face's
    height over its em — Spectral 1.53, Poppins 1.49, Volkhov 1.31, Carlito 1.18 (measured) — so
    Spectral's 24pt line, raised 4pt, stood 2.4pt high there.

Both editors keep a paragraph's space above as written — LibreOffice to the twip, Word on its
0.12pt grid — so a line moved up into it stands where the page sets it in both.

What changed

In DocxSemanticBackend (and the measurement noted on DocxTextBands.BASELINE_SHARE):

  • raiseIntoTheSpaceAbove: a line Word sets lower than the page by 0.1pt or more (Word's grid
    is 0.12pt; less moves a line a step or nothing) is moved up into its paragraph's w:before by as
    much as that holds, written to the twip, and the same is owed below the paragraph
    (owePendingSpacingAfter), so what follows stays where it was.
  • What the space cannot give stays on w:position, where the whole difference calls for one: a
    line 0.6pt low under 0.2pt of space is moved by its position too, not left 0.4pt low.
  • Only a raise. A line moved down takes its room out of the space below it, not known when the
    line is written; a line Word sets high keeps w:position.
  • Not where the owed space would not reach what follows, or moves the wrong lines:
    • a Word paragraph that is not this paragraph's alone (ownLine — a line pair's, a container's
      stacked lines, a text box's — or one holding other runs first), or a line cut to its pictures;
    • a line in an overlay — a band, a layer stack, a shape container, a canvas, the text laid over
      the flow — whose writers measure what follows them from the page;
    • a paragraph the layout moves to a new page (paragraphOpeningItsPage), whose space above Word
      drops, or breaks over one (onOnePage), whose lines on the next page the space above does not
      move;
    • the space above a panel's first line that the panel's top border takes
      (panelsFirstLineKeeps, from writePanelPiece's border reach less what the padding takes):
      raised into it, the line gave it back and the panel grew by what it owed.
  • Layer columns. A later layer zeroes what the layers above owe and measures its gap from the
    page; the raise the line above it still owes is added to that gap (raiseOwedByTheLastLine), as
    it stood that much higher. A raise paid since — at a cell's end, before a table — is not
    (flushSpacingAfter), and a later layer that writes nothing leaves the raise owed in its cell.

Docs: the recipe's Line height row, the backend capability matrix's paragraph row, CHANGELOG.md
(v2.5.0). The committed preview assets/readme/examples/word-export-companion.docx is
re-rendered: its spaces above and below raised lines move by the raise, nothing else.

Verification

  • ./mvnw -B -ntp clean verify → BUILD SUCCESS: render-docx 1332 run, 0 failures, 1 skipped
    (+14), qa 1820, examples 93, core 818.
  • New tests (DocxBaselineSeatTest, DocxLayerColumnsTest): a low line is raised into the space
    above and owes it below; what the space cannot give goes to the position; a raise under a tenth
    is left; a high line keeps its space; a line pair's first, taller half keeps the shared line; a
    line in a layer stack keeps its space; a paragraph broken over a page keeps its space; what a
    space leaves of a shift over half a point keeps its position; a cell's last line owes its raise
    inside the cell and nothing of it below the row; a panel's first line leaves its top border the
    space above it, the note unchanged; a later layer takes back the raise of the line above, not one
    a card's cell already paid, and a later layer that writes nothing leaves it owed in the cell; and
    the default text's raise (6 twips, Helvetica 14pt; 3 at 7pt) is derived from the layout's seat
    and Word's four fifths.
  • The space tests that write default text read the space above a raised line as that raise less
    (DocxExports.DEFAULT_LINE_RAISE, SMALL_LINE_RAISE), and the space below it as that much more
    where it is flushed: 78 expectations across 21 classes, each the same 6 (or 3) twips.
  • Thirteen sabotages, each caught by its own test: no raise (32 failures in the five classes run),
    lowering through the space above too, nothing owed below, a line pair moving its line, raising
    inside overlays, raising a page-opening paragraph, raising one broken over a page, no floor, the
    resume dropping the raise, a raise paid at a cell's end carried again, the position's threshold
    read off what is left, the panel's border space raised into, an empty later layer dropping the
    raise.
  • DOCX fidelity corpus, Word (word-windows*.tsv, via scripts/docx-visual/word-fidelity.ps1 -Update): 27 documents nearer, the sum of the medians 1.65pt nearer — letter-blue_banner
    0.39 → 0.18pt, letter-mint-editorial-letter 0.18 → 0.03pt, cv-modern_professional 0.32 →
    0.18pt; two a grid step further (below). LibreOffice on Windows (libreoffice-windows*.tsv):
    21 nearer — letter-mint-editorial-letter 0.29 → 0.01pt, cv-modern_professional 0.28 →
    0.04pt — two medians 0.01pt further. LibreOffice on Linux (libreoffice-linux*.tsv, from CI's
    docx-fidelity artifact): the same 21 nearer and the same two 0.01pt further. The report is
    unchanged: the same 969 notes.

Notes for review

  • invoice-luma_studio: its company lines, raised to where the page sets them, now show their
    block standing 0.2 to 0.3pt high in both editors (median 0.13 → 0.14pt); letter-panel's median
    moves 0.14 → 0.16pt, one line a grid step. Splitting each line's drift into the seat and the
    block's position (Word's line tops against the page's) shows such block offsets of 0.3 to 0.7pt
    in about fifteen documents, the same in Word and LibreOffice, which the old seat error partly
    cancelled; they are their own change.
  • A line Word sets high is still lowered by its position, in half points, and LibreOffice's
    scaling still applies to it and to what a space above cannot give.
  • A paragraph in a cell that the layout starts on a new page is raised as any cell paragraph:
    whether Word drops a paragraph's space above where a row's cell continues on a new page is not
    measured. No corpus line moves further for it.

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

…both editors keep it

- Word and LibreOffice stand an exact line's baseline four fifths of the
  way down it whatever the face (measured on the fifteen template faces and
  JetBrains Mono, 8 to 36pt, and in the corpus's lines from Word's own line
  tops). The text was moved to the page's baseline by its position only:
  half points, left alone under half a point, and scaled in LibreOffice by
  the face's height over its em (Spectral 1.53, Poppins 1.49).
- raiseIntoTheSpaceAbove moves a line Word sets low by 0.1pt or more up
  into its paragraph's w:before and owes as much below it, so what follows
  stays; what the space cannot give stays on the position where the whole
  difference calls for one. A line set high keeps its position.
- Not a paragraph that is not its Word paragraph's alone, a line in an
  overlay, a line cut to fit, a paragraph the layout moves to a new page or
  breaks over one. A column's later layer takes back the raise the line
  above it still owes.
- Tests: the raise, what is owed, the floor, the exclusions and the column
  resume; the space tests that write default text allow for its raise
  (DocxExports.DEFAULT_LINE_RAISE, pinned from the layout's seat).
- Word and LibreOffice (Windows) fidelity baselines rewritten: Word 27
  documents nearer, LibreOffice 21; the recipe's Line height row and the
  CHANGELOG.
- assets/readme/examples/word-export-companion.docx re-rendered: its
  spaces around raised lines move by the raise.
… and keep a raise owed past an empty layer

- A panel's first line raised into its space above took the room the
  panel's top border reach comes out of (takeTheTopBorderInside): the line
  gave it back, the panel grew by what it owed, and the note said more than
  the line stood off. writePanelPiece now keeps the border's share of that
  space, less what the padding takes, out of the raise
  (panelsFirstLineKeeps).
- A layer column's later layer that writes nothing no longer drops what the
  raised line above it owes: the cell ends with it.
- raiseIntoTheSpaceAbove checks every condition it documents; the Javadoc
  names overlays (layer stacks among them), a container's stacked lines and
  the panel's border space.
- Docs: the backend capability matrix's paragraph row, the recipe and the
  CHANGELOG list the same exclusions; the corpus figures corrected.
- Tests: a panel's first line leaves its top border the space above it, the
  note unchanged; a later layer that writes nothing leaves the raise owed.
…se into the space above

The CI artifact of the DOCX Fidelity job: 21 documents nearer the page, two
medians 0.01pt further, as on Windows.
@DemchaAV
DemchaAV merged commit 86f8da7 into 2.5-dev Oct 8, 2026
22 of 24 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-exact-line-seat branch October 8, 2026 20:44
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