Skip to content

fix(docx): draw a one-page section's backgrounds from the body, so LibreOffice sets it where the page does - #833

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

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-lo-sidebar-header

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Why

The DOCX export paints a session's page backgrounds as page-anchored rectangles in the section's header. When the section has no header of its own, it gets an empty one against the page edge to carry them.

LibreOffice gives a header a height of its own, however little it holds, and sets the body that much lower. Every line of the seven sidebar CVs, whose column is a page background, stood 2.6 to 3.3pt low in LibreOffice: SlateOrange, CharcoalGold, MidnightNavy, NavySidebar, ProfessionalSidebar, SidebarPortrait and MonogramSidebar. That came to 479 of the corpus's 606 LibreOffice lines more than 2pt off. In Word there was no shift.

Just dropping the header does not work. A page laid out as one table opens with a hairline paragraph, which carries the page's shapes because the body has no other paragraph. Over a row as tall as the page, that 0.1pt pushed the row onto a second page in LibreOffice.

What changed

  • DocxSemanticBackend.applyPageBackgrounds. A section whose layout is one page, with no header and no footer, writes no header. Its fills are kept for the body.
  • reportDrawingsLeftOver anchors those fills on the page:
    • in its first body paragraph;
    • or, when the section opens with a table, in the first table-cell paragraph on the page. That paragraph also carries the page's other pending shapes, in place of the hairline, so no paragraph stands over the row;
    • otherwise in the hairline or the closing paragraph. The closing paragraph is now created once, however many callers ask for it.
  • DocxPageBackgrounds.drawing(…, inHeader). In the body a fill is layoutInCell="0" and not locked, so it is laid out from the page even when its anchor is a cell's paragraph. It keeps its 1024·n stack height, under every other body shape (251658240+).
  • Footer excluded. A section with a footer keeps the header path, because Word paints the body's shapes over a footer's text. With the first version of this change, MeteredInvoice's footer band hid its footer text, and ConsultingInvoice's and RotaCobalt's footers vanished too.
  • New accessors. DocxDrawingAnchors.bodyParagraphOn / cellParagraphOn.
  • Docs. The recipe, the capability matrix and the CHANGELOG state the rule and its limit.

Shapes anchored in a cell: #768 recorded Word printing such a shape clipped to the cell. With Word 16.0.20430 I could not reproduce that, in either Word's PDF export or a screen capture of the document. The cases checked were:

  • the two-column sidebar case of DocxDrawingsTest with a page background (the ring stands outside the sidebar cell it is anchored in);
  • the seven CVs, compared page by page against the engine.

The page-0 shapes of these seven now ride in their first cell.

Verification

  • LibreOffice (Windows), -Dgraphcompose.docxFidelity=libreoffice.
    • The seven CVs' medians fall from 2.59–3.27pt to 0.11–0.42pt, with 0 lines past 2pt (were 49–86 each).
    • Corpus lines past 2pt: 606 → 127.
    • Nothing else moved. Baseline rewritten with update=true; the Linux baseline is this PR's CI artifact docx-fidelity-37149422121, which moved the same seven CVs the same way (corpus 724 → 245 lines past 2pt) and nothing else.
  • Word corpus. scripts/docx-visual/word-fidelity.ps1 → BUILD SUCCESS, baseline rewritten with -Update.
    • The seven CVs' lines stand 0.04–0.17pt higher without the hairline. Their mean |offset| is 0.276pt before and 0.287pt after, and lines past 2pt stay at 0.
  • Exported DOCX. Only the seven CVs changed among the 62 corpus documents. The invoices with footers are unchanged.
  • Visual check.
    • Engine, Word and LibreOffice pages for all seven: fills, rails, rings, badges and icons are in place.
    • Word-before vs Word-after pixel diffs show only the 0.12pt text shift.
    • A Word screen capture of SlateOrange and MidnightNavy matches.
  • DocxPageBackgroundTest, +3 tests (12):
    • a page of its own draws its fills from the body with no header, and keeps the page's top margin;
    • a page of one table carries them in its first cell, layoutInCell="0", with no hairline over the table;
    • a page of its own under a footer keeps them in a header.
  • Existing tests. The header-path tests now export two pages, so they still exercise the header. DocxDrawingsTest.theBodysDrawingsStandAboveThePageBackgrounds reads the fills from body and header alike.
  • Sabotage. Each rule fails its test:
    • turning the body path off fails the two body tests;
    • ignoring the footer fails the footer test;
    • always opening a hairline fails the first-cell test.
  • 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 → BUILD SUCCESS.
  • Examples. CommittedAssetDriftTest in examples → 3/3 green.

Trade-off and known limits

  • Editing past the page. Drawn from the body, the fills are on that one page. If a reader's text in Word runs a one-page CV onto a second page, the new page has no sidebar column. The header used to paint it on every page. The fix trades that for LibreOffice standing every line where the page does.
  • First cell. The first cell's paragraph is the first one written on the page. A page whose first cell holds a nested table may anchor there.
  • Inherited blank footer. A one-page section after a section with a footer inherits a blank footer and keeps the header path.

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

@DemchaAV
DemchaAV marked this pull request as draft October 3, 2026 20:03
@DemchaAV
DemchaAV marked this pull request as ready for review October 3, 2026 20:19
@DemchaAV
DemchaAV merged commit 30b778a into 2.5-dev Oct 3, 2026
4 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-lo-sidebar-header branch October 3, 2026 20:19
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