Skip to content

fix(docx): stand OrangeOps on one page in Word — room for a one-line heading, and a negative bottom edge kept - #810

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-orange-ops
Oct 1, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-orange-ops

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Why

OrangeOps ran to two pages in Word, and 6 of its 108 lines were found in Word's rendering. Its two columns are one table row Word keeps whole, and the page fills to within 2pt. In Word the row grew 51pt and the masthead 4pt, so the whole row went to a second page.

  • Its headings broke. "ACHIEVEMENTS", "EDUCATION" and "CERTIFICATIONS" are Oswald SemiBold, each in a box as wide as the word. The face's file says it is a regular face of a family named "Oswald SemiBold", weight 600. Word sets it about 1% wider than the page does, as Oswald,Bold, and broke each heading onto a second line.
  • Its role bar stood low. The bar is pulled 3.8pt up inside the name's line by the name's negative bottom margin. The export dropped a negative bottom edge.

What changed

  • wordsMeasure: a paragraph of one line has room for a line three hundredths wider than its own (ONE_LINE_FACE_SLACK), through its right indent.
  • applyVerticalSpacing / newBodyParagraph: a negative bottom edge pulls the next paragraph up (pullBelow). That covers a paragraph's, an image's, a barcode's and a rule's.
    • It comes out of the space above that paragraph, its own top edge included, as the page sums the two. That holds even out of the container the first stands in.
    • What that space cannot give is not written: Word has no negative space above a paragraph.
    • Before a table, a page break or a cell's end, flushSpacingAfter takes it out of the space owed below.
    • A spacer under the pulling paragraph takes what the space above it could not give out of its own height.
    • Layers that overlap take none of it from one another: a band's next layer and its end, and a layer column's next layer, measure from the page's boxes. The layers drawn over a columns table keep the pull they found.
    • It is reset with the rest of an export's state, and kept and restored around a cell.
  • Baselines. word-windows and libreoffice-windows are rewritten for the documents this moves nearer the page.
  • Docs. CHANGELOG.md, and docs/recipes/docx-export.md (the negative-edge paragraph, and the half-point measure).

Verification

  • Template corpus, line by line against 2.5-dev's baselines. Both gates pass: no line is lost or set further from the page, and no page count moves further from the engine's.

    Editor Document Pages Lines found Past 2pt Median
    Word 16.0.20430 OrangeOps 2 → 1 6 → 99 of 108 5 → 2 3.95 → 0.42pt
    LibreOffice (Windows) NorthlineProposal 2 131 → 132 34 → 6
    LibreOffice (Windows) OrangeOps 2 5 5 81.97 → 77.92pt

    No other document changes in either editor. "Lines found" counts the lines the gate pairs with the page's.

  • New tests.

    • DocxVerticalSpacingTest:
      • a −4pt bottom edge, out of its section, takes 4pt off a 10pt top edge;
      • a −4pt pull against a 2pt edge writes nothing;
      • 2pt of padding owed, a −4pt pull and a 10pt edge write 8pt;
      • before a table, a −4pt pull takes 4pt off 10pt owed below;
      • a 10pt spacer under a −4pt pull holds 6pt;
      • a table between them drops the pull.
    • DocxOverlayBandTest:
      • a layer's −4pt bottom moves neither the next layer nor the shape container's next paragraph;
      • the last layer's −4pt bottom is taken once off the paragraph under the band.
    • DocxWordSizeMeasureTest: a line of one as wide as its box is given room past it, and one with room to spare is not.
  • Each rule fails its own test when taken out.

    • Dropping the pull fails the two pull tests. A zero slack fails the box test.
    • Leaving the pull out of flushSpacingAfter, or out of the spacer, fails the table and spacer tests.
    • Keeping the pull at a band's next layer, or at its end, fails the layer tests.
  • Re-measured after the layer and spacer rules. The Word and LibreOffice corpus baselines re-record byte-identical, so no template reaches those paths.

  • DocxPanelInsetTest. Two assertions now allow a right indent only at or past the box. They still allow no left indent. A heading as wide as its panel now has room past it on the right.

  • render-docx runs 778 tests. The committed word-export-companion.docx preview exports unchanged (document.xml byte-identical).

  • 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 (qa 1811).

    • After install, examples run 93 green.
    • The knowledge checks pass.

Known limits

  • OrangeOps still runs to two pages in LibreOffice, which sets its body taller for its own reasons.
  • The Linux LibreOffice baseline is rewritten from this PR's CI run.

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

…ll the next paragraph up by a negative bottom edge
…nd none of it between overlapping layers

Before a table or a page break the pull now comes out of the space owed
below the paragraph, and a spacer under it takes what the space above it
could not give. A band's next layer and its end, a layer column's next
layer, and the layers drawn over a columns table no longer carry the pull,
as the space there is measured from the page's boxes.
@DemchaAV
DemchaAV merged commit 63949a2 into 2.5-dev Oct 1, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-orange-ops branch October 1, 2026 22:42
DemchaAV added a commit that referenced this pull request Oct 2, 2026
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