diff --git a/doc.go b/doc.go
index 2d25af3..e33cd3e 100644
--- a/doc.go
+++ b/doc.go
@@ -105,6 +105,11 @@
// positioned layout, a row, anything with keep intact, anything inside an
// — moves whole.
//
+// Except a positioned container whose author wrote keep intact="none" on it.
+// That is a permission pdf.js's clause order makes unreachable and pdfium
+// reads, and such a container is laid out whole and then CUT across the
+// boundary, its children keeping their written y. See [placer.cuttable].
+//
// What moves whole is still put on the paper. The first such container of each
// sheet is not measured against anything: pdf.js's checkDimensions returns
// true while the sheet has had none (layout.js:266-268) and the one that
diff --git a/layout.go b/layout.go
index e0edd18..7b0794f 100644
--- a/layout.go
+++ b/layout.go
@@ -176,6 +176,14 @@ var flowLayouts = map[string]bool{
// unplaced where it fits no page at all. A container that WRAPS is split at a
// line boundary and never inside one.
//
+// A POSITIONED container is the one exception, and only where its author asked
+// for it. `` on one is a permission pdfium reads and
+// pdf.js's clause ORDER cannot reach, and a container carrying it is laid out
+// whole and then CUT: its children keep the y the template wrote for them,
+// less however much of the container is on the sheets before. See
+// [placer.cuttable], which is a different question from [placer.splittable] and
+// answered by a different mechanism.
+//
// # What it deliberately does not do, and reports instead
//
// - rl-row, which fills a ROW from the right. That needs the row's own
diff --git a/paginate.go b/paginate.go
index ac03075..522999d 100644
--- a/paginate.go
+++ b/paginate.go
@@ -327,6 +327,18 @@ func (p *placer) whole(kid *FormNode, lv *level, lay string) bool {
// This is why a container taller than a whole content area is not a
// refusal: it comes out one per sheet, overflowing, exactly as pdf.js
// draws it.
+ //
+ // Unless the author said it may be cut. A positioned container carrying
+ // `` is sliced across the sheets instead of being put
+ // down overflowing — see [placer.cuttable] — and the free pass does not
+ // reach it, because the pass exists so that something too tall for any
+ // sheet still lands somewhere and a container that can be cut does not
+ // need it.
+ if !fits(p.y+h, lv.bottom) && p.cuttable(kid) {
+ if done, cut := p.cut(kid, lv, h); cut {
+ return done
+ }
+ }
for !p.free && !fits(p.y+h, lv.bottom) {
if !p.advance(p.overflowTo(kid)) {
p.blocked = noNextPage
diff --git a/split.go b/split.go
new file mode 100644
index 0000000..f4f7614
--- /dev/null
+++ b/split.go
@@ -0,0 +1,338 @@
+// Copyright (c) 2026, the go-pdfkit/xfa authors
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+package xfa
+
+// cutPrecision is pdfium's kXFALayoutPrecision
+// (cxfa_contentlayoutprocessor.h:28), the slop every comparison in its split
+// arithmetic is made with. It is half a thousandth of a point: not a tolerance
+// for heights written in millimetres — that is [fitSlop] — but a guard against
+// a cut landing exactly on a child's edge and being read as falling inside it.
+const cutPrecision = 0.0005
+
+// cuttable says a container is one this package CUTS across a content area
+// boundary rather than moving whole: it is laid out at its own coordinates and
+// then sliced, so its children keep the y the template wrote for them.
+//
+// It is not [placer.splittable] and must not be confused with it. splittable
+// gates a FLOW-based split, where a container's children are stacked by the
+// cursor and the ones that did not fit begin again at the top of the next
+// content area. That is pdf.js's mechanism and it is the wrong one here: a
+// positioned container's children are placed by their own x and y, so resuming
+// one under the flow cursor would move every one of them.
+//
+// # Why a positioned container is cut at all
+//
+// Because the author asked. pdfium decides what may be cut in
+// FindLayoutItemSplitPos (cxfa_contentlayoutprocessor.cpp:503-588), which
+// switches on CXFA_Node::GetIntact():
+//
+// None recurse into the children; the cut may stand
+// ContentArea, PageArea *fProposedSplitPos = fCurVerticalOffset — walk UP
+// default return false — no change
+//
+// and GetIntact (cxfa_node.cpp:1536-1591) answers ContentArea for a subform
+// whose layout is position or row — UNLESS the subform writes a , in
+// which case GetIntactFromKeep (:2486-2500) returns what the author wrote. At
+// the outermost call fCurVerticalOffset is nought, so a positioned subform with
+// no keep drives the proposed cut to nought, FindSplitPos returns nought, and
+// InsertFlowedItem's `if (fSplitPos > kXFALayoutPrecision)` (:2691) is false:
+// the container moves whole.
+//
+// So pdfium does NOT cut positioned containers in general. It cuts one exactly
+// where `` is written on it.
+//
+// pdf.js cannot reach that permission at all. Subform[$isSplittable]
+// (template.js:4940-4975) answers false at
+//
+// if (this.layout === "position" || this.layout.includes("row")) { return false; }
+//
+// BEFORE it ever reads this.keep.intact. This package followed pdf.js there and
+// put 1 423 body leaves off the paper across 39 forms where pdfium put 25 across
+// 6. Following pdfium is the standard that has decided every other tie in this
+// work, and here the template's author asked for it in the file.
+//
+// # How large the permission is
+//
+// Fifty-eight positioned subforms across seventeen of the corpus's 560 forms
+// write it. None of them is a form root, thirty-four write a height and thirteen
+// write a margin, of which all but three are nought on every side. That is the
+// whole population this touches — not the thirty-nine forms whose off-paper
+// leaves the position clause dominates, which is a wider set that mostly does
+// NOT carry the permission.
+//
+// # What it does not cover, and why
+//
+// - A container that is not a subform. pdfium's GetIntact answers None for an
+// exclGroup, so pdfium would cut one; no exclGroup of the corpus writes a
+// keep of "none", so nothing would say whether it were right.
+// - A row. GetIntact reads position and row alike, so ``
+// on a row would be cut by pdfium too. A row's children are not at written
+// coordinates — they are cut from the columnWidths above and stretched to
+// the tallest — so cutting one is a different mechanism, and the corpus
+// writes none.
+// - A child of a container that WRAPS onto lines. [placer.whole] is reached
+// only from [placer.flowStack]; a LINE is what moves in [placer.flowLines],
+// and cutting one member of a line would leave the rest of that line on a
+// sheet the first half of it is not on.
+// - A container the template hides. pdfium does not lay one out at all —
+// PresenceRequiresSpace is false and no layout item is made — so there is
+// nothing to cut, and [placer.heightOf] gives it no height either.
+func (p *placer) cuttable(n *FormNode) bool {
+ return n.Kind == "subform" && layoutOf(n) == "position" && !hidden(n.Template) &&
+ n.Template.Child("keep").Get("intact") == "none"
+}
+
+// intactOf is pdfium's CXFA_Node::GetIntact (cxfa_node.cpp:1536-1591): what a
+// cut does when it lands inside this element.
+//
+// It is asked of the children of a container being cut, and the answer that
+// matters is whether it is "none". Anything else means the cut may not fall
+// inside the element and is walked up to its top.
+//
+// A written wins outright. GetIntactFromKeep has one exception —
+// a keep of "none" on a ROW is ignored below XFA 2.08 — and it cannot fire
+// here: every template of the corpus is an xfa-template 3.0 document, and
+// [placer.cuttable] admits no row anyway.
+//
+// The field arm is the one that is easy to read past. A field's intact depends
+// on the container above it: ContentArea where that container is itself kept
+// together, and "none" where the container is positioned, a row or a table —
+// which is exactly the case a cut arrives in. So a FIELD inside a container
+// being cut may be cut in half, and pdfium does cut it in half. See [cutAt] for
+// why this package will not.
+func intactOf(p *placer, n *FormNode) string {
+ if v := n.Template.Child("keep").Get("intact"); v != "" {
+ return v
+ }
+ switch n.Kind {
+ case "subform":
+ switch layoutOf(n) {
+ case "position", "row":
+ return "contentArea"
+ default:
+ return "none"
+ }
+ case "draw":
+ // A draw is never cut, whatever it holds and wherever it sits.
+ return "contentArea"
+ case "field":
+ parent, ok := p.up[n]
+ if !ok || parent.Kind == "pageArea" {
+ return "contentArea"
+ }
+ if intactOf(p, parent) != "none" {
+ return "contentArea"
+ }
+ // The remaining arm of pdfium's switch — a tb parent below XFA 2.08
+ // with a written height — keeps a field together. Every corpus template
+ // is 3.0, so it is named rather than written: a rule nothing measures is
+ // a rule nobody can be wrong about out loud.
+ return "none"
+ default:
+ return "none"
+ }
+}
+
+// A piece is one child of a container being cut, with where it sits inside the
+// container's own box.
+//
+// y is measured from the TOP of that box, with the container's own top inset
+// already added, because that is where [placer.children] puts a positioned
+// child and because pdfium's layout items carry their ancestors' insets in
+// their position too (CXFA_ContentLayoutItem::GetAbsoluteRect,
+// cxfa_contentlayoutitem.cpp:82-90).
+type piece struct {
+ node *FormNode
+ y, h Measure
+ // gone says the template hides it, so it takes no room and no cut is ever
+ // walked up to it. pdfium does not lay a hidden element out at all —
+ // PresenceRequiresSpace is false and no layout item is made — so it is in no
+ // cut's arithmetic. It is still PLACED, with the part of the container its
+ // own y falls in, because this package reports a hidden element rather than
+ // dropping it.
+ gone bool
+ // keep says a cut may not fall inside it, which is [intactOf] answering
+ // anything but "none".
+ keep bool
+}
+
+// piecesOf measures every child of a container about to be cut, in the order
+// the container holds them.
+//
+// Every y and every height here is readable, and that is not an assumption:
+// [placer.whole] asks [placer.heightOf] for this container's own height before
+// it asks for a cut, and the positioned arm of [placer.contentHeight] reads the
+// same y of the same children and the same [placer.heightOf] of each, at the
+// same width. A container holding something none of that can be read for is
+// blocked there and never arrives — see [placer.cut] for the margin, which
+// [placer.measure] reads in the same pass.
+func (p *placer) piecesOf(n *FormNode, in insets, wide Measure) []piece {
+ kids := contained(n)
+ out := make([]piece, 0, len(kids))
+ for _, k := range kids {
+ y, _, _ := k.Template.Measure("y")
+ h, _ := p.heightOf(k, wide, 0)
+ out = append(out, piece{
+ node: k, y: in.top + y, h: h,
+ gone: hidden(k.Template),
+ keep: intactOf(p, k) != "none",
+ })
+ }
+ return out
+}
+
+// cutAt walks a proposed cut up until it falls inside nothing, which is
+// pdfium's FindLayoutItemSplitPos (cxfa_contentlayoutprocessor.cpp:503-588).
+//
+// pdfium's rule: an item the cut does not land INSIDE never moves it — the
+// guard at :509-513 returns false for one wholly above and for one wholly
+// below. An item it does land inside moves the cut up to that item's own top
+// where the item's GetIntact is ContentArea or PageArea, and is recursed into
+// where it is None.
+//
+// bisects is the case this package will not follow. A child whose intact is
+// "none" — a field of a positioned container is one, by [intactOf] — moves the
+// cut nowhere, and pdfium then CUTS IT IN HALF: SplitLayoutItem emits two
+// layout items for the one node (:729-864), the top half on one sheet and the
+// bottom half on the next. A [Box] is one rectangle for one occurrence and
+// [Layout] promises every body leaf appears in exactly one of [Page.Boxes] and
+// [Layout.Unplaced]; a field emitted twice would break the promise the type
+// exists for, and any judge pairing by name would drop it as ambiguous. So the
+// cut is REFUSED there rather than moved somewhere pdfium never puts it: see
+// [placer.cut].
+//
+// behind is how much of the container is already on earlier sheets, so that the
+// walk cannot go back past what has been placed.
+func cutAt(pieces []piece, behind, proposed Measure) (at Measure, bisects bool) {
+ at = proposed
+ for changed := true; changed; {
+ changed = false
+ for _, s := range pieces {
+ if !inside(s, behind, at) || !s.keep {
+ continue
+ }
+ at, changed = s.y, true
+ break
+ }
+ }
+ for _, s := range pieces {
+ if inside(s, behind, at) {
+ return at, true
+ }
+ }
+ return at, false
+}
+
+// inside says a cut lands within one child, which is pdfium's guard at both
+// ends (FindLayoutItemSplitPos, :509-513): a child the cut is at or above the
+// top of, and one it is at or past the bottom of, is left alone. A child the
+// flow has already carried past is not in the arithmetic at all, nor is one the
+// template hides.
+func inside(s piece, behind, at Measure) bool {
+ if s.gone || s.y+s.h <= behind+cutPrecision {
+ return false
+ }
+ return at > s.y+cutPrecision && at <= s.y+s.h-cutPrecision
+}
+
+// cut lays a positioned container out whole and then slices it across as many
+// content areas as it needs, which is pdfium's mechanism rather than pdf.js's.
+//
+// The container is never put on the flowing chain. Its children are placed at
+// the y the template wrote for them, less however much of the container is
+// already on earlier sheets — so a child written at 744 pt inside a container
+// whose first 744 pt are on the sheet before lands at the top of this one,
+// which is what SplitLayoutItem does when it moves a child to the second item
+// with `pChildItem->s_pos_.y -= fSplitPos` (:806). The partition is by the
+// child's TOP y, which is the same line's test: `fSplitPos <= childY` sends it
+// on, `fSplitPos >= childY + childH` keeps it here.
+//
+// total is the height the container reports to the stack above it, which
+// [placer.whole] has already measured.
+//
+// It returns done=false where the flow cannot go on, and cut=false where no cut
+// was made at all and the caller should place the container the way it always
+// did. Nothing is placed on the second of those.
+func (p *placer) cut(n *FormNode, lv *level, total Measure) (done, cut bool) {
+ // The margin is readable, for the reason [placer.piecesOf] gives: the
+ // height [placer.whole] already has in hand was computed by
+ // [placer.measure], which stops at a margin it cannot read before it
+ // measures anything else.
+ in, _ := marginOf(n.Template)
+ wide := innerWide(n, lv.wide, 0, "position", in)
+ pieces := p.piecesOf(n, in, wide)
+ cols, _ := columnWidths(n.Template)
+ left := make([]bool, len(pieces))
+ for i := range left {
+ left[i] = true
+ }
+
+ var behind Measure
+ for {
+ at, last := unbounded, true
+ if !fits(p.y+total-behind, lv.bottom) {
+ // pdfium's FindSplitPos(fAvailHeight - fContentCurRowY),
+ // InsertFlowedItem:2690, in the frame the flow has got to. The fit
+ // it is asked after is [fits] rather than pdfium's own, because the
+ // flow around it is pdf.js's and one page boundary cannot be
+ // decided by two different tests.
+ var bisects bool
+ at, bisects = cutAt(pieces, behind, behind+lv.bottom-p.y)
+ last = bisects || at <= behind+cutPrecision
+ if last && behind == 0 {
+ // The container was never cut. Nothing has been placed, and the
+ // caller carries on as it always did.
+ return false, false
+ }
+ // A last part that could not be cut goes down whole, which is what
+ // pdfium does when FindSplitPos returns nought.
+ if last {
+ at = unbounded
+ }
+ }
+ // pdf.js's free pass, applied to the part in hand: the first thing on a
+ // sheet that moves in one piece cannot fail, and neither can anything
+ // inside it. See [placer.whole].
+ first := p.free
+ p.free, p.noFail = false, first
+ p.touch()
+ top := p.y - behind
+ for i, s := range pieces {
+ if !left[i] || s.y+cutPrecision >= at {
+ continue
+ }
+ left[i] = false
+ // The container's own insets move its children in, exactly as
+ // [placer.children] does for a positioned container that is not
+ // cut. The top one is already in the part's own y, so the frame
+ // carries it and the part's y is added to it by [placer.place]
+ // reading the child's written y.
+ p.place(s.node, frame{
+ x: lv.x + in.left, y: top + in.top,
+ avail: lv.bottom - top, wide: wide, cols: cols,
+ })
+ }
+ p.noFail = false
+ if last {
+ // What the container adds to the stack above it is the whole of its
+ // height measured from where this last part began, which is
+ // SplitLayoutItem's second item being `s_size_.height - fSplitPos`
+ // tall (:762).
+ p.y = top + total
+ return true, true
+ }
+ behind = at
+ if !p.advance(p.overflowTo(n)) {
+ p.blocked = noNextPage
+ for i, s := range pieces {
+ if left[i] {
+ p.rejectAll(s.node, noNextPage)
+ }
+ }
+ return false, true
+ }
+ }
+}
diff --git a/split_test.go b/split_test.go
new file mode 100644
index 0000000..aaedbe5
--- /dev/null
+++ b/split_test.go
@@ -0,0 +1,240 @@
+// Copyright (c) 2026, the go-pdfkit/xfa authors
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+package xfa
+
+import (
+ "strings"
+ "testing"
+)
+
+// cutting is a positioned container of three draws at written y, inside a tb
+// root, on sheets whose content area is 30 points tall. The container is 60
+// points, so it cannot fit on any sheet whole.
+//
+// B at y=0 h=10 -> 0..10
+// C at y=20 h=10 -> 20..30
+// D at y=50 h=10 -> 50..60
+func cutting(keep, attrs string) string {
+ return `` + keep + `
+
+
+ `
+}
+
+func TestAPositionedContainerIsCutOnlyWhereItsAuthorPermitsIt(t *testing.T) {
+ // With no the subform is pdfium's ContentArea — GetIntact,
+ // cxfa_node.cpp:1550-1557 — so it moves whole onto the next sheet and runs
+ // off the bottom, which is what this package has always done and what
+ // pdf.js does.
+ l := laidOut(t, sheets(`>`+sheet("P", "", "30", ""),
+ ``+cutting("", "")))
+ same(t, "an unpermitted container", byPage(l), []string{
+ "0: draw f.A 0,0 1x10",
+ "1: draw f.G.B 0,0 1x10", "1: draw f.G.C 0,20 1x10", "1: draw f.G.D 0,50 1x10"})
+
+ // is the author's permission, and pdfium reads it
+ // through GetIntactFromKeep. The cut falls at 20 — the room left below A —
+ // bisecting nothing, and the second part begins again at the top of the
+ // next sheet with 20 taken off every child's written y. D still does not
+ // fit in the 30 that are left, so it is cut again at 50.
+ l = laidOut(t, sheets(`>`+sheet("P", "", "30", ""),
+ ``+cutting(``, "")))
+ same(t, "a permitted container", byPage(l), []string{
+ "0: draw f.A 0,0 1x10", "0: draw f.G.B 0,10 1x10",
+ "1: draw f.G.C 0,0 1x10",
+ "2: draw f.G.D 0,0 1x10"})
+}
+
+func TestACutIsWalkedUpToTheTopOfWhateverItLandsIn(t *testing.T) {
+ // The room left below A is 25, which falls inside C (20..30). C is a draw,
+ // and pdfium's GetIntact answers ContentArea for every draw
+ // (cxfa_node.cpp:1586-1587), so the cut is walked up to C's own top at 20
+ // — FindLayoutItemSplitPos:580-583. B alone stays on the first sheet.
+ l := laidOut(t, sheets(`>`+sheet("P", "", "30", ""),
+ ``+cutting(``, "")))
+ same(t, "the walked-up cut", byPage(l), []string{
+ "0: draw f.A 0,0 1x5", "0: draw f.G.B 0,5 1x10",
+ "1: draw f.G.C 0,0 1x10",
+ "2: draw f.G.D 0,0 1x10"})
+}
+
+func TestACutIsRefusedRatherThanBisectAField(t *testing.T) {
+ // A field of a positioned container has pdfium's intact of "none"
+ // (cxfa_node.cpp:1558-1584), so pdfium would cut it in half and emit the
+ // node twice. This package will not: with the only cut position falling
+ // inside the field, no cut is made at all and the container moves whole,
+ // which is also what pdfium does when FindSplitPos comes back at nought.
+ body := `
+
+ `
+ l := laidOut(t, sheets(`>`+sheet("P", "", "30", ""), body))
+ same(t, "the refused cut", byPage(l), []string{
+ "0: draw f.A 0,0 1x5",
+ "1: field f.G.B 0,0 1x40"})
+
+ // The same field with a of its own is one the cut may not fall
+ // inside, so it is walked up to the field's top instead — and there is
+ // nothing above it, so no cut is possible there either and the container
+ // still moves whole. This is the second way [placer.cut] arrives at "no
+ // cut": pdfium's, rather than ours.
+ l = laidOut(t, sheets(`>`+sheet("P", "", "30", ""),
+ strings.Replace(body, ``,
+ ``, 1)))
+ same(t, "the cut with nowhere to go", byPage(l), []string{
+ "0: draw f.A 0,0 1x5",
+ "1: field f.G.B 0,0 1x40"})
+}
+
+func TestWhatCannotBeCutAgainGoesDownWhole(t *testing.T) {
+ // The first cut is made, and what is left holds one child taller than a
+ // whole content area. There is no second cut, so the rest of the container
+ // goes down on the sheet the flow has reached and overflows it — pdfium's
+ // answer when FindSplitPos returns nought mid-container.
+ l := laidOut(t, sheets(`>`+sheet("P", "", "30", ""),
+ `
+
+ `))
+ // The sheet in between is the price, and it is pdfium's too: on it the cut
+ // proposed at 60 is walked up to C's own top at 40, which is progress from
+ // 30, so a part with nothing in it is emitted and the page ends.
+ // FindSplitPos returns the same 10 (relative) there.
+ same(t, "the uncuttable remainder", byPage(l), []string{
+ "0: draw f.G.B 0,0 1x10",
+ "2: draw f.G.C 0,0 1x100"})
+}
+
+func TestACutContainerCarriesTheFlowOnBelowIt(t *testing.T) {
+ // The cut falls at 30, so B and C stay and D moves with 30 taken off its
+ // written y — SplitLayoutItem's `pChildItem->s_pos_.y -= fSplitPos` — and
+ // lands at 20 rather than at the top of the sheet. What the container then
+ // adds to the stack is its height measured from where its last part began,
+ // which is that item's `s_size_.height - fSplitPos` of 30: the flow reaches
+ // exactly the bottom, and E turns the page again.
+ l := laidOut(t, sheets(`>`+sheet("P", "", "30", ""),
+ cutting(``, "")+``))
+ same(t, "the flow below a cut container", byPage(l), []string{
+ "0: draw f.G.B 0,0 1x10", "0: draw f.G.C 0,20 1x10",
+ "1: draw f.G.D 0,20 1x10",
+ "2: draw f.E 0,0 1x5"})
+}
+
+func TestACutContainerKeepsItsOwnMarginAndOffsets(t *testing.T) {
+ // The container's top inset moves every child down inside it and is part of
+ // the height it is cut into, exactly as pdfium's layout items carry their
+ // ancestors' insets (CXFA_ContentLayoutItem::GetAbsoluteRect). With a top
+ // inset of 4 the children sit at 4, 24 and 54; the room left below A is 25,
+ // which falls inside C at 24..34, so the cut is walked up to 24.
+ l := laidOut(t, sheets(`>`+sheet("P", "", "30", ""),
+ ``+
+ cutting(``, "")))
+ same(t, "a cut container's margin", byPage(l), []string{
+ "0: draw f.A 0,0 1x5", "0: draw f.G.B 2,9 1x10",
+ "1: draw f.G.C 2,0 1x10",
+ "2: draw f.G.D 2,0 1x10"})
+}
+
+func TestAHiddenChildIsInNoCutsArithmeticAndStillPlaced(t *testing.T) {
+ // pdfium lays a hidden element out not at all — PresenceRequiresSpace is
+ // false and no layout item is made — so it moves no cut. This package
+ // reports it rather than dropping it, with the part its own y falls in.
+ l := laidOut(t, sheets(`>`+sheet("P", "", "30", ""),
+ `
+
+
+
+ `))
+ // The cut falls at 25, where the room ran out, because nothing that counts
+ // straddles it — so C keeps 15 of its written 40 on the sheet it moves to.
+ same(t, "a hidden child of a cut container", byPage(l), []string{
+ "0: draw f.A 0,0 1x5", "0: draw f.G.B 0,5 1x10", "0: draw f.G.H 0,25 1x10",
+ "1: draw f.G.C 0,15 1x10"})
+}
+
+func TestACutStopsWhenTheFormRunsOutOfSheets(t *testing.T) {
+ // One sheet only. The first part goes down, and there is no second sheet
+ // for what is left, so the rest is reported rather than dropped.
+ l := laidOut(t, sheets(`>`+sheet("P", `>`, "")))
+ same(t, "the one sheet", byPage(l), []string{
+ "0: draw f.G.B 0,0 1x10", "0: draw f.G.C 0,20 1x10"})
+ same(t, "what is left", notLaid(l), []string{"f.G.D: " + noNextPage})
+}
+
+func TestACutContainerNobodyCanMeasureNeverReachesTheCut(t *testing.T) {
+ // A margin that is not a length, a child whose y is not a place and a child
+ // whose height is not a length each stop the container ONE STEP EARLIER
+ // than the cut: [placer.whole] asks [placer.heightOf] first, and
+ // [placer.measure] reads exactly those three. That is why [placer.cut] and
+ // [placer.piecesOf] read them back without checking, and this says so with
+ // a form rather than with a comment.
+ for _, tc := range []struct{ what, body string }{
+ {"a margin nobody can read", `
+ `},
+ {"an origin nobody can read", `
+ `},
+ {"a height nobody can read", `
+ `},
+ } {
+ l := laidOut(t, sheets(`>`+sheet("P", "", "30", ""),
+ ``+tc.body))
+ if got := len(l.Pages); got != 1 {
+ t.Errorf("%s came to %d sheets, want 1: nothing should have been cut", tc.what, got)
+ }
+ if got := len(l.Pages[0].Boxes); got != 1 {
+ t.Errorf("%s put %d boxes on the sheet, want only the draw above it", tc.what, got)
+ }
+ }
+}
+
+func TestIntactFollowsWhatTheContainerAboveSays(t *testing.T) {
+ // [intactOf] is pdfium's GetIntact, and the field arm is the one that reads
+ // upwards. Each of its answers is asked here of a form laid out for it,
+ // because the arms are reached through the cut rather than directly.
+ p := newPlacer()
+ src := `
+
+
+
+
+
+
+
+ `
+ root := firstOfKind(Expand(parse(t, src), nil).Root, "subform")
+ p.mapUp(root)
+ find := func(name string) *FormNode {
+ var got *FormNode
+ root.Walk(func(n *FormNode) {
+ if n.Template.Get("name") == name {
+ got = n
+ }
+ })
+ return got
+ }
+ for _, tc := range []struct{ name, want string }{
+ {"G", "none"}, // a written keep wins outright
+ {"K", "contentArea"}, // and so does one that keeps it together
+ {"T", "none"}, // a tb subform with no keep
+ {"P", "contentArea"}, // a POSITIONED subform with no keep: pdfium's default
+ {"R", "contentArea"}, // and a row, which pdfium reads the same way
+ {"f", "none"}, // the root, likewise
+ {"E", "contentArea"}, // a draw, always
+ {"X", "none"}, // anything else pdfium's switch does not name
+ {"B", "none"}, // a field of a positioned container that is cut
+ {"C", "contentArea"}, // a field of a container kept together
+ {"D", "none"}, // a field of a tb container, XFA 3.0
+ } {
+ if got := intactOf(p, find(tc.name)); got != tc.want {
+ t.Errorf("the intact of %s came out as %q, want %q", tc.name, got, tc.want)
+ }
+ }
+ // A field whose layout parent is not a container of the body at all is kept
+ // together, which is pdfium reading a null parent or a page area
+ // (cxfa_node.cpp:1559-1562).
+ if got := intactOf(newPlacer(), find("B")); got != "contentArea" {
+ t.Errorf("the intact of a parentless field came out as %q, want contentArea", got)
+ }
+}