Skip to content

feat(api): GraphCompose.documents() — multi-section documents in one PDF#238

Merged
DemchaAV merged 2 commits into
developfrom
feat/multi-section
Jun 26, 2026
Merged

feat(api): GraphCompose.documents() — multi-section documents in one PDF#238
DemchaAV merged 2 commits into
developfrom
feat/multi-section

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

A document has a single page geometry — one page size, one margin, one numbering scheme for the whole file. Assembling a book (a full-bleed cover of one size in front of a margined, page-numbered body of another) meant rendering separate documents and stitching them with external PDFBox PDFMergerUtility, which re-parses each part and is awkward to keep navigable. This is the payoff of the book-template groundwork (pageIndex #231, line().fill() #234, columns() #235, addPageReference #236, addTableOfContents #237).

What changed

  • GraphCompose.documents()MultiSectionDocumentBuilderMultiSectionDocument. Concatenates several independent DocumentSession sections — each with its own page size, margins, fonts, and footer numbering — into one PDF inside the engine, no external merge.
  • Cross-section navigation. Each section's pages are appended at a growing page offset; anchors, internal links, and the bookmark outline resolve across section boundaries against the combined document, so a link on the cover jumps to a chapter in the body.
  • Per-section numbering. Each section's footer {page}/{pages} counts from that section's own first page (roman front-matter can precede an arabic body). Document-level metadata and protection are taken from the first section that declares them.
  • Engine: PdfRenderEnvironment gains a page-index offset applied at the four document-relative recording sites (anchor / link source / bookmark / external-link page); the header/footer and watermark renderers gain a windowed overload [base, base+count); PdfFixedLayoutBackend gains the writeSections orchestration over an extracted renderGraph.

Lane: canonical API (GraphCompose.documents, MultiSectionDocument) + shared-engine (backend assembly, windowed chrome). Purely additive → japicmp-safe.

Verification

  • ./mvnw test -pl .green (1555 tests), zero changed visual baselines → single-section output is byte-identical (the renderers delegate to the windowed overload at offset 0; page indices rebase only when a section starts past page one).
  • MultiSectionDocumentTest (7): each section keeps its page size; a cover link resolves to the body's global page; a bookmark and an external link authored in a later section rebase to their global pages; a duplicate anchor resolves to the last section; each section is numbered from its own first page ("Page 1 of 2", not "Page 2 of 3").
  • A cold review confirmed the four rebase sites, the per-section numbering, nav ordering, and resource safety; the external-link/bookmark-in-a-later-section assertions were added as a result.
  • Runnable MultiSectionExample + committed preview: a landscape cover (440×300) precedes a portrait, page-numbered body (300×440) in one PDF.

Render

Mixed orientation in one document — landscape cover, then portrait body pages footed 1 / 3, 2 / 3, 3 / 3 (section-local), with the cover's call-to-action linking into the body.

This removes the cover-plus-body multi-session + external-merge workaround: a real book is now one DocumentSession assembly.

A document had a single page geometry: one page size, one margin, one
numbering scheme for the whole file. Assembling a book — a full-bleed cover
of one size in front of a margined, page-numbered body of another — meant
rendering separate documents and merging them with external PDFBox, which
re-parses each part and is awkward to keep navigable.

GraphCompose.documents() concatenates several independent DocumentSession
sections — each with its own page size, margins, fonts, and footer
numbering — into one PDF inside the engine, with no external merge. Each
section's pages are appended at a growing page offset; anchors, internal
links, and the bookmark outline resolve across section boundaries against
the combined document, so a link on the cover can jump to a chapter in the
body. Each section's footer counts from its own first page, so roman
front-matter can precede an arabic-numbered body. Document-level metadata
and protection are taken from the first section that declares them.

Single-section output is unchanged: the existing renderers delegate to a
windowed overload with a zero page offset, and the link/anchor/bookmark
page indices are rebased only when a section starts past page one. The full
suite passes with no changed visual baselines.

Tests: MultiSectionDocumentTest renders a cover + body and asserts each
section keeps its page size, a cover link resolves to the body's global
page, a bookmark and external link in a later section rebase to their
global pages, a duplicate anchor resolves to the last section, and each
section is numbered from its own first page. Example: MultiSectionExample
(landscape cover + portrait, page-numbered body in one PDF).
…he correct page

A gradient- or pattern-stroked path, SVG, or text layer in a section after the
first registered its shading pattern against the section-local page index of the
combined document, so the pattern landed on an earlier section's page resources
while the stroke drew on the section's own page. The stroke then referenced a
pattern absent from its own page dictionary and dropped from the output. Resolve
the page through the render environment's page-offset mapping at that site and at
the debug node-label MediaBox lookup, which shared the flaw. Single-section
rendering is unaffected (offset zero).

Also mark the low-level multi-section assembly seam — PdfFixedLayoutBackend.Section,
renderSections, and writeSections — @beta so its shape can still change, and move
the example and tests onto the canonical DocumentHeaderFooter / DocumentMetadata
APIs instead of the deprecated PDF-options forms.

Tests: a gradient-stroked path in a later section registers its pattern on its own
page (red before the fix); an internal link whose source is on a third section
lands on its global page; a footer can skip its section's own first page; document
metadata is taken from the first section that sets it; and close() is idempotent,
rejects further rendering, and buildPdf() without a default file throws.

private static int patternCount(PDDocument doc, int zeroBasedPage) throws IOException {
int count = 0;
for (COSName ignored : doc.getPage(zeroBasedPage).getResources().getPatternNames()) {
@DemchaAV DemchaAV merged commit d0e8d1b into develop Jun 26, 2026
11 checks passed
@DemchaAV DemchaAV deleted the feat/multi-section branch June 26, 2026 00:01
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.

2 participants