Skip to content

fix(docx): draw a page zone on the pages its predicate picks - #727

Merged
DemchaAV merged 1 commit into
2.5-devfrom
feature/docx-page-classes
Sep 23, 2026
Merged

DemchaAV merged 1 commit into
2.5-devfrom
feature/docx-page-classes

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

A page zone chooses its pages with appliesTo(Predicate<PageContext>): a cover-only header, a footer that skips the cover, a running head on even pages. The DOCX export could not evaluate a predicate, so it wrote every such zone on every page and only logged a warning. In Word, the cover header repeated on every page, and the "not on the cover" footer appeared on the cover.

Word does not test pages. It has three kinds of header and footer per section:

  • one for the first page, when the section states a title page (w:titlePg);
  • one for even pages, when the document states different even and odd pages (w:evenAndOddHeaders);
  • one for the rest.

What changed

  • New DocxPageClasses (package-private) asks a zone's predicate about sample pages and sorts the answers into those three kinds.
    • The sample runs to the document's laid-out page count, and to at least 6 pages, so each kind is asked more than once.
    • A predicate that answers alike for every page of a kind is one Word can state. It returns the kinds the zone is drawn on.
    • A predicate that picks pages within a kind — the last page, the third — has no Word equivalent. So does one that throws on a sample page past the real document, e.g. one that looks something up by page number. For both it returns null.
  • DocxSemanticBackend.applyPageZones writes each zone into the part of every kind it is drawn on.
    • A section states w:titlePg when some zone tells the first page from the rest.
    • The document states w:evenAndOddHeaders when some zone tells even pages from odd. That setting is document-wide, so it is decided before any section is written, and every section then states its even pages.
    • A kind of page the section draws no zone on gets an empty one-point part, in both of these cases:
      • the section draws that header or footer on other pages;
      • an earlier section had one, which Word would otherwise repeat.
    • This generalises the empty-part rule from the multi-section export to all three kinds.
    • A zone with no Word equivalent is written on every page, with a docx.zone.pagePredicate warning and an APPROXIMATED "page zone" note in the export report. A zone that asks for no page at all is not written.
  • DocxLayoutMetrics.zoneDistanceFromEdge now measures a zone on any page it is drawn on. Before, it read only the first page. A zone that skips the first page had nothing there and fell back to its padding.
  • Unchanged for a zone with no predicate: one default part, no titlePg, no settings flag, same bytes. The committed word-export-companion.docx preview did not move.
  • Recipe (with a predicate → Word table), capability-matrix page-zone row, and CHANGELOG under v2.5.0.

Verification

In an editor. A three-page document with three zones, converted with LibreOffice:

  • a cover-only header;
  • an even-page header;
  • a not-on-the-cover footer.
Page LibreOffice Engine PDF
1 cover header only cover header only
2 even header + footer even header + footer
3 footer only footer only

Both break the text at the same lines.

Gate. Full reactor gate (clean verify over core, render-pdf/docx/pptx, templates, testing, qa, coverage) → BUILD SUCCESS. Examples after install: 93 green. extract-api --check is current: no public API change.

New tests.

DocxPageClassesTest (6):

  • no predicate → every kind;
  • isFirst, !isFirst, even and odd page numbers and never are each sorted into the right kinds;
  • isLast, the third page, and the first four pages have no Word equivalent;
  • a predicate that throws on a sample page has no Word equivalent;
  • the sample follows the document's own page count;
  • page numbers are filed as Word files them.

DocxPageZoneKindsTest (7):

  • a first-page header gets titlePg, and the other pages get an empty header;
  • a not-first footer gets an empty first-page footer;
  • an even-page footer turns on even/odd pages (read from settings.xml, since POI's getter only reports that the element is present) and leaves the default part empty;
  • a zone with no predicate is unchanged;
  • a last-page footer is written on every page and reported;
  • a not-first footer's distance equals an every-page footer's;
  • once one section needs even pages, every section states them.

Sabotaging each fails the matching tests:

  • titlePg;
  • the even/odd flag;
  • the any-page distance search.

Changed existing test. qa DocxPageZoneTest.aPagePredicateCannotBeEvaluatedSoTheZoneLandsEverywhereAndSaysSo pinned "every predicate is written on every page". It used !isFirst, which Word now states exactly. It is now aPagePredicateWordCannotStateLandsEverywhereAndSaysSo, with a last-page predicate, and keeps its point: an inexpressible predicate lands everywhere and is warned about.

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

The DOCX export wrote every page zone on every page and only warned
about an appliesTo predicate. The predicate is now asked over sample
pages and sorted into Word's kinds: a first-page zone becomes the
title page's part (w:titlePg), an even- or odd-page zone the even or
default part (w:evenAndOddHeaders, document-wide), and pages a zone
skips get an empty part. A predicate that picks pages within a kind,
or refuses a sample page, is written on every page and reported.

A zone's distance from the page edge is measured on any page it is
drawn on, so a zone that skips the first page is placed where the
layout drew it.
@DemchaAV
DemchaAV merged commit af07334 into 2.5-dev Sep 23, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the feature/docx-page-classes branch September 23, 2026 14:34
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