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
31 changes: 30 additions & 1 deletion corpus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1585,8 +1585,16 @@ func readPdfium(name string) (map[string][]pdfiumBox, int, error) {
// template did not name, which contributes nothing to [FormNode.Path] either.
// The two are the same walk written differently, so this is a rewrite and not
// a guess.
//
// A dot INSIDE a name is escaped, which is why the steps are cut by
// [somSteps] and not by strings.Split. 35 of the 559 dumped forms name an
// element that way — "ArtifactedHeader[0].A\.Original[0]" — and splitting on
// every dot put a backslash in the rewritten path, so the key matched nothing
// this package emits and 592 leaves were paired with nothing at all. The
// agreement rates were never wrong, both sides being unpaired, but they could
// not see those boxes.
func somPath(som string) string {
steps := strings.Split(som, ".")
steps := somSteps(som)
var out []string
for _, s := range steps {
name, idx := s, 0
Expand All @@ -1605,6 +1613,27 @@ func somPath(som string) string {
return strings.Join(out, ".")
}

// somSteps cuts a SOM expression at its UNESCAPED dots. pdfium writes a name
// holding a dot with a backslash before it (CXFA_Object::GetSOMExpression),
// and the backslash is not part of the name.
func somSteps(som string) []string {
var out []string
var cur strings.Builder
for i := 0; i < len(som); i++ {
switch {
case som[i] == '\\' && i+1 < len(som):
i++
cur.WriteByte(som[i])
case som[i] == '.':
out = append(out, cur.String())
cur.Reset()
default:
cur.WriteByte(som[i])
}
}
return append(out, cur.String())
}

// TestIntraLineAgainstPdfium checks where a box went ACROSS a line against a
// reference that computes the answer.
//
Expand Down
42 changes: 34 additions & 8 deletions flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,41 @@ func (p *placer) measure(n *FormNode, wide, colW Measure) (Measure, string) {
if err != nil {
return 0, fmt.Sprintf("its height is written as h=%q, which is not a length", n.Template.Get("h"))
}
if !ok {
own = 0
// pdfium disables the auto-size only for a height ABOVE
// kXFALayoutPrecision (0.0005pt, cxfa_contentlayoutprocessor.h:28), so a
// height written as nought is no height and the content decides. No
// container of the 560-form corpus writes one, which is why this is a
// guard and not a measured claim.
if own <= 0 {
ok = false
}
// A WRITTEN height is the height. It is a ceiling and not a floor: what
// the container holds does not grow it.
//
// pdf.js takes the other reading — Math.max(this[$extra].height + marginV,
// this.h || 0) (template.js:5222) — and this package followed it until the
// oracle was asked. pdfium decides it in two steps.
// CalculateContainerSpecifiedSize (cxfa_contentlayoutprocessor.cpp:97-137)
// turns bContainerHeightAutoSize off as soon as a subform writes an h above
// kXFALayoutPrecision, and CalculateContainerComponentSizeFromContentSize
// (:140-184) then leaves componentSize.height at that written height,
// never reading fContentCalculatedHeight at all.
//
// Measured over the corpus against pdfium, following pdfium: leaves
// agreeing on sheet AND y 161453/177676 -> 161937/177676, forms agreeing on
// every one of them 395/559 -> 403/559, no form worse and not one x moved.
// ca-cra__rc243-fill-26e's Detail2 row is the case in the small: it writes
// h="4.233mm" and holds children reaching 8.466mm, and pdfium puts the row
// that follows at 607.0961 — the written 11.9991 below the row above, to
// the point.
//
// The content height is still computed first, and a reason it could not be
// still wins, so a container holding something unmeasurable stays
// unmeasurable rather than falling back on what is written.
if ok {
return own, ""
}
// pdf.js: Math.max(this[$extra].height + marginV, this.h || 0)
// (template.js:5222). A container is as tall as what it holds even where
// the template writes a height, which is why holding a thing of unknown
// height leaves the container's own height unknown too rather than falling
// back on what is written.
return max(content+in.vertical(), own), ""
return content + in.vertical(), ""
}

// innerWide is how much horizontal room a container gives what it holds.
Expand Down
41 changes: 41 additions & 0 deletions flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,44 @@ func TestInsideAContainerThatMovesWholeAStackStillStops(t *testing.T) {
"f.G.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"})
}

// TestAWrittenHeightIsACeilingNotAFloor pins the rule the pdfium oracle
// settled: a container that writes an h is that tall, and what it holds does
// not stretch it. pdf.js takes the max of the two (template.js:5222) and this
// package followed that until the oracle was asked; pdfium's
// CalculateContainerSpecifiedSize turns the auto-size off outright
// (cxfa_contentlayoutprocessor.cpp:97-137).
func TestAWrittenHeightIsACeilingNotAFloor(t *testing.T) {
for _, tc := range []struct {
what string
body string
want []string
}{
{"content taller than the written height does not stretch it",
// The box holds a child reaching 24 points and writes 12, so the
// draw that follows it begins at 12 and not at 24.
`<subform name="Box" h="12pt"><draw name="A" y="12pt" w="1pt" h="12pt"/></subform>
<draw name="After" w="1pt" h="3pt"/>`,
[]string{"draw f.Box.A 0,12 1x12", "draw f.After 0,12 1x3"}},
{"content shorter than the written height does not shrink it",
`<subform name="Box" h="30pt"><draw name="A" w="1pt" h="4pt"/></subform>
<draw name="After" w="1pt" h="3pt"/>`,
[]string{"draw f.Box.A 0,0 1x4", "draw f.After 0,30 1x3"}},
{"a height written as nought is no height, and the content decides",
// pdfium reads a height only above kXFALayoutPrecision
// (cxfa_contentlayoutprocessor.h:28), so this stacks as if the
// attribute were absent.
`<subform name="Box" h="0pt"><draw name="A" w="1pt" h="7pt"/></subform>
<draw name="After" w="1pt" h="3pt"/>`,
[]string{"draw f.Box.A 0,0 1x7", "draw f.After 0,7 1x3"}},
{"a written height with a margin is still the whole of it",
`<subform name="Box" h="12pt"><margin topInset="5pt" bottomInset="5pt"/>
<draw name="A" w="1pt" h="20pt"/></subform>
<draw name="After" w="1pt" h="3pt"/>`,
[]string{"draw f.Box.A 0,5 1x20", "draw f.After 0,12 1x3"}},
} {
if got := laid(laidOut(t, page(`w="500pt" h="500pt"`, tc.body))); strings.Join(got, "\n") != strings.Join(tc.want, "\n") {
t.Errorf("%s:\n got %v\n want %v", tc.what, got, tc.want)
}
}
}