xfa

package module
v0.21.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

README

xfa — go-pdfkit

License

Read the form inside a PDF whose pages are blank. Pure Go, no cgo.

A PDF form comes in two kinds and only one of them is a PDF form. Of 2 240 real government forms, 560 carry an XFA package — and 546 of those are static: a second, proprietary description of a form already drawn on the pages, which go-pdfkit/forms reads and fills without help.

The other fourteen are dynamic. Their pages hold a panel reading "Please wait... your PDF viewer may not be able to display this type of document", and the form exists only as XML. Adobe removed the format from PDF 2.0; no browser and no other reader lays one out. Somebody handed such a file has a document that looks blank and is not.

What it does not do, and why that is a decision

It does not run the forms' scripts. That is measured, not assumed: of 5 744 scripts across those fourteen forms, 5 131 are handlers for enter, exit, change and click — they fire when somebody types. Only 188 run at load, so the form a reader first sees does not depend on them.

FormCalc, XFA's other scripting language, is not read either: every script in the corpus declares itself as JavaScript. The reference implementation (pdf.js) spends 37 KB on FormCalc that this does not need.

Joining a field to its value

Bind takes a parsed template and parsed datasets and hands back each field with the value bound to it — name, path, data path, value — so a caller can fill in a form without knowing XFA.

That is not name-matching. A form binds explicitly through <bind> elements, and where it does not, each container consumes the next unclaimed data node of its own name in document order. The two trees are named differently on purpose: the template names a field for the layout, the data names it for the record. In one real form the field at FormulaireGf.Page1.MEP.NumeroCerfa takes its value from the attribute at FormulaireGf.MetierGf.DemandePensionAscendants/@Cerfa.

Measured over all 560 XFA packages in the corpus:

templates read 560
<bind> elements 26 573
match="none" 11 875
match="global" 11 544
— no match (means once) 2 589
match="dataRef", every one with a ref 565
fields placed 80 482
fields bound to a data node 60 865
<bind> expressions holding a construct this does not read 0

Every ref in the corpus is a plain path, a $record path, a $ path, or one of those with [*]. None of SOM's hard parts — FormCalc subexpressions .[…], JavaScript predicates .(…), negative indices — occurs even once.

Where this deliberately differs from pdf.js

The reference implementation is Mozilla's src/core/xfa/bind.js, and this follows it closely. Four differences, each deliberate:

  • It invents no data nodes. pdf.js, meeting a ref that matches nothing, creates the nodes the expression describes so a person can type into them. This reads documents rather than editing them, so the field is reported unbound — the same answer, since an invented node is empty.
  • It does not repeat a container to satisfy occur's initial. Those clones carry no data; they change the layout, not the values.
  • It descends into pageSet and pageArea. pdf.js binds nothing there. Of 82 386 template fields in the corpus, 628 sit under a page area across 177 forms — and one form has no fields anywhere else, so dropping them silently would lose it whole.
  • It stops a global search that has begun to repeat. pdf.js's global lookup does not skip what it has already handed back, so match="global" under an unbounded occur asks for the same node for ever.

It is also linear where pdf.js is quadratic: a search for a name under a data node resumes where the last one stopped, rather than beginning again.

Where a field goes on the page

Expand joins the two trees and returns the form as a document holds it — one node per occurrence, so a table row written once and filled three times is three nodes. Layout runs over that and never over the template, which is what pdf.js's binder does too (bind.js:61).

Place lays a form out on paper. Under a positioned layout a box's place is its own x and y added to those of every container above it, down from the content area's origin, with anchorType and rotate resolved. Under a flow layout the coordinates are thrown away and the children are stacked, and this follows two of the six:

  • tb and table stack downwards. Each child begins where the one above it ends (layout.js:144-158, extra.height += h), and a container's own height is the taller of what it holds and what the template writes for it (template.js:5222) — so the heights are arrived at from the leaves upwards, out of the literal w and h the template already carries. A container's margin is outside what it holds: it moves the children in and adds all four insets to the height reported upwards.
  • row cuts its cells from the columnWidths of the table above it (html_utils.js:81-106), a cell spanning colSpan of them, and stretches every cell to the height of the tallest.

lr-tb wraps its children onto lines, which needs a line-breaking rule; rl-tb and rl-row fill from the right, which needs a width. Only the first child of an lr-tb is placed.

A leaf's own height is often not written either, and then it is its text: broken into lines at the width it has, a line count times a line height. See Measuring text.

Nothing is dropped. Every field and every draw of the form's body comes back in exactly one of Page.Boxes and Layout.Unplaced, and each unplaced one carries the reason in words. A layout that reaches part of a form is useful; one that silently drops the rest is not, because nothing downstream can tell a form that was laid out from a form that was half laid out. A page area's own furniture is the exception, and it is not a leak: a letterhead belongs to the sheet, so it is drawn once on every sheet that page area makes.

Where it runs off the bottom, it turns the page

Which page comes next is not "another of the same". It is decided by a state machine — the page set's relation, each page area's <occur>, the parity of the page number, and the explicit <breakBefore> and <breakAfter> the template writes — and this follows pdf.js's (template.js:4064-4236, 5418-5657) rather than assuming. Every container of the chain being flowed begins again at the top of the new content area, which is what pdf.js arrives at by re-entering the whole tree with a new space.

Breaks are not a corner: 435 of the 560 forms carry a <breakBefore>, and 2 155 of the 2 561 in the corpus are startNew="1", which means "a fresh sheet here, once".

Splitting a container is a chain question, not a node question

A container that may be split is broken across the boundary: what it placed before the break stays where it is, and the rest of it begins again at the top of the next content area — which is what pdf.js's saved generator and failingNode do (layout.js:38-53).

Whether it may is a property of the whole chain above it, and Subform[$isSplittable] (template.js:4940-4975) asks the container above before it looks at the container itself. Four things must all hold:

  1. the container above it is splittable — the recursion ends at the <template> element, which answers yes (template.js:5401-5403), and every other kind of node inherits the base answer, no (xfa_object.js:214-216). The one that matters is <area>: it holds body content, it is not a subform, and $getSubformParent does not skip it — so nothing inside an area is ever split;
  2. its own layout is neither position nor anything containing row;
  3. its <keep intact> is none. 1 355 elements in the corpus carry keep intact="contentArea", a designer saying "do not let this row land half on one page and half on the next". An exclGroup has the same predicate without this clause (template.js:2405-2429), read there rather than assumed;
  4. if the container above it has a layout ending in -tb and has already put something on the line in hand, it is not splittable. That clause is not code here and the doc comment says why: no container this package flows through ends in -tb, so it cannot change an answer, and it needs numberInLine, which the lr-tb slice will bring.
What moves whole is still put on the paper

A container that may not be split is not dropped for being too tall. checkDimensions returns true outright while the sheet has had nothing that moves in one piece (layout.js:266-268), and the one that claims that pass has noLayoutFailure switched on before its own check runs (setFirstUnsplittable, template.js:313-319), so it cannot fail either — and nor can anything inside it, because the flag is cleared only on the way out of that same node.

Everything after it on the sheet is checked, and what does not fit sends the whole chain to the next content area, which is what splitting the containers above it means. There it is first in its turn, and goes down whatever its height. So a container taller than a whole content area comes out one per sheet, hanging over the bottom, exactly as pdf.js draws it. The pass is handed out once per sheet, not once per content area: pdf.js clears firstUnsplittable in the page loop, before the loop over that sheet's content areas (template.js:5536-5538).

The recursion pdf.js dies on is not in the branch it looks to be in

pdf.js exhausts its stack on 7 of the 560 forms, going round between PageSet[$getNextPage] and PageArea[$getNextPage]. The branch it is usually blamed on is the last one:

this[$cleanPage]();
return this[$getNextPage]();

$cleanPage clears the [$extra] of every page area and page set below the set and not the set's own, so the call returns to state no different from the state it left.

The defect is not in that one branch. The branch above it does the same thing — a page set whose <occur> still allows another run resets its own indices and calls itself, and comes back to the same place when every page area below it has spent its own <occur>. A faithful port has to guard every way a page set can start itself again, not the one named after cleaning. Both branches were found by porting the machine and running it over the corpus, not by reading it: the second one was a stack overflow in this package's own tests.

Guarding a recursion is not the same as answering it, and the guard alone was the wrong answer — see Getting more sheets below, which is what the restart was for.

Measured over the same 560 packages:

fields in the body of the expanded forms 81 750
placed 71 230
draws placed with them 130 846
sheets they came to 2 737
a place computed and nowhere left to put it 6 208
under a layout this does not follow (lr-tb) 4 303
anchored by a corner, with no size of its own 9

Measuring text

Half the corpus was held up by one thing, and it was not a font.

Only 8 466 draws and 598 fields of the corpus's ~234 000 leaves write no height — under four per cent. But a stack is a chain: where a tb container's second child begins is the height of the first, so one leaf nobody can measure leaves every sibling below it, and every sibling of every container above it, with nowhere to begin. Those few thousand leaves held up 44 246 fields, more than half the corpus.

The regime this measures in is the reference's own

pdf.js measures with the fonts of the PDF the XFA package came in, and has a written-down fallback for when it has none:

  • TextMeasure.addString with no font — "When we have no font in the pdf, just use the font size as default width" (text.js:211-219): one em per character, a line 1.2 ems tall, and a first line one em tall.
  • getMetrics with no font (fonts.js:173-179): the constants { lineHeight: 12, lineGap: 2, lineNoGap: 10 }.

This package implements that regime and nothing else, which is why go.mod still has no dependencies. It is not a stand-in for something better: it is what the reference runs on a form whose fonts it cannot resolve, so the two sets of numbers are directly comparable rather than merely plausible.

The size is not the template's, and that is pdf.js's doing. FontInfo asks the font finder for the typeface (text.js:42) and, when it does not have it, replaces the whole of xfaFont with the default's (:43-46, 55-83) — typeface Courier, size 10, letter spacing 0. A <font size="14pt"> beside a typeface nobody has is discarded along with it. So is a <span style="font-size:14pt">, and so is line-height. Honouring the written size would disagree with the reference on nearly every leaf.

When real advances are wanted, they belong in go-opentype/opentype, which already exposes glyph advances and units per em. Filling glyph.w from a face is the whole change; the line breaking and the arithmetic above it do not move.

Rich text is walked in the order the markup writes it

2 255 of the unsized draws hold <exData contentType="text/html"> rather than a string, and 32 109 of the corpus's <p> elements hold text both before and after a <span>. So character data inside a rich text is kept as ordered #text children rather than joined and trimmed, which is what pdf.js's XmlObject does with it (xfa_object.js:865-887).

The normalisation is pdf.js's, applied run by run rather than once over the joined text, because that is where pdf.js applies it: a newline is removed, runs of whitespace collapse to one space unless the element writes xfa-spacerun:yes44 099 spans of the corpus do — and <body> and <html> drop whitespace-only text altogether.

The defect not to copy: <b> and <i> call measure.pushFont (xhtml.js:377, 467), and TextMeasure has no such method. A rich text carrying either throws inside pdf.js's own measurement. No unsized leaf of the corpus writes one.

Two things decided it, and neither is about fonts

The last no-break space of a run becomes an ordinary one. pdf.js does this to every text node of the document (parser.js:61-63), with the comment "normally by definition a &nbsp is unbreakable but in real life Acrobat can break strings on &nbsp". It decides line breaking outright: Line 2 of Form\u00a0AB(S11) in a 63-point column is four lines with the space and three without it. Until it was applied outside rich text, 31 container heights disagreed with pdf.js, all of them in tables of ca-cra and us-irs forms.

A leaf whose text gives no height is not left unmeasured. computeBbox (html_utils.js:290-324) is called on every draw and every field before it returns, and where the height is still unwritten it fills it in from minH — or from nought under a positioned parent that writes a height of its own. The container above stacks that. 7 475 of the unsized draws write a minH.

The one case this refuses is maxH above nought, where pdf.js answers with the room the leaf has rather than with anything the template wrote. A height is arrived at before the room it will go in is known — that is why the measurement is a pass of its own — and no corpus leaf that needs it writes one.

What the measurement is worth

25 585 more fields, 29 123 to 54 708.

what blocks a field, before and after slice 3 slice 4
placed 29 123 54 708
waiting on a height only measuring text would give 44 246 0
below a height written as =0mm 3 649 16 936
under lr-tb 3 124 4 303
a place computed and nowhere left to put it 1 608 5 794

The three blockers that grew grew because they were behind the wall: a field below an unmeasurable leaf was reported for the leaf, and is now reported for whatever is genuinely in its way. h="=0mm" is now the largest single thing between here and a whole corpus — and the section below settles what it means.

A measure written as a calculation

XFA lets a length be written as a calculation, with a leading =. The corpus writes exactly one — h="=0mm", 955 times over 101 of its 560 forms, on draws named Line — and that one shape held up 16 936 fields, because a stack cannot say where its next child begins while a height above it is unreadable.

The tempting move is to copy pdf.js, and it would be right by accident. It places all of them: getMeasurement's pattern /([+-]?\d+\.?\d*)(.*)/ is unanchored, so it matches the 0mm inside the string having never noticed the = at all (utils.js:83-87). That is a property of a regular expression, not a statement about XFA — the same shape as the px this package refused in its first slice.

The authority is pdfium's CXFA_Measurement, Foxit's implementation and the closest to Adobe's own. SetString strips a leading = deliberately, and then parses what is left leniently (cxfa_measurement.cpp):

  • blanks before the number are skipped, and only spaces are — FXSYS_wcstof skips ' ' and nothing else (fx_extension.cpp:41-45), so a tab stops the number before it starts;
  • the value is the longest floating-point number beginning what is left;
  • a value that is not finite is nought, which is what isfinite is there for;
  • the unit is the whole of the rest, matched exactly and with its case.

So ="0mm" really is nought. The two implementations agree on the answer and only one of them agrees for a reason, which is why the tests here are a port of cxfa_measurement_unittest.cpp rather than an agreement count with pdf.js.

An expression is not evaluated, and that is the rule rather than a gap. ="Foo.h * 2" has nothing numeric beginning it, so under pdfium's rule it is nought in a unit pdfium does not know — and a length in a unit it does not know is nought points, because ToUnitInternal has no arm for one and ToUnit turns "cannot convert" into nought. Nought is the reference's answer for an expression, not a shortfall standing in for one. A script engine behind this would disagree with pdfium on every form that writes one.

Two answers therefore differ from the ones a plain measure gets, and neither is written in the corpus: a calculated number with no unit is nought (="5" is not five points, where "5" is), and the unit is matched with its case (="5MM" is nought, where "5MM" is nine tenths of an inch).

What it is worth: 16 522 more fields, and 16 936 was not the prediction
slice 4 slice 5
placed 54 708 71 230
below a height written as =0mm 16 936 0
a place computed and nowhere left to put it 5 794 6 208
under lr-tb 4 303 4 303
anchored by a corner, with no size of its own 9 9

Counted field by field against slice 4's own dispositions, the 16 936 whose first blocker was =0mm reconcile exactly:

what became of a field blocked by =0mm
placed 16 687
taller than a whole content area, and cannot be broken 249

189 fields that slice 4 placed are now reported unplaced, and this is the honest direction. Each sits in a container whose height slice 4 could not compute: the stack placed the child it had reached and stopped, so part of the container went on the sheet. Now the container measures, and it measures taller than a whole content area — so it cannot be placed at all without splitting it across a sheet, which this slice does not do. Losing them is what it looks like when a masked blocker comes out from behind a wall. 128 of them are one form, us-opm__sf144a.

The arithmetic: 16 687 placed + 24 more freed elsewhere − 189 reported = 16 522, and 54 708 + 16 522 = 71 230.

The judge had a fault of its own, and it was the same shape as the last one

Four draws of us-ssa__ss-5-ar-inst are written w="-0.106in" — a negative width — and became reachable for the first time in this slice. The check called all four defects: boxes placed left of the container holding them.

They are not. A negative extent is not a box reaching left of where it was put: pdfium normalises a widget's rectangle before using it — CFX_RectF::Normalize moves the origin by the extent and takes its magnitude (cxfa_fffield.cpp:293, cxfa_ffwidget.cpp:288) — and so does this package. pdf.js does not, because CSS cannot: it emits width:-0.11px, which a browser ignores. The judge was comparing our normalised left edge against pdf.js's unnormalised origin, which is the same box counted two ways. Normalising both sides puts the count back to 0.

It is slice 4's fault in a new place: a comparison at fault announcing itself as a disagreement about the subject.

Splitting, and 4 260 fields that were never a splitting problem

What it is worth: 4 260 more fields, and none of them wanted splitting
slice 5 slice 6
placed 71 230 75 490
taller than a whole content area, and cannot be broken 4 214 0
past the last sheet the page set gives 1 887 1 887
no room inside a container that moves in one piece 107 61
under lr-tb 4 303 4 303
anchored by a corner, with no size of its own 9 9
sheets 2 737 2 864

The disagreement first. "Most of the 6 208 want a container broken across a sheet" is not what they wanted. All 4 214 of the largest bucket are positioned subforms, and a positioned layout fails clause 2 of $isSplittable: no rule anywhere would ever have split one of them. They are whole-page subforms — form1.Page1, topmostSubform.Page5 — six to thirty points taller than the content area they are drawn for, one per printed sheet, which is how LiveCycle writes a multi-page form.

What was refusing them was not the absence of a splitter. It was a fit-check this package applied and pdf.js does not: the first thing on a sheet that moves in one piece is never measured against anything. Because the first whole-page subform was rejected, the flow never turned the page at all, and every later one was rejected against the same sheet. us-irs__fw9 came out on one sheet with 321 fields unplaced, against pdf.js's six sheets.

The reconciliation is exact: 4 214 too-tall placed + 46 freed from noRoomInside (inside the first such container of a sheet, pdf.js checks nothing either) = 4 260, and 71 230 + 4 260 = 75 490. No count went down anywhere this time.

1 887 past the last sheet did not move, which is worth saying: turning more pages did not exhaust any page set that was not exhausted before.

Checked against pdf.js, which CAN see all of this

The judge from slices 2 and 3 is extended rather than replaced, and one thing it did before was wrong.

Boxes are now paired by their whole chain of names, not by their own. Six subforms of us-irs__fw9 are called Bullet1, in three lists on three sheets; while most of them had no height they were never compared, and once they all had one they became six entries of one list with nothing to make the two lists line up. Pairing on the chain turns a mispairing into an unpaired box rather than into a disagreement — and it moved 8 090 unpairable boxes down to 5 133 while moving 2 139 boxes out of the "pdf.js emits a place" column and into the "pdf.js placed it by flexbox" one, where they belong.

container heights
heights this package computes, paired with pdf.js's 7 764
agree to within 1/100 pt 7 764 100.00%
disagree 0 0.00%
more paired but written outright in the template — no check in agreeing 5 289
where the boxes went
boxes where pdf.js emits a real place 176
agree exactly 176 100.00%
boxes pdf.js placed by flexbox 161 820
... at the flow container's own origin, where a first child goes 157 108
... below or to the right of it, where the rest go 4 712
... above or to the left of it, which would be outside it 0 0.00%
which sheet they went on
forms where this package placed every element of the body 473
of those, agreeing with pdf.js on the number of sheets 469 99.2%
boxes paired on those forms 149 433
on the same sheet as pdf.js put them 149 433 100.00%
on another sheet 0 0.00%
forms where fewer elements were placed, and so fewer sheets used 5
... the same number of sheets 5
... MORE sheets, which would be a defect 0 0.00%

The pairing itself was re-audited, not just the rate. Splitting changes which boxes are comparable, so the question is what the judge never gets to compare. Of 156 574 body boxes on the 469 agreeing forms: 149 433 paired, 6 680 unnamed and so never keyed, 461 keyed but absent from pdf.js's dump — 460 of those 461 are presence="hidden", which pdf.js emits as display:none and the dump does not carry — and 0 dropped because the two sides produced a different number of boxes for one key. That last zero is the one that matters: splitting did not create a single new mispairing.

Four forms disagree on the sheet count. Two (us-opm__sf813, us-opm__sf39a) are the same two as before and are not pagination: they have a positioned outermost subform, and pdf.js's checkDimensions position arm (layout.js:355-364) sends children reaching past the bottom of the content area onto a second sheet, which this package does not fit-check at all. Two (us-uscis__i-600a, us-uscis__i-821) are newly visible: they were nowhere near fully placed before — i-600a came out on one sheet against pdf.js's fourteen — and now come out on thirteen. In both, every box agrees up to a <breakBefore targetType="pageArea" startNew="1"/> and is one sheet behind after it. That is a question about breaks, not about splitting, and it is not diagnosed to the line here.

What none of this covers: where inside a container the children ended up (two orderings come to the same total, and no height tells them apart); borders, margins and insets on positioned layouts; anything under a rotated ancestor; where a break inside a container that moves whole would have sent the page; and real per-glyph advances, which nothing here has and which the comparison is therefore blind to in both directions. It also could not run on 77 of the 560 forms — 70 because pdf.js's selectFont dereferences a null typeface when no font is supplied (fonts.js:159), which is precisely the line getMetrics documents as its no-font answer, and 7 for the recursion above.

Lengths

XFA's units are mm, pt, in, cm, pc, mp, em and % — the list Foxit's implementation recognises in pdfium's CXFA_Measurement, which is the implementation closest to Adobe's own. px is not one of them. pdf.js reads it as one point and this package used to read it as 72/96 of one; both were HTML habits carried into a format that does not have the unit. A length in px is now refused rather than guessed at, and the element carrying it is reported unplaced. No x, y, w or h in the corpus is written in it.

A length may also be written as a calculation, with a leading =. That is read by pdfium's rule, which strips the = and parses the rest leniently and without ever failing — see the section above.

Node.Measure answers three ways, not two: the attribute is absent, or it is a length, or it is there and unreadable. XFA's "unspecified" and its "zero" are different things — a field with no width has no width until its text is measured, which is not the same as a field nought wide — and pdf.js loses the distinction in measureToString, which turns any string into "0px".

Getting more sheets when a page set runs out

<occur> on a page area does not bound a form, and reading it as if it did left 1 887 fields on 10 forms off the paper.

A page area's max is a bound on ONE RUN of the set holding it

Restarting a page set offers its page areas again. Both references say so and neither quite carries it out.

pdfium says it in a line: FindPageAreaFromPageSet_Ordered walks a set from its first child and sets cur_page_count_ = 1 on the page area it settles on (cxfa_viewlayoutprocessor.cpp:1240-1244) — and cur_page_count_ is the counter GetNextAvailPageArea tests the max against (:1414-1425). What bounds the whole run is the set's own max, read against page_set_map_ (:1196-1215), which nothing resets. So an uncapped page set over capped page areas yields sheets for ever, and a capped one stops.

pdf.js means the same and cannot reach it. Its $cleanPage (template.js:4160-4167) is exactly that reset, but it sits in the LAST branch of PageSet[$getNextPage] (:4230-4231), below the branch that restarts a usable set (:4222-4227) — and a set with no <occur> is usable for ever ($isUsable, :4169-4174, whose first clause is !this.occur). 559 of the 560 corpus forms write a page set with no <occur> at all. So the restart fires, hands back a page area whose own max is spent, and recurses until the stack is gone. That is the seven-form crash above, and it is this branch.

Cleaning the page areas on the restart is the whole change. The sequence then terminates because it has a page to give, not because a counter stopped it.

An absent max is unbounded, whatever the min says

The second half is one line of occurMax. Occur[$clean] reads as though a written min with no max pins the max to the min (template.js:3925-3932) — but that branch cannot fire on an attribute nobody wrote. The constructor tests attributes.max !== "" (:3896-3903), and a missing attribute is undefined rather than "" (_mkAttributes, parser.js:79-111), so getInteger's default of -1 is taken and $clean's this.max === "" is already false. Only max="" written out reaches it, and nothing in the corpus writes one.

That is pdf.js arriving somewhere by accident, so it is not read from pdf.js. pdfium asks the same question with the default suppressed — TryInteger(XFA_Attribute::Max, /*bUseDefault=*/false) — and takes -1 where the attribute is absent. The two agree on the number by different routes.

It decides one form outright: us-ssa__ssa-3371-bk writes <occur min="1"/> on its only page area. Read as a max of one it gives a single sheet and 127 fields fall off; pdf.js's own dump for it is nine sheets, and this package now puts it on nine.

What it is worth
slice 7 slice 8
placed 79 851 81 738
past the last sheet the page set gives 1 887 0
anchored by a corner, with no size of its own 9 9
no room inside a container that moves in one piece 3 3
sheets 3 040 3 088

Reconciled field by field against main, 81 750 dispositions paired on (form, path, occurrence) with the same multiset of keys: 1 887 placed, 0 lost, and 0 fields placed both times on a different sheet.

Where the reference could see it, and where it could not

Of the 1 887, 1 760 are on forms pdf.js cannot lay out at all — the seven it recurses to death on, plus ca-cra__t2121-fill-24e and -25e, which have the same page set and die of its font defect first. The remaining 127 are us-ssa__ssa-3371-bk, which pdf.js does lay out, and which is therefore the only external check this change has. It is a good one: the form goes from one sheet to nine, its 190 body boxes all pair, and every one lands on the sheet pdf.js put it on.

which sheet they went on slice 7 slice 8
forms where this package placed every element 476 477
of those, agreeing with pdf.js on the number of sheets 472 473
boxes paired on those forms 150 792 150 982
on the same sheet as pdf.js 100.00% 100.00%
forms placing fewer elements and using MORE sheets 2 2

Container heights: 7 794 of 7 794 agree to 1/100 pt, none unmeasurable. Placement: 176 of 176 exact, and 0 of the 163 382 flexbox boxes above or left of their container.

The pairing re-audited, because more sheets means more boxes. Of 162 353 body boxes on the 473 agreeing forms: 150 982 paired, 6 781 unnamed and so never keyed, 4 590 keyed but absent from pdf.js's dump (1 900 presence="hidden"), and 0 dropped because the two sides counted a key differently. The same audit on main gives 6 781, 4 590 and 1 900 — identical — so the newly comparable form contributed 190 boxes and every one of them paired.

One correction that came with it

pdf.js starts a form with pageSetIndex: 0 (template.js:5487) on the page set the first sheet came from, which says a nested page set has been offered when none has. It changed no answer while the restart did not clean, because the restart offered the nested set on its second pass. It does now, so it is -1 here. pdfium looks at the siblings after the spent page area, descending into a nested set as it meets one (cxfa_viewlayoutprocessor.cpp:1444-1447, :1249-1258). No corpus form nests a page set — 560 forms, 560 page sets — so this is measured by one unit test and by nothing else.

What it does not settle

The four sheet-count disagreements are the same four, and none is this. Neither us-opm__sf813 nor us-opm__sf39a (the positioned fit check, layout.js:355-364) nor us-uscis__i-600a nor us-uscis__i-821 (one sheet behind after a breakBefore) writes an <occur> on any page area, so neither half of this change can touch them — checked before assuming it. The two forms using more sheets than the reference, us-uscis__i-956 and i-956g, are likewise unchanged.

Documentation

Overview

Package xfa reads Adobe's XML Forms Architecture — the form description carried inside a PDF whose pages are a placeholder.

Why this exists

A PDF form comes in two kinds, and only one of them is a PDF form. Measured over 2 240 real government forms: 560 carry an XFA package, and 546 of those are STATIC — a second, proprietary description of a form that is already drawn on the pages, which go-pdfkit/forms reads and fills without help.

The other fourteen are DYNAMIC. Their pages hold a panel reading "Please wait... your PDF viewer may not be able to display this type of document", and the form exists only as XML, laid out when the document is opened. Adobe removed the format from PDF 2.0; no browser and no other reader lays one out. A person handed such a file has a document that looks blank and is not.

What it does, and what it will not

This reads the template — the description of the form — into a typed tree, and the datasets, which hold what has been filled in. It does not run the forms' scripts, and that is a measured decision rather than a shortcut: of 5 744 scripts across those fourteen forms, 5 131 are handlers for enter, exit, change and click. They fire when somebody types. Only 188 run at load, so the form a reader first sees does not depend on them.

FormCalc, XFA's other scripting language, is not read either: every script in the corpus declares itself as JavaScript.

Joining a field to its value

Bind does that, and it is not a matter of matching names. A form binds its fields to its data explicitly, through <bind> elements — 26 573 of them across the 560 XFA packages in the corpus — and where it does not, each container consumes the next unclaimed data node of its own name, in document order. The two trees are named differently on purpose: the template names a field for the layout and the data names it for the record.

Taking the fourteen dynamic forms and asking how many template paths the data happens to answer:

cerfa_12064      212 fields, 220 values,  0 paths in common
t657-fill-25e    459 fields, 459 values,  0 paths in common
cerfa_12818       72 fields,  72 values, 47 paths in common
CA-27_sample     136 fields,  99 values, 69 paths in common

The same count of each and not one path in common is what settles it: two of these forms name every field twice over, and only the binding says which goes with which. Guessing by name would answer confidently and wrongly.

Measured over all 560 packages:

560 templates read, 560 with a data tree
26 573 <bind> elements: 11 875 match="none"
                        11 544 match="global"
                         2 589 with no match, which means "once"
                           565 match="dataRef", every one with a ref
80 482 fields placed, 60 865 of them bound to a data node
     0 <bind> expressions holding a construct this does not read

Where a field goes

Expand joins the two trees and returns the form as a document holds it — one node per occurrence, so a table row written once in the template and filled three times by the data is three nodes. Layout runs over that and never over the template, which is what pdf.js's binder does too and for the same reason (bind.js:61).

Place then lays a form out on paper. Under a POSITIONED layout a box's place is its own x and y added to those of every container above it, down from the content area's origin, with anchorType and rotate resolved. Under a FLOW layout the coordinates are thrown away and the children are stacked instead, and this follows two of the six: tb and table stack downwards, each child beginning where the one above it ends, and row cuts its cells from the table's columnWidths. A container's height is the taller of what it holds and what the template writes for it, so the heights are arrived at from the leaves upwards.

A leaf's own height is often not written either, and then it is its TEXT: broken into lines at the width it has, a line count times a line height. Where the text is the XHTML of a rich value it is walked in the order the markup writes it, since half of the corpus's paragraphs hold text both before and after a span.

The fonts are the caller's

A template names typefaces and carries none; the PDF it travelled in carries them, and this package reads neither. So the font set is an argument. PlaceWithFonts takes one, FontSet is how a caller builds it out of Face implementations of its own, and [FontSet.find] is pdf.js's own fuzzy lookup from the name a template writes to a family the caller supplied. No font file is read here and none should be: glyph advances and vertical metrics belong to a font parser, and go-opentype/opentype has both.

Place passes none, and that is a regime of the reference's rather than a shortfall: pdf.js measures a form whose fonts it cannot resolve at one em per character, a first line one em tall and every line after it 1.2 ems, at ten points whatever the template asks for — because the size written beside a typeface nobody has is discarded along with it. Its numbers and these are the same numbers. See the note at the top of text.go.

How much it decides, measured over the 559 forms pdfium lays out, with the 6 960 leaves whose height comes from their text and which pair unambiguously with pdfium's. The question asked is how many LINES the text came to, which pdfium's probe now reports per leaf; comparing heights would not answer it, because pdfium measures with its own font files and no implementation reproduces another's advances in points:

                                no fonts   with fifteen families off one machine
same number of lines as pdfium     23.2%   76.3%
one line more                      35.2%   20.2%
two or more                        41.5%    2.4%
FEWER lines                         0.1%    1.1%
body leaves off the paper           1000      10, of which 9 pdfium puts off too
agreeing on sheet+y               91.29%   92.64%

v0.19.0 published 25.9% and 82.5% in the first row. Those were a PROXY for it — (our height less pdfium's) over OUR line height, bucketed — and the proxy flatters: where the two disagree on the number of lines AND on how tall a line is, the errors can cancel to a height that matches. 1 002 leaves scored as agreeing under the proxy 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, which is 36 pt either way.

Where the line counts DO agree the heights still differ, and the whole of that difference is the FIRST line: on 5 372 of 5 373 such leaves the residue is exactly our first line height less pdfium's, and our line 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 (CXFA_TextParser:: GetLineHeight, its bFirst branch); pdf.js makes it the face's line height less its line gap, which for Arial is 1.1172 ems, and this follows pdf.js. The gap is therefore 0.1172 of the size — 1.172 pt at ten points — and it is a disagreement with pdfium rather than a defect against the reference.

Where the stack runs off the bottom, the page turns. Which page comes next is a state machine rather than "another of the same" — the page set's relation, each page area's occur, the parity of the page number, and the explicit breaks the template writes — and Place follows pdf.js's (template.js:4064-4236, 5418-5657).

A container that MAY be split is broken across the boundary: what it placed before the break stays where it is, and the rest of it begins again at the top of the next content area. Whether it may is a property of the whole CHAIN above it and not of the container alone, which is pdf.js's rule and the reason [placer.splittable] recurses upward. One that may not — a positioned layout, a row, anything with keep intact, anything inside an <area> — 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 claims that pass cannot fail either, nor can anything inside it. Everything after it on that sheet is checked, and what does not fit turns the page — where it is first in its turn. So a container taller than a whole content area comes out one per sheet, hanging over the bottom, which is what pdf.js draws.

A page set says which sheet comes after this one, and its <occur> is what bounds a form: a page AREA's max caps how many sheets it makes in one run of the set holding it, and starting the set again offers it afresh. Both references say so and neither can quite carry it out — see [pager.cleanKids].

WHICH page area each sheet is opened on agrees with pdfium everywhere. Nine of the 559 forms disagree with it on how many sheets there are, and on every one of those nine one side's sequence of page areas is a SUBSEQUENCE of the other's: no sheet of the corpus is opened on a page area pdfium does not choose at that point in the order. Lining the two up by INDEX instead reports ten sheets across three forms as being on the wrong page area, and that number measures the sheet COUNT rather than the choice — a sheet missed in the middle of a run shifts every sheet after it. See [TestSheetsAgainstPdfium], which now asks both.

Everything it does not reach — rl-row, which fills a row from the right, a form that runs out of pages, an element with no room inside a container that moves whole — comes back in Layout.Unplaced with the reason written out, one element at a time. Nothing of the body is dropped. A page area's own furniture is drawn once on every sheet that page area makes, which is the one thing not in one-to-one correspondence with the boxes on the paper.

Measured over the same 560 packages:

81 750 fields in the body of the expanded forms
81 750 placed, 151 323 draws with them, on 3 088 sheets
     0 left over: every field and every draw of the body reaches the paper
    12 elements of the page areas' own furniture are drawn on no sheet,
       because no sheet of the form is that page area

What checks it

pdf.js emits no coordinates for a child of a flow layout — it writes them into a flexbox column and lets the browser stack them — so for exactly the layouts this computes, its output says where the CONTAINER is and nothing about where the second child went. It does emit the accumulation itself, as a number: a subform's style.height is Math.max(extra.height + marginV, this.h || 0) (template.js:5222).

It also emits one div per SHEET, with every element inside the one it belongs to — which is a thing it says outright, so the page a box landed on can be compared even where its coordinates cannot:

  7 764 container heights this package computes, all agreeing with pdf.js's
        to within 1/100 pt
    176 boxes placed where pdf.js also emits a place: all agreeing
152 446 more that pdf.js placed by flexbox, of which 149 488 came out at
        the container's own origin, 2 958 below or to the right of it, and
        NONE above or to the left, which would be outside the container
    431 forms where every element of the body was placed, so that the two
        are laying out the same thing
    428 of those agreeing with pdf.js on the NUMBER of sheets
117 378 boxes paired on them, every one on the same sheet as pdf.js put it,
        and none on another

Where a box goes ACROSS a line

pdf.js says nothing about that either — an lr-tb container's children go in a flexbox div of class xfaLr and a table row's cells in one of class xfaRow, and the browser places them — so it was unjudged in both directions until pdfium was asked. pdfium is a renderer rather than a DOM emitter and computes the answer outright (CalculateRowChildPosition, cxfa_contentlayoutprocessor.cpp:2028-2160), though nothing in public/ returns it and its own suite asserts no coordinate anywhere; the dump comes from a probe added to its embedder tests. It lays out 559 of the 560 forms, against pdf.js's 483.

    959 leaves under a container that wraps its children onto lines, all
        agreeing with pdfium on x
 19 516 leaves under a table row, 19 448 agreeing on x
167 632 leaves under neither — the control — 160 022 agreeing

64 of the 68 row disagreements are on ca-cra__rc1-fill-11-25e, where 552 of 804 CONTROL leaves disagree too, so x on that form is not comparable at all. The other four are on fr-cerfa__cerfa_12818, whose control agrees entirely: the row puts the cell where pdfium does, and pdfium then places the positioned children two levels inside it 3.6 pt further right than their written x. That one is open.

pdfium runs the form's scripts and measures text with real fonts, neither of which this does, so y and the sheet a box landed on are informative rather than a verdict there. X survives: a positioned box's x is its written attribute, and a line member's is arithmetic over written widths.

A box is compared as the same box on both sides. Four draws of us-ssa__ss-5-ar-inst are written w="-0.106in", and a negative extent is not a box reaching left of where it was put: pdfium normalises a widget's rectangle before using it (CFX_RectF::Normalize, cxfa_fffield.cpp:293) and so does this, where pdf.js cannot because CSS ignores a negative width. Until both sides were normalised the check called those four boxes defects.

A box is paired by its whole chain of names and not by its own. Six subforms of us-irs__fw9 are called Bullet1, in three lists on three sheets; once each of them has a height they are six entries of one list, and nothing makes the two lists line up. Pairing on the chain turns a mispairing into an unpaired box rather than into a disagreement.

Text measurement was the wall three counts in a row failed to see. A count over the templates said 446 fields — half of one per cent — need their text measured; only 8 466 draws and 598 fields of the corpus's 234 000 leaves lack a height. But a stack is a chain, and one unmeasurable height leaves every sibling below it with nowhere to begin: those leaves held up 44 246 fields, more than half the corpus. Measuring them places 25 585 more.

A length may be written as a CALCULATION, with a leading "=", and the corpus writes one shape of it: h="=0mm", on 955 draws of 101 forms, holding up 16 936 fields. pdfium's CXFA_Measurement strips the "=" deliberately and parses the rest leniently (cxfa_measurement.cpp, SetString), so it is nought; pdf.js reaches the same answer only because its pattern is unanchored and finds the "0mm" inside the string (utils.js:83-87). An expression is NOT evaluated: ="Foo.h * 2" is nought under the same rule, which is the reference's answer rather than a shortfall standing in for one. See ParseMeasure. It places 16 522 more fields, and reports 189 that were placed before: their containers now measure, and measure taller than a whole content area.

Two things inside the measurement turned out to decide it, and neither is about fonts. The last no-break space of a run becomes an ORDINARY one (parser.js:61-63), which is where "Form\u00a0AB428" comes apart at the end of a column; without it 31 container heights disagreed. And a leaf whose text gives no height is not left unmeasured: computeBbox fills it in from minH (html_utils.js:290-324) and the stack above adds that.

Values and FieldNames remain what they were — what the data says, and what the template says, each on its own — for a caller that wants one side without the other. Values names a repeated sibling the way a binding does, "Row[1]", because a form's table is repeated siblings and naming them alike was losing 10 125 of the corpus's 71 346 values.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FieldNames added in v0.2.0

func FieldNames(template *Node) []string

FieldNames are the paths the TEMPLATE gives its fields, in the order it places them.

The path is the template's, and it is NOT necessarily the path the data uses. XFA binds a field to its data explicitly, through <bind> elements — one real form here carries 222 of them — and a form may name its root subform one thing while its data calls it another. Joining the two by path is right for a form that binds implicitly and wrong for one that does not, which is why this does not try: Bind does the joining properly, and hands back each field with the value actually bound to it.

Fields are looked for inside page areas as well as inside subforms. That is not a nicety: of the 212 fields in one real form, every one sits under a pageArea, and a walk that followed only subforms found none of them.

func Value added in v0.2.0

func Value(data *Node, path string) (string, bool)

Value is one field's value, by its whole name. The second and later of repeated siblings are addressed by index — "form1.Row[1].Cell".

func Values added in v0.2.0

func Values(data *Node) map[string]string

Values are what a form has been filled with, by the whole name of each field: "form1.Page1.Line101".

The names are joined with dots, as XFA's own scripting language names them, and the root of the data tree is included because a package may hold more than one form.

Repeated siblings are numbered the way Bind numbers them and the way a SOM expression addresses them: the first of a name plain, the rest "Row[1]", "Row[2]", counting from zero. Without the numbers they collide, and a table of six rows comes back as one — 10 125 of the 71 346 values the corpus carries, across 216 of its 560 forms, were lost that way before this counted them.

A field a person has not filled in has an empty value rather than no entry. The difference matters: "this field exists and is empty" and "there is no such field" are different answers, and a caller checking whether a form is complete needs the first.

Every value the data carries comes back. There is one entry per leaf and one per group that carries text of its own, and no two share a key.

Types

type Binding added in v0.3.0

type Binding struct {
	// Name is the field's own name, as the template writes it.
	Name string
	// Path is where the FORM puts it: dotted names from the root, with an
	// index — "Row[2]" — on the second and later of repeated siblings. A
	// nameless container contributes nothing to it.
	Path string
	// Kind is "field" or "exclGroup".
	Kind string
	// Node is the template element.
	Node *Node
	// Data is the data element it was bound to, or nil.
	Data *Node
	// DataPath is where that element sits in the DATA, which is a different
	// tree and often a differently named one. Empty when nothing was bound.
	DataPath string
	// Value is what the data says, trimmed. Empty when nothing was bound.
	Value string
	// Bound says a data node was found. A false here with a non-empty form is
	// the ordinary state of a blank document, not an error.
	Bound bool
}

A Binding is one value-carrying container of a form — a field, or the exclusion group a set of radio buttons shares — joined to the data node it draws its value from.

Every such container appears, filled or not. Bound says which: a field the form places but the data never mentions has Bound false and an empty Value, and that is a different answer from a field bound to an empty string. A caller checking whether a form is complete needs the difference.

type Box added in v0.5.0

type Box struct {
	// Node is the occurrence this box was placed for.
	Node *FormNode
	// Kind is "field" or "draw".
	Kind string
	// Path is [FormNode.Path], repeated here so a box can be reported on its
	// own.
	Path string
	// Rect is where it goes, absolute on the page, with anchorType and rotate
	// already resolved.
	Rect
	// Rotate is what the template says, in degrees clockwise: 0, 90, 180 or
	// 270. Rect is the box the rotated element occupies; this says how the
	// content inside it is turned.
	Rotate int
	// Value is what the data put in it, for a bound field. Empty otherwise.
	Value string
	// Hidden says the template asks for it not to be drawn: presence
	// "hidden" or "inactive". pdf.js emits display:none for both
	// (html_utils.js:133-141) and a flow layout gives them no room, so the
	// element after one begins where it does. It is reported rather than left
	// out, because a caller counting what is on a page needs to know that
	// something is there and asked not to be seen.
	Hidden bool
}

A Box is one element of the form, placed.

type Face added in v0.19.0

type Face interface {
	// Advance is how far the pen moves for r, in thousandths of an em.
	//
	// Nought means the face has no advance for it, and
	// [FaceMetrics.DefaultWidth] stands in — which is pdf.js's
	// `glyph.width || fallbackWidth` (text.js:190), a test that cannot tell a
	// missing glyph from one of zero width and does not need to.
	Advance(r rune) float64
	// Metrics are the face's vertical metrics and its fallback advance. It is
	// read once per run of text, so it may be computed rather than stored.
	Metrics() FaceMetrics
}

A Face is one concrete typeface at one weight and one posture: the whole of what a measurement asks of a font.

The unit is the font's, not the page's. pdf.js measures with a PDF font, whose advances are in PDF glyph space — thousandths of an em — and scales them by the size divided by a thousand (text.js:181, 192). A face reading a TrueType file gets there by dividing the glyph's advance by the head table's units per em and multiplying by a thousand.

type FaceMetrics added in v0.19.0

type FaceMetrics struct {
	// LineHeight is how tall a line is, in ems. Nought means the face gives
	// none and 1.2 stands in (text.js:174).
	LineHeight float64
	// LineGap is the space between lines, in ems.
	LineGap float64
	// HasLineGap says the face gives one. A gap of NOUGHT and a gap the face
	// does not give are different: pdf.js tests `lineGap === undefined` and
	// falls back to 0.2 only for the second (text.js:176), so a face that
	// really has no gap must say so with HasLineGap and a LineGap of nought.
	HasLineGap bool
	// DefaultWidth is what stands in for a rune the face gives no advance
	// for, in thousandths of an em. Nought means the face's own space stands
	// in instead: `pdfFont.defaultWidth || pdfFont.charsToGlyphs(" ")[0].width`
	// (text.js:179).
	DefaultWidth float64
}

FaceMetrics is what a face says about itself beyond its advances.

The two vertical numbers are in EMS, and pdf.js says where a real font's come from: the line gap is hhea's, divided by the head table's units per em, and the line height is the ascent less the descent plus that gap, each of them scaled the same way (fonts.js:3005-3017). A face over an AFM metric file, which has no hhea, leaves both unwritten and takes the constants below.

type FontSet added in v0.19.0

type FontSet struct {
	// contains filtered or unexported fields
}

A FontSet is the fonts a form may be measured with, in the order they were added.

The order is not decoration. find's prefix passes push every family whose name starts with what was asked for and then take the FIRST of them (fonts.js:143-149), so which font a template naming "Helvetica" gets is decided by insertion order where a set holds two that would answer. pdf.js iterates a Map, which is insertion ordered; this keeps the order explicitly because Go's map does not have one.

The zero value is not usable; see NewFontSet. A nil *FontSet is, and is what Place measures with: it finds nothing, so every leaf falls to the regime described at [defaultSize].

func NewFontSet added in v0.19.0

func NewFontSet() *FontSet

NewFontSet is an empty set, ready to be filled with FontSet.Add.

func (*FontSet) Add added in v0.19.0

func (s *FontSet) Add(family string, t Typeface)

Add registers a family under the name a template would call it by.

The first family added becomes the set's default, and a family whose regular face the caller did not give takes one from its other three. Adding a family twice replaces it. Add before measuring: it clears find's cache, but a Layout already computed is not recomputed.

type Form added in v0.5.0

type Form struct {
	// Root is the <template> element's node. Under it sit the form's
	// outermost subform and, inside that, the page set.
	Root *FormNode
	// Unsupported are the <bind> expressions that were not followed, as
	// [Bind] reports them.
	Unsupported []Unsupported
	// Truncated says expansion stopped early. See [Result.Truncated].
	Truncated bool
}

A Form is a template and the data that fills it, joined and expanded: the tree layout runs over.

func Expand added in v0.5.0

func Expand(template, data *Node) *Form

Expand joins a template to its data and returns the form tree, with every container repeated once per record that fills it.

Why this exists beside [Bind]

Bind answers "what is in this form", and a flat list of fields answers it well. Layout asks a different question — where does each one go — and the answer is in the shape: a field's place is its own x and y added to those of every container above it, and a container repeated eleven times is in eleven different places. A list of bindings has nowhere to put the eleven.

This follows pdf.js, whose binder does the same thing and for the same reason: Binder.bind returns a clone of the template (bind.js:61) into which _bindOccurrences (bind.js:385-417) inserts one copy of the container per matched data node. Layout there never sees the template either.

Where it goes further than [Bind]

It descends into an exclGroup. A group of radio buttons carries one value between them, so binding stops at the group; but each button is drawn in its own place inside the group's box, so layout needs them. They appear with Bound false, which is what they are: the value belongs to the group.

It carries <draw> — the static text, lines and boxes a form is mostly made of. A draw holds no data, so Bind has nothing to say about it and does not report it. It is half of what is on the paper.

Where it stops, as [Bind] does

It does not clone a container to satisfy occur's initial count when the data is empty, which pdf.js's _createOccurrences (bind.js:421-462) does. Those clones carry no data; they are blank rows on a blank form.

type FormNode added in v0.5.0

type FormNode struct {
	// Template is the element this was made from. Everything the template
	// says that this struct does not carry — x, y, w, h, layout, border,
	// caption — is read from there.
	Template *Node
	// Kind is the element's name: "subform", "field", "draw", "pageArea".
	Kind string
	// Name is what the template calls it, which may be empty.
	Name string
	// Path is where this occurrence sits in the form: dotted names from the
	// root, with an index — "Row[2]" — on the second and later of repeated
	// siblings. It is the same path [Binding] carries, for the containers
	// that carry one.
	Path string
	// Kids are the containers inside it, in the order the form places them.
	Kids []*FormNode
	// Data is the data element this occurrence was bound to, or nil.
	Data *Node
	// Value is what that element says. Empty when nothing was bound.
	Value string
	// Bound says a data node was found for this occurrence.
	Bound bool
}

A FormNode is one container of the expanded form: one subform, one field, one draw, as the document actually holds it rather than as the template writes it.

The difference is repetition. A template writes a table row once and says, in its <occur>, that there may be many; the document has one row per record. Expand does that expansion, so a FormNode tree has one node per occurrence and a template tree has one per element. Two nodes cloned from the same template element share their Template and differ in their Path and their value.

func (*FormNode) Walk added in v0.5.0

func (n *FormNode) Walk(visit func(*FormNode))

Walk calls visit on the node and everything under it, depth first, in the order the form places them.

type Layout added in v0.5.0

type Layout struct {
	// Pages is the sheets the form came to, in order.
	Pages []Page
	// Unplaced is everything it did not place, with a reason each.
	Unplaced []Unplaced
}

A Layout is a form placed on paper.

func Place added in v0.5.0

func Place(form *Form) *Layout

Place lays a form out on paper.

What it does

Under a POSITIONED layout every container has an x and a y that mean what they say, so a box's place on the page is its own x and y added to those of every container above it, down from the content area's origin, with anchorType and rotate resolved on the way (pdf.js layout.js:202-259). That is what pdf.js computes too — under "position" it emits style.left and style.top from the node's own x and y (html_utils.js:112-123).

Under a FLOW layout the coordinates are thrown away (html_utils.js:347-350) and the children are stacked instead. This follows five of the six:

  • tb and table stack downwards. The first child sits at the container's own origin, and each one after it begins where the one above it ends (layout.js:144-158, extra.height += h). A container's own height is the taller of what it holds and what the template writes for it (template.js:5221-5223), so the heights are arrived at from the bottom of the tree upwards, out of the literal w and h of the fields and draws at the leaves.
  • row cuts its cells from the columnWidths of the table above it (html_utils.js:81-106), a cell spanning colSpan of them, and stretches every cell to the height of the tallest (layout.js:135-143).
  • lr-tb and rl-tb fill a line across the page and begin another when what comes next does not fit on it (layout.js:107-129, 279-338). lr-tb runs from the container's left edge and rl-tb from its right, which is the only difference between them (xfa_layer_builder.css:263-273). See [placer.pack].

A container's margin is its own, outside what it holds: it moves the children in by the left and top insets and adds all four to the height the container reports upwards (template.js:5217-5223, layout.js:162-171). A field's and a draw's margin is not — both turn it into padding (template.js:1949-1952, 2917-2920), inside a box the template already sized.

Where it runs off the bottom, it turns the page

A form is longer than a sheet, and the page it goes onto next is not simply "another of the same". Which page area comes next is decided by a state machine — the page set's relation, each page area's <occur>, the parity of the page number, and the explicit <breakBefore> and <breakAfter> the template writes — and [pager] follows pdf.js's (template.js:4064-4236, 5418-5657) rather than assuming. Every container of the chain being flowed begins again at the top of the new content area, which is what pdf.js arrives at by re-entering the whole tree with a new space.

A container that may be split (Subform[$isSplittable], template.js:4940-4975) has its children distributed across the sheets: what it managed to place stays where it is and the rest begins again at the top of the next content area, which is the same thing pdf.js arrives at by resuming its generator. One that may not — a positioned layout, a row, anything kept intact, anything inside an <area>, and anything that is not first on the line of a container that wraps — moves whole, and is reported 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. `<keep intact="none"/>` 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 width, which is the sum of the columnWidths above it rather than anything measured, and no form of the corpus writes one — so nothing would say whether it were right. Only its first child is placed, at the container's origin.
  • Refusing a child of a line for want of vertical room. checkDimensions would move one too tall for what is left onto the next line (layout.js:284-290); the room left is not a quantity the measurement carries. See [placer.pack].
  • A container anchored along its middle or its right edge that writes no width. The height such a container is measured against is computed — see [placer.place] — but the width is a different quantity and no container of the corpus asks for it.
  • Borders. A child's origin is the inside of its parent's margin, not the inside of its parent's border.

Each of those leaves its elements in Layout.Unplaced with the reason written out. Nothing is dropped: every field and draw of the form's body comes back in one list or the other.

A nil form, or one with no outermost subform, lays out nothing.

It measures text with NO fonts, which is a regime of the reference's own and not a stand-in for one: see the note at the top of text.go, and PlaceWithFonts for the other.

func PlaceWithFonts added in v0.19.0

func PlaceWithFonts(form *Form, fonts *FontSet) *Layout

PlaceWithFonts is Place, measuring the form's text with the given fonts.

A template names typefaces and carries none. The document it travelled in carries them, and so does the machine; this package reads neither, which is why the set is an argument. See FontSet for how a caller builds one and [FontSet.find] for how a template's name reaches a family of it.

What it changes is a leaf whose height the template does not write: instead of one em per character and 1.2 ems per line at ten points, its words are measured at the size the template asks for with the advances of the face it asks for. That decides how many LINES the leaf comes to, and a leaf one line taller than it should be pushes every one of its siblings below it down by a line — so the fonts reach the sheet a field lands on, and not only its own box.

A nil set is Place.

func (*Layout) Fields added in v0.5.0

func (l *Layout) Fields() int

Fields counts the placed boxes that are fields rather than static drawing.

type Measure

type Measure float64

A Measure is a length, in points.

XFA writes lengths as a number and a unit — "62mm", "9pt", "0.25in" — and mixes them freely within one form: the corpus's simplest template gives its page in points, its content area's origin in inches and its fields' widths in millimetres, all in the same subform.

func ParseMeasure

func ParseMeasure(s string) (Measure, error)

ParseMeasure reads a length. A bare number is points, which is what the specification says and what the corpus writes for a page's height.

A length may also be written as a CALCULATION, with a leading "=", and that is read by [calculated] rather than here.

func (Measure) Points

func (m Measure) Points() float64

Points converts to the unit a PDF is drawn in.

type Node

type Node struct {
	// Kind is the element's name, without its namespace: "subform", "field",
	// "draw", "pageArea".
	Kind string
	// Attr are its attributes, by name without namespace.
	Attr map[string]string
	// Text is the character data directly inside it, with surrounding space
	// trimmed. A <text> element's caption lives here.
	//
	// Inside a rich text — the XHTML of an <exData contentType="text/html"> —
	// it is empty, and the character data is in "#text" children of [Node.Kids]
	// instead. Rich text is the one place where WHERE the text sits among the
	// elements decides how it reads: half of the corpus's <p> elements hold
	// text both before and after a <span>, and a paragraph's height is how
	// many lines its words come to in the order they are written. See
	// [parseXML].
	Text string
	// Kids are the elements inside it, in order. Order is not decoration: a
	// subform laid out top-to-bottom places its children in it.
	Kids []*Node
}

A Node is one element of a template, with the children it contains.

The tree is kept whole rather than reduced to the elements this package understands today. XFA has some hundreds of element types and a form uses a few dozen; throwing the rest away at parse time would mean re-parsing to add each one, and would lose the ordering that layout depends on.

func ParseDatasets added in v0.2.0

func ParseDatasets(r io.Reader) (*Node, error)

ParseDatasets reads the datasets part of an XFA package — what has been filled in — and returns the data under it.

The tree mirrors the template's names. A form whose root subform is called form1 and whose field is called Number1 keeps its value at <data><form1><Number1>1.00</Number1></form1></data>, which is why the values can be found without laying the form out.

What comes back is the <data> element, not the <datasets> wrapper: the wrapper carries the package's plumbing, and every path into the form starts below it.

func ParseTemplate

func ParseTemplate(r io.Reader) (*Node, error)

ParseTemplate reads the template part of an XFA package.

It is lenient in one direction and strict in the other. Namespaces are dropped, because a template writes the same element under xfa-template and under no namespace at all depending on who produced it, and the difference carries nothing. Processing instructions and comments are skipped: a template written by Adobe's designer carries hundreds of them recording what the designer did, which is not part of the form.

A document whose XML does not close is refused. Half a template is not a form, and laying one out would put half a form on paper without saying so.

func (*Node) Child

func (n *Node) Child(kind string) *Node

Child is the first child of that kind, or nil. Most of the template is singular — a field has one ui, one border, one caption — so looking one up by name is how it is read.

func (*Node) Children

func (n *Node) Children(kind string) []*Node

Children are every child of that kind, in order.

func (*Node) Get

func (n *Node) Get(name string) string

Get returns an attribute, or the empty string.

func (*Node) Measure added in v0.5.0

func (n *Node) Measure(name string) (m Measure, ok bool, err error)

Measure reads one of a node's attributes as a length.

It answers three ways, not two, and the third is the point of it. ok is false when the attribute is not there at all; err is non-nil when it is there and is not a length. XFA's "unspecified" and its "zero" are different things — a field with no width has no width until its text is measured, which is not the same as a field nought wide — and a layout that collapses them draws the second when it meant to report the first.

pdf.js keeps the distinction as the empty string and then loses it in measureToString (html_utils.js:35-41), which turns any string, meant or mistaken, into "0px".

func (*Node) Walk

func (n *Node) Walk(visit func(*Node))

Walk calls visit on the node and everything under it, depth first, in document order.

type Page added in v0.5.0

type Page struct {
	// Width and Height are the sheet, with a landscape medium already
	// swapped. Both are zero when the page area writes no medium, which
	// pdf.js also declines to guess at (template.js:4108).
	Width, Height Measure
	// Areas are the content areas the page offers the body, in order: the
	// boxes the form's body is laid out in, as against the furniture the page
	// area draws around them. Nearly every page area in the wild holds one.
	Areas []Rect
	// Boxes are the elements placed on it, in the order the form places them.
	// The page area's own furniture comes first and is drawn again on every
	// sheet that page area makes; the body follows.
	Boxes []Box
}

A Page is one sheet, with what this package could put on it.

type Rect added in v0.5.0

type Rect struct {
	X, Y, W, H Measure
}

A Rect is a box on the page, in points.

It is measured in XFA's own frame — from the TOP-LEFT corner, with Y increasing downwards — and not in PDF's, which counts up from the bottom left. The flip belongs to whatever draws the page, because it needs the page height to do it and because leaving it here would make every coordinate in this package disagree with the template it was read from.

type Result added in v0.3.0

type Result struct {
	// Fields are the form's value-carrying containers, in the order the form
	// places them.
	Fields []Binding
	// Unsupported are the <bind> expressions that were not followed. Each one
	// leaves its field unbound.
	Unsupported []Unsupported
	// Truncated says binding stopped early because the form produced more
	// containers than this package will hold. It is a defence against a file
	// built to make a reader work forever, not something a real form does.
	Truncated bool
}

A Result is what a form holds once its template and its data are joined.

func Bind added in v0.3.0

func Bind(template, data *Node) *Result

Bind joins a template to the datasets that fill it and returns each of the form's fields with the value bound to it.

How a field finds its value

XFA does not put the value under the field's own name. The template names a field for the layout and the data names it for the record, and the two need not agree — of fourteen dynamic forms measured here, two have the same count of fields and values and not one path in common. What joins them is either an explicit <bind ref="..."> or, far more often, a walk in which each container consumes the next unconsumed data node of its own name. Both are implemented, following Mozilla's pdf.js, which is the only complete free implementation of this.

The <bind> element's match attribute is honoured in all four forms: once (the default), global, dataRef and none.

Which SOM expressions are read

A ref is a SOM expression. These forms are read:

Name.Sub.Leaf      a path from the current data node, walking up to the
                   parent and its parents if the first name is not found
Name[2]            the third sibling of that name
Name[*]            every sibling of that name
$data.Name         rooted at <data>
$record.Name       rooted at the first record under <data>
$.Name             rooted at the current data node
!.data.Name        rooted at <datasets>
Parent..Leaf       Leaf anywhere below Parent
Parent.#field      by element name rather than by name attribute
Name.attr          an attribute reads like a child

These are NOT read, and each one is reported in Result.Unsupported rather than guessed at:

Name.[expr]        a FormCalc subexpression
Name.(expr)        a JavaScript predicate
Name[-1]           a negative index
$template, $form, $layout, $host, $event, $connectionSet, $dataWindow,
$xfa               references outside the data, which name no value

Measured over 560 real templates carrying 26 573 <bind> elements, none of the unsupported forms occurs: every ref is a plain path, a $record path, a $ path, or one of those with [*].

Where this deliberately differs from pdf.js

It does not create data nodes. pdf.js, meeting a ref that matches nothing, invents the nodes the expression describes so that a person typing into the form has somewhere to put the answer. This reads a document rather than editing one, so the field is simply reported unbound — which is the same answer, since an invented node is empty.

It does not repeat a container to satisfy occur's initial count. pdf.js, merging into empty data, clones a subform until there are as many as the template asks for. Those clones carry no data; they change the layout, not the values, and this returns values.

It descends into pageSet and pageArea. pdf.js does not bind their contents at all, because it lays them out separately — but of one real form's 212 fields, every single one sits under a pageArea, and dropping them silently would lose the whole form.

It stops a global search that has begun to repeat. pdf.js's global lookup does not skip nodes it has already taken, so a container with no upper occur bound and match="global" asks for the same node forever; this stops at the repeat.

A nil template gives an empty result; a nil data tree binds nothing, which is what a document nobody has filled in should say.

func (*Result) Values added in v0.3.0

func (r *Result) Values() map[string]string

Values are the bound fields by their form path. Fields that were not bound are left out, so a caller filling a document gets what the data actually says and nothing invented.

A path repeated by two nameless branches keeps the first. Use Result.Fields where that matters; it keeps every one, in order.

type Typeface added in v0.19.0

type Typeface struct {
	// Regular is the upright face of normal weight. Where a family has none,
	// [FontSet.Add] fills it in from whichever of the other three the caller
	// gave — `pdfFont.regular ||= pdfFont.italic || pdfFont.bold ||
	// pdfFont.bolditalic` (fonts.js:32-34) — because everything downstream
	// asks for it by name.
	Regular Face
	// Bold, Italic and BoldItalic are the other three, each of them optional.
	Bold, Italic, BoldItalic Face
	// Name is the regular face's OWN name, as against the family it was filed
	// under: the PostScript name of the file, "ArialMT" for the family
	// "Arial". [FontSet.find]'s third and fifth passes match against it
	// (fonts.js:104-116, 130-141), which is how a template naming
	// "TimesNewRomanPSMT" reaches a family called "Times New Roman". Empty
	// where the caller has no such name, and then those passes simply do not
	// match.
	Name string
}

A Typeface is one family's four faces.

pdf.js builds exactly this shape, keyed by CSS font family, and fills the four properties by reading each PDF font's weight and italic angle (fonts.js:45-73). A family missing a face is not a defect: [selectFace] answers nil for one and the measurement falls back to the default font, the way FontInfo does (text.js:49-51).

type Unplaced added in v0.5.0

type Unplaced struct {
	// Node is the occurrence that was not placed.
	Node *FormNode
	// Kind is "field" or "draw".
	Kind string
	// Path is [FormNode.Path].
	Path string
	// Why says what stopped it, in words fit to show somebody.
	Why string
}

An Unplaced is one element of the form this package did not place, and why.

Every field and draw of the form's BODY is in exactly one of Page.Boxes and Layout.Unplaced. That is the point of the type: a layout that reaches four fifths of a form is useful, and one that silently drops the other fifth is not, because nothing downstream can tell a form that was laid out from a form that was half laid out.

A page area's own furniture is the one exception, and it is not a leak: a letterhead belongs to the sheet rather than to the form, so it is placed once on every sheet that page area makes, and appears in Layout.Unplaced only where its page area is never used at all.

type Unsupported added in v0.3.0

type Unsupported struct {
	// Field is the form path of the container carrying the <bind>.
	Field string
	// Ref is the expression as written.
	Ref string
	// Why names the construct, in words fit to show somebody.
	Why string
}

An Unsupported records a <bind> whose expression this package met and did not follow. It is reported rather than swallowed: a binder that quietly differs from Adobe on one construct is worse than one that says where it stops, because the caller cannot tell a field nobody filled in from a field whose value was not looked for.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL