Skip to content

fix(docx): write a page zone built otherwise for the first page into the title page's header or footer - #910

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-zones-by-page
Oct 16, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-zones-by-page

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Why

Word paginates, so the DOCX export built a page zone's content once — with PageContext.unpaginated(), a page that is both the first and the last — and wrote it on every page. A footer page.isFirst() ? "Original keep" : "Continuation sheet" read "Original keep" on all three pages in Word, where the PDF draws "Continuation sheet" from page 2, and the report said nothing. Content built otherwise for the last page was written on every page too, its note blaming the measurement rather than the page.

What changed

  • core — PageContext.unpaginated(boolean first, boolean last), new, @Beta, @since 2.5.0: a semantic export's context for a kind of page. number() / total() still refuse; unpaginated() is (true, true). The Javadoc of isFirst / isLast and of DocumentPageZone (whose appliesTo text still said a predicate "lands on every page") follows; the member is named in docs/api-stability.md.
  • DocxSemanticBackend.applyPageZones builds a zone for the first page — (true, layout runs to one page) — and for the pages after it — (false, false) — for the kinds it is drawn on only (ZoneBuilds). Built otherwise, it becomes two writers (zoneVariantsOf): the first page's part, with w:titlePg, and the rest's DEFAULT and EVEN parts. Built alike, one writer, as before.
  • Alike is DocxNodeValues.alike: records component by component, GraphCompose's own other values — a colour, picture data, an SVG drawing, a barcode's or a table's options — field by field, lists, maps and arrays element by element; anything else by equals, so a value of another kind built afresh reads as otherwise and the zone is written twice alike rather than once with the other build's content. DocxZoneParts.readAlike is not used for this — it compares what sets the line and skips a boxed picture's data.
  • Each line is measured on its own layout page: DocxLayoutMetrics.zoneText / zonePictures(zone, page), zoneFirstPage(zone, pages), zoneDistanceFromEdge(zone, pages, …). An unmeasured line drawn on none of its pages — the rest's, in a layout of one page — reads its distance from any page instead of falling back to the padding. Where the two lines cannot share one distance from the edge (other twips, or one not measured), each measured one is framed, as two zones of a kind are.
  • reportTheLastPage: content built otherwise for the last page — or refused there for reading number() — gets an APPROXIMATED note saying the last page is written as built for the pages between; a layout of one page builds its first page as the last and needs none. reportAKindBuiltNone: a build of none for the first page, or for the pages between, where the layout draws the zone there, is DROPPED and named.
  • Fonts: zoneContentsIn collects both builds. Notes: a split zone's line notes say "on the first page" / "on the pages after the first"; a note the second line gives alike one the first line gave is given once, each once (DocxExportReport.Builder.skipRepeatsOf), so two losses of the second line alike stay two.
  • Behaviour change (in CHANGELOG): content reading page.number() only after the first page, never built before, is now refused there as on the first; content reading it only on the last page, refused before, now exports with the note unless the first page is the last.

Verification

  • Word (COM ExportAsFixedFormat) on a 3-page fixture: p1 "Original keep", p2–p3 "Continuation sheet", baseline 190.08 against the page's 190.05 — before, "Original keep" on all three. LibreOffice (Windows) the same; isFirst / isLast content reads Cover / Middle / Middle with the last-page note.
  • New: DocxZoneByPageTest (18: first-page footer and header, later-only predicate, last-page note, one-page document, frames, colour, picture data, even pages, two sections, refusals, distance on the later pages, one note for both lines, two losses of the second line, a barcode zone written once, a kind built none), DocxNodeValuesTest (7), PageContextTest (3), DocxFontTableTest +1. Nine zone tests that made content read otherwise on its first page through isLast() — the defect itself — now do it through isPaginated(); their assertions are unchanged.
  • 25 sabotages, each undoing one part (the factory's encoding, the split, each build, the field and map walks of the value comparison, the last-page note and its refusal, a kind built none and the pages it reads, the pages each lookup reads, framing, the distance fallback, fonts, labels, the repeats and the range they are read from): all caught.
  • Corpus: 62/62 DOCX byte-identical, notes 1244 → 1244.
  • Knowledge pack regenerated (authoring: unpaginated(boolean first, boolean last) [beta]); extract-api --check, check-stability-doc, check-claims --check, check-routes green; core javadoc BUILD SUCCESS, 0 warnings.
  • ./mvnw -B -ntp clean verify → BUILD SUCCESS (core 821, render-docx 1679, qa 1820).

Lane: shared semantic backend (render-docx) + canonical (core PageContext, @Beta).

…the title page's header or footer

Word paginates, so a page zone's content was built once, for a page
that was both the first and the last, and written on every page: a
footer reading page.isFirst() ? "Original keep" : "Continuation sheet"
read "Original keep" on every page, and the report said nothing.

PageContext.unpaginated(boolean, boolean), new and @beta, gives a
semantic export a context for a kind of page that still refuses the
numbers. The export builds a zone for the first page and for the pages
after it, and compares the two by value, down to every run, colour and
picture. Built otherwise, the zone becomes the section's title-page
part and the ordinary one, each measured on a page the layout draws it
on and framed where the two lines cannot share one distance from the
edge; the faces of both are shipped, and what both lines lose is said
once. Word has no part for the last page: content built otherwise for
it is written there as on the pages before it, and named. A document of
one page builds its first page as the last.

Zone tests that made content read otherwise on its first page through
isLast() now do it through isPaginated(), which the page and the file
still answer differently.
@DemchaAV
DemchaAV marked this pull request as draft October 16, 2026 12:15
…d line's own losses

A zone's builds for the first page and the rest were compared record by
record and by equals for anything else, so a barcode's or a table's
options, which have no equals, read as otherwise and split a zone that
is the same on every page. GraphCompose's own values are now read field
by field, and maps by their values.

A note the second line gives alike one its first line gave is given
once, each once: two losses of the second line alike stay two, where a
note the report held from anywhere was dropped. A zone building none
for the first page, or for the pages between it and the last, where the
page draws it there, is named. The last-page note says the page is
written as built for the pages between, which on two pages is no page's
before it.
@DemchaAV
DemchaAV marked this pull request as ready for review October 16, 2026 12:46
@DemchaAV
DemchaAV merged commit 9b12d36 into 2.5-dev Oct 16, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-zones-by-page branch October 16, 2026 14:16
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