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) + } +}