Skip to content

fix(docx): count a spacer's hairline in its height - #826

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-editorial
Oct 3, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-editorial

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Why

writeSpacer writes a SpacerNode as an empty paragraph held to an exact line of a tenth of a point (holdToHairline). That keeps Word from giving it a line of text. Then it owed the spacer's whole height as the space above the next block, so every spacer stood 0.1pt taller than the page's.

Word keeps a line that short. In a hand-made document, eleven exact 12pt lines with a 2-twip empty paragraph between each pair are set 12.098pt apart on average in Word, against 12.002pt without the paragraphs. It is the same in the body and in a table cell, with or without an empty run in the paragraph.

EditorialBlue shows the effect most.

  • Its entries are held apart by spacers, and its first page ran lower in Word by 0.1–0.2pt an entry, 1.2pt by the foot.
  • In the PDF, the last entry's heading, "Backend Engineer 2019-2021", fits at the foot of page 1 with half a point to spare. In Word it found no room there.
  • Word moved the heading to page 2, and that page stood 10pt low.

separateFromTheTableAbove already takes its 0.1pt out of the gap it stands in.

What changed

  • writeSpacer. The spacer owes its height less its hairline (SEPARATOR_POINTS), floored at 0. The hang and pull reductions after it are unchanged.
  • New tests in DocxContainerSpacingTest.
    • aSpacerOverATableHoldsTheRestOfItsHeightBelowItsHairline: a 10pt spacer over a table writes 2 twips of line and 198 below it, the spacer's 200. Nothing between them takes the hairline out a second time.
    • aSpacerUnderATableInACellIsTheTablesCloserAndItsHeightOnce: in a card's cell, the paragraph closing the table becomes the spacer's. Line and space come to 200 twips, with no separator.
    • aSpacerIsItsHeightAndNotALineOfTextAsWell now pins the line at exactly 2 twips.
  • Tests that pinned the spacer's height above the next paragraph now count the hairline in it:
    • DocxContainerSpacingTest.aSpacerIsItsHeightAndNotALineOfTextAsWell: the hairline and the space above the next entry add up to the spacer's 4.5pt.
    • DocxVerticalSpacingTest.aPullComesOutOfASpacerUnderIt.
    • DocxSpaceAboveOnANewPageTest's filler spacer, through an owed(points) helper.
  • Baselines. word-windows, libreoffice-windows and their -lines files are rewritten. 17 of the 62 documents move in Word (16 in LibreOffice), each line by a tenth of a point per spacer above it on its page: exact multiples of −0.1pt in LibreOffice, and steps of 0.12pt in Word.
    • Linux LibreOffice. libreoffice-linux and its -lines file are taken from this PR's CI run (docx-fidelity-37112941286, on 855d2c3; the review commit changes only tests and a comment).
  • Docs. CHANGELOG.md, the Spacers row of docs/recipes/docx-export.md, and the Javadoc of holdsNothing. A spacer's paragraph carries an empty run, so it counts as content in a cell.

Verification

  • Template corpus, re-measured in full in both editors.

    Word 16.0.20430 LibreOffice (Windows)
    Lines past 2pt 28 → 19 624 → 615
    Mean |drift|, per line 0.494 → 0.450pt 1.848 → 1.807pt
    Lines better by > 0.05pt 504 455
    Lines worse by > 0.05pt 124 154
    EditorialBlue past 2pt 9 → 0 (p90 10.00 → 0.63pt) 9 → 0 (p90 10.06 → 0.52pt)

    Page counts are unchanged, and no line is lost. EditorialBlue now has all 82 of its lines matched in both editors (81 before), because the heading is back on page 1.

    The biggest gains in Word are EditorialBlue (mean |drift| 1.62 → 0.38pt), CenteredHeadline (0.34 → 0.13), ClassicSerif (0.31 → 0.16), InvoiceConsulting (1.00 → 0.85) and Panel (0.50 → 0.36).

  • Documents that get worse in Word.

    • MinimalUnderlined: mean 0.56 → 0.65pt. It already stood high, by an offset that starts above its first spacer and that the extra tenth per spacer partly cancelled. Its entries now step as the page does.
      • Its summary already stands 0.57pt high, and its skills 0.7pt.
      • Its education entries now stand at about −0.85pt, and its project entries at about −1.3pt, each within 0.1pt of the others, where each used to step 0.1pt lower than the one before.
    • ProfessionalSidebar: mean 0.12 → 0.17pt.
      • Its main column starts level, and its spacers come after text hanging below a band. The extra tenth hid a deficit of about 0.05pt per block elsewhere, so it now drifts high block by block.
      • One line goes from +0.03 to −0.57pt, past the gate's 0.5pt.
      • In LibreOffice it improves (median 2.71 → 2.59pt).
    • BoxedSections and Executive: mean 0.29 → 0.30pt and 0.20 → 0.22pt. Both show the same pattern as MinimalUnderlined: the steps flatten over an offset of about 0.2pt high that is already there.
  • Each test fails when the change is taken out. With the spacer's whole height owed again, the three DocxContainerSpacingTest spacer tests and DocxVerticalSpacingTest.aPullComesOutOfASpacerUnderIt fail.

  • 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. render-docx runs 835 tests, qa 1814.

Known limits

  • The offset MinimalUnderlined starts with, and ProfessionalSidebar's per-block deficit, are not found yet. MinimalUnderlined's summary already stands 0.57pt high. Both are next in the queue.
  • A spacer shorter than its hairline still stands a tenth of a point.

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

@DemchaAV
DemchaAV merged commit 56344de into 2.5-dev Oct 3, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-editorial branch October 3, 2026 10:04
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