Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,18 @@ func TestWhatAStackReportsRatherThanPlaces(t *testing.T) {
`<margin topInset="1pt" rightInset="a bit"/><draw name="A" w="1pt" h="1pt"/>`,
[]string{"f.A: the container that stacks it writes a margin that is not in lengths"}},
{"a margin that is not in lengths, on something being stacked",
// Twice over, and for two different reasons. S is positioned, and
// a positioned child begins at its container's leftInset and
// topInset — so A cannot be placed at all when the insets cannot
// be read, which is the same refusal [placer.stack] has always
// made rather than taking an unreadable margin as nought. And S's
// own height is unknown for the same reason, so B, which comes
// after it in the stack, does not know where it begins.
`<subform name="S"><margin bottomInset="a bit"/><draw name="A" w="1pt" h="1pt"/></subform>
<draw name="B" w="1pt" h="1pt"/>`,
[]string{"f.B: a tb layout stacks its children, and the height of the one above it is not computed: " +
"its margin is not written in lengths"}},
[]string{"f.S.A: the container holding it writes a margin that is not in lengths",
"f.B: a tb layout stacks its children, and the height of the one above it is not computed: " +
"its margin is not written in lengths"}},
{"an origin inside a positioned container that is not a place",
`<subform name="S"><draw name="A" y="down a bit" w="1pt" h="1pt"/></subform>
<draw name="B" w="1pt" h="1pt"/>`,
Expand Down
15 changes: 14 additions & 1 deletion layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,8 +523,21 @@ func (p *placer) children(n *FormNode, f frame) {
case "rl-row":
p.firstOnly(kids, lay, f, wide, cols)
default:
// A positioned child is measured from INSIDE its container's insets,
// not from the container's own corner. pdfium adds every ancestor's
// margin/@leftInset and @topInset on the way down —
// CXFA_ContentLayoutItem::GetAbsoluteRect,
// xfa/fxfa/layout/cxfa_contentlayoutitem.cpp:82-90 — for every layout
// kind, and that is the rectangle it draws with
// (cxfa_ffwidget.cpp:228). [placer.stack] has always done the same for
// a stacked child; this arm did not.
in, ok := marginOf(n.Template)
if !ok {
p.rejectKids(kids, "the container holding it writes a margin that is not in lengths")
return
}
for _, kid := range kids {
p.place(kid, frame{x: f.x, y: f.y, avail: f.avail, wide: wide, cols: cols})
p.place(kid, frame{x: f.x + in.left, y: f.y + in.top, avail: f.avail, wide: wide, cols: cols})
}
}
}
Expand Down
44 changes: 38 additions & 6 deletions layout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func TestAFormPlacedByHand(t *testing.T) {
func TestAPositionedFormNeedsNoFirstChildRule(t *testing.T) {
// The same body under a positioned outermost subform: now every child is
// placed at its own coordinates, Body's x=5 y=7 among them.
l := laidOut(t, strings.Replace(worked, `name="form1" layout="tb"`, `name="form1"`, 1))
l := laidOut(t, strings.Replace(worked, `name="form1" layout="tb"`, `name="form1" layout="position"`, 1))
same(t, "the page", laid(l), []string{
"draw form1.Page1.Stamp 10,20 30x40",
// Body: 18+5, 36+7 = 23,43; A: 24,45; Inner: 33,63; B: 36,67
Expand Down Expand Up @@ -182,7 +182,7 @@ func TestWhatIsReportedRatherThanPlaced(t *testing.T) {
</pageSet></subform></template>`,
[]string{"f.P2.Two: its page area is never used: no page of this form is one"}},
{"an origin that is not a place",
`<template><subform name="f"><pageSet><pageArea name="P"><contentArea/></pageArea></pageSet>
`<template><subform name="f" layout="position"><pageSet><pageArea name="P"><contentArea/></pageArea></pageSet>
<subform name="S" x="over there"><field name="A" w="1pt" h="1pt"/></subform></subform></template>`,
[]string{`f.S.A: its origin is written as x="over there" y="", which is not a place`}},
{"a size that is not a size",
Expand All @@ -198,12 +198,12 @@ func TestWhatIsReportedRatherThanPlaced(t *testing.T) {
<draw name="A" w="1pt" maxH="9pt"/></subform></template>`,
[]string{"f.A: " + boundByTheRoom("maxH")}},
{"a container anchored by a corner, with no size of its own",
`<template><subform name="f"><pageSet><pageArea name="P"><contentArea/></pageArea></pageSet>
`<template><subform name="f" layout="position"><pageSet><pageArea name="P"><contentArea/></pageArea></pageSet>
<subform name="S" anchorType="bottomLeft"><field name="A" w="1pt" h="1pt"/></subform></subform></template>`,
[]string{"f.S.A: it is anchored by a corner other than its top left, and its size is not written: " +
"only measuring its contents would give it"}},
{"a container turned on its side",
`<template><subform name="f"><pageSet><pageArea name="P"><contentArea/></pageArea></pageSet>
`<template><subform name="f" layout="position"><pageSet><pageArea name="P"><contentArea/></pageArea></pageSet>
<subform name="S" rotate="90" w="9pt" h="9pt"><field name="A" w="1pt" h="1pt"/></subform></subform></template>`,
[]string{"f.S.A: its contents are turned, which this slice does not follow"}},
{"a row that fills from the right",
Expand Down Expand Up @@ -240,7 +240,7 @@ func TestASubformSetHoldsNoPlaceOfItsOwn(t *testing.T) {
}

func TestAnExclGroupPlacesItsButtons(t *testing.T) {
l := laidOut(t, `<template><subform name="f">
l := laidOut(t, `<template><subform name="f" layout="position">
<pageSet><pageArea name="P"><contentArea/></pageArea></pageSet>
<exclGroup name="Sex" x="10pt" y="10pt">
<field name="M" x="0pt" y="0pt" w="8pt" h="8pt"/>
Expand Down Expand Up @@ -371,7 +371,7 @@ func TestAContainerAnchoredByACornerMovesWhatIsInside(t *testing.T) {
// transform moves the subtree with it. Worked by hand: the subform names
// its bottom left corner at 0,100 and is 50 tall, so its top left is at
// 0,50, and the field one point across and two down from that is at 1,52.
l := laidOut(t, `<template><subform name="f">
l := laidOut(t, `<template><subform name="f" layout="position">
<pageSet><pageArea name="P"><contentArea/></pageArea></pageSet>
<subform name="S" anchorType="bottomLeft" y="100pt" w="100pt" h="50pt">
<field name="A" x="1pt" y="2pt" w="5pt" h="5pt"/>
Expand All @@ -398,3 +398,35 @@ func TestTheShadowWalkStopsGoingDown(t *testing.T) {
t.Errorf("it followed %d nodes down a tree %d deep, want %d", n, maxDepth+9, maxDepth+1)
}
}

// TestAPositionedChildBeginsInsideTheInsets is the second thing pdfium settled
// that pdf.js could not: a container's margin moves what is positioned inside
// it, and not only what is stacked inside it.
//
// pdfium adds every ancestor's margin/@leftInset and @topInset on the way down
// to an absolute rectangle — CXFA_ContentLayoutItem::GetAbsoluteRect,
// xfa/fxfa/layout/cxfa_contentlayoutitem.cpp:82-90 — with no test of the
// layout, and cxfa_ffwidget.cpp:228 makes that the rectangle it draws with.
// pdf.js hands <margin> to CSS as `margin` on the element (html_utils.js:171-174)
// and never resolves it into a coordinate at all.
//
// 1.27mm is 3.6pt, which is what fr-cerfa__cerfa_12818 writes between a table
// cell and its four buttons, and 3.6 is exactly what pdfium put them right of
// where this package did.
func TestAPositionedChildBeginsInsideTheInsets(t *testing.T) {
l := laidOut(t, `<template><subform name="f" layout="position">
<pageSet><pageArea name="P"><contentArea x="10pt" y="20pt"/></pageArea></pageSet>
<subform name="S" x="100pt" y="200pt">
<margin leftInset="1.27mm" topInset="1.27mm" rightInset="1.27mm" bottomInset="1.27mm"/>
<field name="A" x="6.35mm" y="0pt" w="5pt" h="5pt"/>
<subform name="In" x="0pt" y="0pt"><field name="B" w="5pt" h="5pt"/></subform>
</subform></subform></template>`)
// S sits at the content area's origin plus its own x and y: 110,220.
// A is 6.35mm = 18pt across of S's INSIDE, which begins 3.6 in from S:
// 110 + 3.6 + 18 = 131.6, and 220 + 3.6 + 0 = 223.6.
// In writes no margin of its own, so B moves by S's insets only.
same(t, "the page", laid(l), []string{
"field f.S.A 131.6,223.6 5x5",
"field f.S.In.B 113.6,223.6 5x5",
})
}
54 changes: 51 additions & 3 deletions paginate.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ func (p *placer) splittable(n *FormNode) bool {
switch layoutOf(n) {
case "tb", "table", "lr-tb", "rl-tb":
default:
return false
if !p.forcedTb(n) {
return false
}
}
if n.Kind == "subform" {
switch n.Template.Child("keep").Get("intact") {
Expand All @@ -146,6 +148,43 @@ func (p *placer) splittable(n *FormNode) bool {
return true
}

// forcedTb says the outermost subform of the template writes no layout it can
// read, which pdfium lays out as tb rather than as position.
//
// pdf.js reads an absent or unrecognised layout as "position" wherever it
// appears (getStringOption, utils.js:70-76), and this package followed it —
// see [layoutOf]. pdfium does not, and only for this one node: GetLayout
// (cxfa_contentlayoutprocessor.cpp:365-379) returns the attribute where it
// parses to one of the seven values, and where it does NOT and the node's
// parent is the <form> root it returns Tb with bRootForceTb set. A positioned
// container is never flowed, so a <break> inside one is never reached
// (DoLayoutPositionedContainer recurses with bUseBreakControl false, line
// 1163, and the break stage begins by testing it, line 1751) — which is why
// the difference shows as a sheet count and not as a coordinate.
//
// FOUR of the corpus's 560 root subforms write no layout, and three of them
// are forms this package put on one sheet where pdfium and pdf.js both use
// two: us-opm__sf39, us-opm__sf39a and us-opm__sf813. The fourth,
// us-opm__sf181, has one child and no break.
//
// # The half of bRootForceTb this does NOT do
//
// pdfium's flag has a second effect: CalculateRowChildPosition
// (2062-2117) gives such a root's children their WRITTEN x and y
// (CalculatePositionedContainerPos) instead of the packing cursor — flowed for
// pagination, positioned for coordinates. This stacks them. Nothing in the
// corpus separates the two: every child of all four roots writes no x and no
// y, so the cursor and the written origin are the same point. It is named here
// rather than written because no form measures it, and a rule nothing measures
// is a rule nobody can be wrong about out loud.
func (p *placer) forcedTb(n *FormNode) bool {
if n != p.root || n.Kind != "subform" {
return false
}
v := n.Template.Get("layout")
return !flowLayouts[v] && v != "position"
}

// opened is the container's own level of the flowing chain, or nil where it is
// not one of them.
func (p *placer) opened(n *FormNode) *level {
Expand Down Expand Up @@ -213,13 +252,22 @@ func (p *placer) body(root *FormNode) {
// the bottom: this asks for another content area, and those report what is
// left.
func (p *placer) flow(n *FormNode) {
if wraps(layoutOf(n)) {
if wraps(p.effective(n)) {
p.flowLines(n)
return
}
p.flowStack(n)
}

// effective is the layout the flowing chain works in. It is [layoutOf]
// everywhere but the one node [placer.forcedTb] names.
func (p *placer) effective(n *FormNode) string {
if p.forcedTb(n) {
return "tb"
}
return layoutOf(n)
}

// flowStack puts a splittable stack's children one below the other.
func (p *placer) flowStack(n *FormNode) {
lv := p.chain[len(p.chain)-1]
Expand All @@ -239,7 +287,7 @@ func (p *placer) flowStack(n *FormNode) {
}
p.flow(kid)
p.pop()
} else if !p.whole(kid, lv, layoutOf(n)) {
} else if !p.whole(kid, lv, p.effective(n)) {
p.rejectKids(kids[i+1:], p.blocked)
return
}
Expand Down
42 changes: 42 additions & 0 deletions paginate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,3 +463,45 @@ func TestSplittabilityIsAskedOfTheWholeChainAndNotOfOneNode(t *testing.T) {
}
}
}

// TestARootThatWritesNoLayoutFlows is the one node pdfium reads differently
// from pdf.js, and the difference is a sheet count rather than a coordinate.
//
// pdf.js reads an absent layout as "position" wherever it appears, so the
// outermost subform of these three templates would be laid out in one piece on
// one sheet and the <break> inside it would never be reached — a positioned
// container is never flowed. pdfium's GetLayout
// (cxfa_contentlayoutprocessor.cpp:365-379) returns Tb instead, but only where
// the node's parent is the <form> root, and only where the attribute does not
// parse. See [placer.forcedTb].
//
// Three of the corpus's four such roots are forms this package put on one
// sheet where pdfium and pdf.js both use two.
func TestARootThatWritesNoLayoutFlows(t *testing.T) {
body := `<pageSet><pageArea name="P"><medium long="100pt" short="100pt"/>
<contentArea w="100pt" h="100pt"/></pageArea></pageSet>
<subform name="One"><field name="A" w="5pt" h="5pt"/></subform>
<subform name="Two"><break before="pageArea" startNew="1"/>
<field name="B" w="5pt" h="5pt"/></subform>`
for _, tc := range []struct {
what string
root string
pages int
want []string
}{
{"no layout at all: pdfium's Tb, so the break fires", `<subform name="f">`, 2,
[]string{"field f.One.A 0,0 5x5", "field f.Two.B 0,0 5x5"}},
{"layout written as position: the attribute parses, so no forcing", `<subform name="f" layout="position">`, 1,
[]string{"field f.One.A 0,0 5x5", "field f.Two.B 0,0 5x5"}},
{"a layout nobody can read is an absent one", `<subform name="f" layout="sideways">`, 2,
[]string{"field f.One.A 0,0 5x5", "field f.Two.B 0,0 5x5"}},
{"a written flow layout is itself", `<subform name="f" layout="tb">`, 2,
[]string{"field f.One.A 0,0 5x5", "field f.Two.B 0,0 5x5"}},
} {
l := laidOut(t, `<template>`+tc.root+body+`</subform></template>`)
if len(l.Pages) != tc.pages {
t.Errorf("%s: %d sheets, want %d", tc.what, len(l.Pages), tc.pages)
}
same(t, tc.what, laid(l), tc.want)
}
}