Skip to content

Cut a positioned container where its author permits it, and place 182 more leaves - #21

Merged
tannevaled merged 2 commits into
mainfrom
split-positioned
Sep 5, 2026
Merged

Cut a positioned container where its author permits it, and place 182 more leaves#21
tannevaled merged 2 commits into
mainfrom
split-positioned

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Closes nothing; it is one slice of #7.

pdf.js refuses to split a container at layout === "position" before it
reads keep.intact, so <keep intact="none"/> — the author's explicit "you may
break this" — is unreachable in it. This package followed pdf.js and put 1 423
body leaves off the paper across 39 forms
where pdfium put 25 across 6.

The rule is narrower than #7's diagnosis said, and that is the first finding

FindSplitPos calls 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
— so the proposed cut is driven to nought,
FindSplitPos returns nought, and InsertFlowedItem's
if (fSplitPos > kXFALayoutPrecision) is false: the container moves whole —
unless a <keep> is written, where GetIntactFromKeep returns what the
author wrote.

So pdfium does not cut positioned containers in general. It cuts one exactly
where <keep intact="none"/> is written on it.
#7's previous comment read the
exemption off bCalculateMargin = eLayout != Position, which governs the margin
arithmetic and not the gate.

The population is 58 positioned subforms across 17 of the corpus's 560 forms
— not the 39 whose off-paper leaves the position clause dominates. Recorded
before measuring anything, so the measurement is not read against the wrong
expectation.

The mechanism

pdfium's split is a post-hoc slice of an already-laid-out item tree, so
cuttable is a different question from splittable and answered by a different
function. The container is never put on the flowing chain: it is laid out
whole and then cut, and each part's children keep the y the template wrote for
them, less however much of the container is on the sheets before —
SplitLayoutItem's pChildItem->s_pos_.y -= fSplitPos. Letting the flow split
it would have placed every child by the cursor instead, which is the design
constraint #7 named.

One difference, deliberate: pdfium recurses into a child whose intact is none
— a field of a positioned container is one — and, finding nothing finer, cuts
it in half
, emitting two layout items for the one node. A Box is one
rectangle for one occurrence and Layout promises every body leaf appears
exactly once, so the cut is refused there and the container moves whole,
which is also what pdfium does when FindSplitPos comes back at nought.

Measured, against pdfium over 559 forms

before after
paired body leaves 177676 177676
ambiguous keys dropped 9157 9157
agree on x 173806 (97.82%) 173806 (97.82%)
agree on sheet+y 162018 (91.19%) 162200 (91.29%)
forms perfect on x 465/559 465/559
forms perfect on sheet+y 403/559 403/559

Exactly two forms move and no form is worse on either measure:

us-uscis__i-956    42 -> 168  of 491
us-uscis__i-956g  118 -> 174  of 538

The judge's pairing was re-audited, because this moves boxes onto sheets they
were not on: the population is identical, 177676 paired and 9157 dropped before
and after. Nothing became ambiguous — that is what the bisect refusal buys.

i-956's page-area sequence goes from Page2 ×2, Page3 ×10 to
Page2 ×13, Page3 — pdfium's shape is Page2 ×16, Page3 — which repairs the
regression #20 recorded rather than tuned away. Sheets: corpus 3086 → 3088,
i-956 12 → 14 (pdfium 17). Off the paper: 1423 → 1378 boxes across 39 → 37
forms.

The cut positions are pdfium's, to four decimals

An instrumented build over all 560 forms attempts the cut five times on four
containers
, all on i-956 and i-956g. i-956's sfPart2:

behind=0.0000     room=162.1843  at=162.1843    5 children
behind=162.1843   room=648.0000  at=744.1824   19 children
behind=744.1824                  rest           1 child

pdfium's own trace: splitPos=162.1843, then 581.9981 from 162.1843, and three
items of 162.1843 + 581.9981 + 222.0010. Independent arithmetic, the same two
numbers, the same partition of the 25 children.
The second is sfP2_Line13's
written y, reached because GetIntact() answers ContentArea for a positioned
subform with no keep.

What it does not do, and what it costs

  • Two of the five attempts are refused because the cut would bisect a field:
    i-956's sfPart7_2 and i-956g's sfCertification. Both still move whole.
  • Of the 58 permitted containers, 54 never fail to fit, so the permission is
    never exercised on them. The rule is real, correct and rare.
  • +182 of 15 658 sheet+y disagreements is 1.2%, against a bucket labelled
    42.1%. The label was a correlation over whole forms; the reason it could not be
    the prediction is above, and was written down before the measurement.
  • An exclGroup and a row carrying the permission would be cut by pdfium too; the
    corpus writes neither, so nothing would say whether it were right.

Gate: 100% statement coverage including every error branch, go vet, gofmt -l
empty against Go 1.27.1 via GOTOOLCHAIN, -race, thirteen cross-compile
targets, CGO_ENABLED=0, GOWORK=off.

🤖 Generated with Claude Code

… more leaves

pdf.js refuses to split a container at `layout === "position"` BEFORE it reads
`keep.intact`, so `<keep intact="none"/>` — the author's explicit "you may break
this" — is unreachable in it. This package followed pdf.js and put 1 423 body
leaves off the paper across 39 forms where pdfium put 25 across 6 (#7).

pdfium reads the permission. `FindSplitPos` calls `FindLayoutItemSplitPos`
(cxfa_contentlayoutprocessor.cpp:503-588), which switches on
`CXFA_Node::GetIntact()`; and `GetIntact` (cxfa_node.cpp:1536-1591) answers
ContentArea for a positioned subform — the cut is driven to nought and the
container moves whole — UNLESS a `<keep>` is written, in which case
`GetIntactFromKeep` returns what the author wrote.

So the rule is NARROWER than the diagnosis on #7 stated. pdfium does not split
positioned containers in general; it splits one exactly where the permission is
written. The previous comment read the exemption off `bCalculateMargin =
eLayout != Position`, which governs the margin arithmetic and not the gate.

The population: 58 positioned subforms across 17 of the corpus's 560 forms. Not
the 39 forms whose off-paper leaves the position clause dominates.

## The mechanism, which is pdfium's and not pdf.js's

pdfium's split is a post-hoc slice of an already-laid-out item tree, so
[placer.cuttable] is a different question from [placer.splittable] and answered
by a different function. The container is never put on the flowing chain: it is
laid out whole and then cut, and each part's children keep the y the template
wrote for them, less however much of the container is on the sheets before —
SplitLayoutItem's `pChildItem->s_pos_.y -= fSplitPos`. Letting the flow split it
would have placed every child by the cursor instead.

The cut is walked up out of whatever it lands inside, which is pdfium's guard at
both ends. One difference, deliberate: pdfium recurses into a child whose intact
is "none" — a field of a positioned container is one — and, finding nothing
finer, CUTS IT IN HALF, emitting two layout items for the one node. A [Box] is
one rectangle for one occurrence and [Layout] promises every body leaf appears
exactly once, so the cut is REFUSED there and the container moves whole, which
is also what pdfium does when FindSplitPos comes back at nought.

## Measured, against pdfium over 559 forms

                            before        after
  paired body leaves        177676        177676
  ambiguous keys dropped      9157          9157
  agree on x                173806        173806   (97.82%, unmoved)
  agree on sheet+y          162018    ->  162200   (91.19% -> 91.29%)
  forms perfect on x          465           465
  forms perfect on sheet+y    403           403

Exactly two forms move and NO form is worse on either measure:

  us-uscis__i-956    42 -> 168  of 491
  us-uscis__i-956g  118 -> 174  of 538

The judge's pairing was re-audited because this moves boxes onto sheets they
were not on: the population is identical, 177676 paired and 9157 dropped before
and after. Nothing became ambiguous, which is what the bisect refusal buys.

i-956's page-area sequence goes from `Page2 x2, Page3 x10` to `Page2 x13, Page3`
— pdfium's shape, `Page2 x16, Page3` — which repairs the regression #20 recorded
rather than tuned away. Sheets: corpus 3086 -> 3088, i-956 12 -> 14 (pdfium 17).
Off the paper: 1423 -> 1378 boxes across 39 -> 37 forms.

## The cut positions are pdfium's, to four decimals

An instrumented build over all 560 forms attempts the cut FIVE times on four
containers. i-956's sfPart2:

  behind=0.0000      room=162.1843  at=162.1843   5 children
  behind=162.1843    room=648.0000  at=744.1824  19 children
  behind=744.1824                   rest          1 child

pdfium's own trace: splitPos=162.1843, then 581.9981 from 162.1843, and three
items of 162.1843 + 581.9981 + 222.0010. Independent arithmetic, same numbers,
same partition of the 25 children.

Two of the five attempts are refused because the cut would bisect a field:
i-956's sfPart7_2 and i-956g's sfCertification. Both still move whole, and that
is the named cost.

Gate: 100% statement coverage, go vet, gofmt clean against Go 1.27.1, -race,
thirteen cross-compile targets, CGO_ENABLED=0, GOWORK=off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The intact table left out the two arms that answer for a container carrying no
<keep> — a positioned subform and a row, which pdfium's GetIntact reads the
same way. The local run reported 100% because XFACORPUS is set here and the
corpus reaches that line through a real form; CI, which sets no corpus, does
not. A check that passed for the wrong reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit ef8aaa6 into main Sep 5, 2026
1 check passed
@tannevaled
tannevaled deleted the split-positioned branch September 5, 2026 14:20
tannevaled added a commit that referenced this pull request Sep 5, 2026
* Bring the corpus census back to what the corpus says

doc.go's census block quoted 81 734 fields placed on 3 090 sheets, and both
numbers went stale before v0.16.0: every field of every body has been placed
since #18, and the corpus has come to 3 088 sheets since #21. The third line
counted the 16 fields that were left over, and there are none.

Measured by TestPlacementOverTheCorpus on the 560-template corpus, which is
where all four numbers come from and which reports them itself. The line that
replaces the leftover count is the one thing in the block that is still not in
one-to-one correspondence with the paper: 12 elements of the page areas' own
furniture belong to a page area no sheet of their form ever is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Reach a page area a break names, whose <occur max> is spent

Six forms of the corpus lay their body on sheets pointing the wrong way.
ca-cra t2042, t2121 and t2125, each in two years, write a portrait Pg1 with
<occur max="1"/> and a landscape page area beside it, and every body subform
carries <breakBefore targetType="pageArea" target="Pg1" startNew="1"/>. This
package refused the target once Pg1's max was spent and let the sequence answer
instead, which is the landscape page area — 612 points tall where the form
wants 792 — so 403 leaves fell off the bottom of the paper.

pdfium reads a page area's <occur max> in exactly one place: the branch of
GetNextAvailPageArea that REPEATS the page area in hand
(cxfa_viewlayoutprocessor.cpp:1552-1578). Every other route runs through
FindPageAreaFromPageSet_Ordered, which scans the siblings after the page area
in hand FOR THE TARGET and, finding none, starts the page set again from its
first child and takes the target there (:1582-1607). That function never reads
a page area's occur at all; the only max it reads is the page SET's
(:1329-1341). An instrumented pdfium says the same at runtime, on t2125:

  BREAKNODE breakBefore on subform:Page2 target="Pg1" resolved=pageArea:Pg1
  | GNAPA advancing: scan siblings after pageArea:Pg1
  | | FPAS_ORD startAfter=pageArea:Pg1 target=pageArea:Pg1     (no match)
  | | FPAS_ORD startAfter=-             target=pageArea:Pg1     (restart)
  | | GNAPA -> pageArea:Pg1 (RESTART of page set pageSet:)

So a page area's max bounds how often the SEQUENCE runs into it and not how
often a break may ask for it by name. This package already held the other half
of that rule — starting a page set again forgets the counts of the page areas
below it, see pager.cleanKids — and pager.reach is that same restart, asked for
by a target rather than by running off the end of the set.

What is NOT changed is the break that names nothing. Two thousand of the
corpus's two and a half thousand breaks are <breakBefore targetType="pageArea"
startNew="1"/>, whose page area is only the one in hand standing in for a
target; pdfium's scan accepts any page area there, so the sequence moves on and
a max still means something. Getting that wrong first cost 2 367 leaves on four
other forms, which is why breakTo now carries whether the template NAMED it.

Measured over the 559 forms pdfium lays out, hidden excluded from both sides:

                              before   after
  body leaves off the paper     1378    1000
  forms with one                  37      34
  sheets whose SIZE disagrees      13       7
  ...of those, orientation          6       0
  agree on x            173806/177676  unchanged
  agree on sheet+y      162200/177676  unchanged
  corpus sheets                   3088   3088

Six forms move and none is worse. t2125-24e 129 -> 0, t2042-24e 94 -> 0,
t2121-24e 17 -> 0, t2125-25e 178 -> 66, t2121-25e 96 -> 71, t2042-25e 67 -> 66;
the remainder on the 25e forms is this package's text measurement, not this
rule. t2125-25e's page-area sequence is now pdfium's sheet for sheet.

The judge did not move by one leaf, and that is worth writing down: it compares
x and sheet+y, so a leaf at the same place on a sheet of the wrong ORIENTATION
counts as full agreement. Nothing in this repository compares a sheet's size.

Nine forms in all carry a pageArea <occur max> a break names — the six above,
ca-cra t1135 in two years and us-uscis i-612 — and 60 carry one at all.

Two tests changed their expectation with the rule and are renamed for it, and
three are added: the untargeted break that must still move on, a page set whose
own <occur> is spent, and a <pageArea> written outside every page set, which a
target can reach by id and which has no page set to start again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tannevaled added a commit that referenced this pull request Sep 5, 2026
…red (#24)

Two measurements this repository could not make, and one number it published
that a sharper instrument contradicts.

THE SHEET. Every judge here compares a box — its x, and the sheet it landed
on with its y — and none of them compares the sheet itself. v0.18.0 moved 378
leaves onto correctly oriented sheets and not one figure in corpus_test.go
changed by a unit, because a leaf at the same (sheet, x, y) on a 612x792 sheet
and on a 792x612 one counted as agreement.

TestSheetsAgainstPdfium asks both halves. The SIZE is the weak one: 528 of the
559 forms give every page area of a form the same medium, so a sheet opened on
the wrong page area is still the right size. Over the corpus the size agrees on
3 035 of 3 035 sheets, the largest disagreement in either dimension being
0.0000 pt — and v0.18.0's reported residue of "7 sheets whose size disagrees"
was 7 FORMS tying at the fourth decimal, 1008.0 against 1008.0001, not a
disagreement about paper at all.

Which PAGE AREA a sheet was opened on has no such blind spot, and it finds ten
sheets on three forms: us-uscis__g-1055 6, i-956g 3, i-956 1 — the three forms
#21 and #23 left open, counted for the first time. placer.sheetAreas records
it here and the probe reads it from GetPage(i)'s form node there, so it is
known even on the 91 sheets of g-1055 whose page area draws no furniture.
Neither side reads it off the geometry being judged.

THE LINE COUNT. doc.go's "same line count as pdfium 25.9% / 82.5%" was not a
line count: it was (our height less pdfium's) over OUR line height, bucketed.
The proxy flatters, because a leaf that disagrees on the number of lines AND on
how tall a line is can come to the same height. pdfium's probe now reports its
own line count per leaf, and the answer is 23.2% and 76.3%. 1 002 leaves the
proxy scored as agreeing take a different number of lines — fw9's
Page4.Col2.F among them, four lines of 9 pt here against pdfium's three of 12.

Where the counts do agree the whole residue is the FIRST line: 5 372 of 5 373.
Our height for every line after the first is pdfium's to a hundredth of a point
on 1 177 of 1 178. pdfium makes the first line the font size; pdf.js makes it
the face's line height less its gap, and this follows pdf.js — so that one is a
disagreement with pdfium and not a defect against the reference. Nothing in the
measurement is changed for it.

The layout does not move: all 560 forms are byte-identical before and after.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant