Skip to content

fix(docx): stand text on the page's baseline in Word's exact lines - #789

Merged
DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-northline-icons
Sep 30, 2026
Merged

DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-northline-icons

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Why

In Word, NorthlineProposal's section headings stood 8pt below the badge icons drawn beside them, and so did everything else on its cover. The icons are drawn where the page puts them; the text in the flow was low for two reasons.

  • Where the baseline stands in an exact line. Word stands it four fifths of the way down the line, whatever the face. The page sets it the face's ascent below the line's top. Measured through Word COM, the ratio is 0.80 (±0.1pt) for Spectral, Lato and Arial at 10 to 46pt, in lines 12 to 100pt tall. LibreOffice lands on 0.80 exactly. For Lato (ascent 0.82 of its line) the two nearly agree; for Spectral (0.70) they do not, and its 46pt title stood 7pt low.
  • Stacked title lines ran past their container. The title's three lines are laid 48pt apart in a 140pt container. They were written 70, 48 and 48pt tall: the first at its own height, each next one at the distance between the two lines' feet. The stack ran 26pt past the container; 18pt of that came out of the gap under it, and the remaining 8pt pushed the cover down.

What changed

  • shiftToThePagesBaseline. A paragraph's runs are moved (w:position) by 0.8 × the written exact line less the page's baseline depth in that line, added to the TextVerticalAlign seat of fix(docx): seat text off its baseline as the page does and hang text below its band #787 in seatInTheLine. The page's depth is measured from where Word's line starts:

    • a line pair's line starts at the higher text's top, so each half is seated from there (lineTopAbove, from the new DocxLayoutMetrics.firstLineTop);
    • lines that took their gaps from the space above start that much higher (applyLineGap records it);
    • several lines are matched at the middle one. Where the space above cannot give a whole gap, Word's lines step a little closer than the page's, and the first and last share the error.

    A paragraph not written at an exact height is left to Word. A difference under half a point is left too: a line of Lato body text is a quarter point off, and moving every such line would win a quarter point at most. List items and table text cells are not moved yet.

  • DocxStackedLines. Word draws an exact line's text on screen only inside the line, although its PDF export draws it whole. Lines a pitch tall cut the title there: "Proposal" lost its descenders, "Brand Refresh" the tops of its capitals.

    • So each line of a stack now ends halfway between its own letters and the next line's. The letters' reach is read from the glyph outlines in the fonts the layout measured with (new DocxInk).
    • Each line's text is seated from where its Word line starts. Where the last layer of a shape container on one page runs past the container's foot, it ends at the foot, or below its letters where they hang past it; hangBelowItsBox takes that overhang from the gap below.
    • Where the page sets two lines' letters into each other, the edge still falls halfway: both lose what they share, and the page does not move. A stack whose letters cannot be read, or that holds a picture, keeps its lines' own heights, and a line with space above or below it stays out of a stack. The NaN path for plain containers ends the last line where its own line ends.
    • EditorialProposal's 41.8pt title, set 41.9pt apart, leaves a point between the "p" of one line and the capitals of the next, and stacks.
  • DocxTextBands.BASELINE_SHARE now documents the measurement, and is the one constant both uses read.

  • Docs. CHANGELOG.md, the paragraph row of backend-capability-matrix.md, and the line-height and stacked-layers text of docs/recipes/docx-export.md are updated; the earlier v2.5.0 entry for stacked lines is amended to the new rule. A picture among moved text moves with it in Word; LibreOffice keeps a picture on its own baseline, where it stood before.

Verification

  • Full reactor gate: ./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-qa,:graph-compose-coverage -am gives BUILD SUCCESS (1791 + 127 tests). After install, examples are 93 green, and the knowledge checks and extract-api --check pass.

  • DocxBaselineSeatTest (new, 5 tests). Each part of the seat was removed in turn, and each removal failed only its own test.

    • A 30pt Spectral line is raised by 0.8 × line − ascent.
    • A 10pt Lato line is left alone.
    • A line pair's lower half is seated from the line's top.
    • Lines that took their gap from above are seated from the higher top.
    • Two lines with a shared-out gap are seated at the middle one.
  • DocxStackedLayersTest. Each line of a stack, the NorthlineProposal title's included, stands on the page's baseline within 0.3pt, and its letters' tops and feet lie inside its Word line. With the edges put back at the page's line tops, "Proposal"'s feet fail.

    • A stack as tall as its box leaves the whole gap under it.
    • Letters hanging past the foot take exactly their overhang from the gap.
    • A last line that is not a stack's still hangs into the gap. Lines whose letters overlap are split halfway and end where the page's line does, and a picture line keeps its own height. A stack line seated at its top holds its raised letters. A picture on the baseline takes its text's move (DocxBaselineSeatTest), and a line holding a picture has no letters to read (DocxInkTest).
  • DocxInkTest (new). A line's reach matches Spectral's own x and p outlines read with fontbox, and is less than the face's ascent plus descent.

  • Word on screen. A capture of Word's window, from a private visible instance, shows both titles and EditorialProposal's "NORTHLINE / STUDIO" whole. Before this change, they were cut.

  • DocxVerticalSeatTest now measures TOP / CENTER / BOTTOM from the DEFAULT seat, and checks that a label keeps its position when the value beside it is seated.

  • Template corpus (62 documents), converted to PDF by Word, baselines matched by text:

    median drift lines > 2pt off
    all 62 (3584 lines) 1.11 → 0.94pt 1464 → 1341
    NorthlineProposal 8.0 → 0.8pt 103 → 50
    EditorialProposal 5.9 → 0.4pt 71 → 19

    NorthlineProposal's cover is within 1.5pt of the page throughout (at most 1.3pt), and its headings level with their icons. No document's median got worse by more than 0.15pt. In LibreOffice the corpus median falls from 2.01pt to 1.60. Page 2's remaining drift is its table rows, each about 0.7pt taller in Word — a separate change.

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

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 down. Runs are moved to the page's baseline where the two are half a point or more apart, and lines a container stacks tighter than their face are written a pitch tall, the last to the container's foot.
… whole

Word draws an exact line's text on screen only inside the line, though its PDF export draws it whole. Lines a pitch tall cut the title's descenders and capitals; each line of a stack now ends halfway between its letters and the next line's, read from the glyph outlines.
… the page still

A stacked line seated off its baseline has its letters' reach read where the page sets them. Lines whose letters meet are still split halfway, so the page does not move; a stack line is seated however small its shift; letters hanging past a container's foot are always taken from the gap below; a line with space above or below it stays out of a stack.
@DemchaAV
DemchaAV merged commit a2b639c into 2.5-dev Sep 30, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-northline-icons branch September 30, 2026 12:03
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