feat(api): pageMargins(List<PageMarginRule>) — per-page-range margin overrides#244
Merged
Conversation
…overrides A document had one margin for every page, so a full-bleed cover and a book-margin body could not live in the same file — the cover had to be a separate document or a hand-built full-bleed hack. DocumentSession.pageMargins(List<PageMarginRule>) overrides the page margin for ranges of pages (1-based, last-covering-rule wins), changing the content box both horizontally and vertically: PageMarginRule.page(1, DocumentInsets.zero()) for a full-bleed cover, PageMarginRule.from(2, ...) for the body. Because the engine measures content before it knows the page, per-page width is a fixed point: each top-level block — and each direct child of the page-flow column — is measured at the content width of the page it begins on, resolved by the same recompile-until-stable loop the page-reference resolver already uses. A nullable PageGeometry resolver returns the base canvas for every page when no rules are set, and the no-rules path compiles once and allocates nothing extra, so a document that sets no margins is laid out byte-for-byte as before (full suite, zero changed baselines). Tests: PageMarginTest asserts horizontal (placementX) and vertical (top edge) per-page geometry off the placed graph, via an explicit page break and via natural overflow (the fixed point), plus last-rule-wins, empty-rules, the documented block-spanning-a-boundary limitation, and per-page margins coexisting with page references. Example: PerPageMarginExample (full-bleed cover + book-margin body in one session).
PageMarginRule rejected negative insets but let NaN/Infinity through (the non-negative check is false for both), so a non-finite inset surfaced only later as an opaque engine error. Reject non-finite insets at the type with the same bleed()-pointing message. The page()/range() factories now also reject Integer.MAX_VALUE — which their +1 would overflow into a confusing message — and point at from() for an open-ended range. Tests cover the non-finite/overflow rejection, null clearing back to the document-wide margin, a middle-only page range leaving its neighbours at the base margin, per-page margins resolving section-locally under a multi-section document, and every top-level block being placed consistently with the page it lands on. The pageMargins() Javadoc now states the per-page model's mid-block limitation on the vertical axis too, and the limitation is recorded in the 2.0 breaking-changes ledger (the API stays Stable).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A document had one margin for every page, so a full-bleed cover and a book-margin body could not live in the same file — the cover had to be a separate document or a hand-built full-bleed hack.
What changed
DocumentSession.pageMargins(List<PageMarginRule>)overrides the page margin for ranges of pages — both horizontally and vertically.PageMarginRule.page(1, DocumentInsets.zero())for a full-bleed cover,PageMarginRule.from(2, …)for the body. Pages are 1-based; rules apply in list order, last-covering-rule wins (mirrorspageBackgrounds).PageMarginRule(record, factoriespage()/range()/from()), plus the engine-sidePageGeometry/PageMarginOverrideresolvers.How it works
The engine measures content before it knows which page the content lands on, so per-page width is a circular dependency. It's resolved as a fixed point — the same recompile-until-stable loop the page-reference/TOC resolver already uses: compile, record each node's start page, recompile feeding those back, until the assignment settles (capped, unconverged returns the last layout). Each top-level block — and each direct child of the page-flow column — is measured at the content width of the page it begins on. A single block that overflows a margin boundary keeps its start-page column for its whole length (documented; the model is per-page-range, not mid-block).
Lane: canonical API + shared-engine (
LayoutCompiler/CompilerState/ pagination). A nullablePageGeometryresolver returns the base canvas for every page when no rules are set; the no-rules path compiles once and the per-page branches are guarded, so a document that sets no margins does no extra work.Verification
./mvnw test -pl .— green (1601), 0 changed visual baselines → a document with no rules is laid out byte-for-byte as before../mvnw -P japicmp verify— binary-compatible (new types +defaultinterface methods + a delegating ctor, all additive). Javadoc gate clean.PageMarginTest(7) asserts horizontal (placementX10→70) and vertical (top edge 285→260) per-page geometry off the placed graph — via an explicit page break and via natural overflow (the fixed point) — plus last-rule-wins, empty-rules, the documented block-spanning-a-boundary limitation, and per-page margins coexisting with page references (the unified loop).depth==1page-column gate, and japicmp; it drove the boundary-limitation and page-reference-coexistence tests.PerPageMarginExample+ committed preview: a full-bleed cover (page 1, zero margin) and a book-margin body (pages 2+) in one session.This is the last of the book-template engine-gap items.