diff --git a/README.md b/README.md
index e23b4fb..f2ec9bb 100644
--- a/README.md
+++ b/README.md
@@ -134,3 +134,49 @@ Documentation for all of it:
## License
BSD-3-Clause — see [LICENSE](LICENSE). Copyright the go-pdfkit/ops authors.
+
+## Forms
+
+`pdfops fields` lists what a form asks for and what it holds; `pdfops fill`
+fills it in and saves it.
+
+```
+$ pdfops fields fw9.pdf
+note: the file also carries an XFA form, which is not read; the standard one is.
+topmostSubform[0].Page1[0].f1_01[0] text ""
+topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[0] checkbox "Off"
+ buttons [1]
+
+$ pdfops fill -set 'topmostSubform[0].Page1[0].f1_01[0]=Wolfgang Amadeus Mozart' \
+ -set 'topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[0]=1' \
+ fw9.pdf filled.pdf
+```
+
+A filled form is written as an **incremental update**: the original file, byte
+for byte, with the objects that changed appended after it and a new
+cross-reference section pointing back at the old one. That is how every program
+that saves a form saves one, and it is the safest thing a program can do to
+somebody's document — nothing already there is rewritten, so whatever this does
+not understand survives untouched, and if the update is wrong the original is
+still the first part of the file.
+
+The update says where its objects went **the same way the file already does**.
+A file whose cross-reference is a stream cannot be pointed back at by a plain
+table: a reader following `/Prev` would find an object where it expected the
+word `xref`. That is not a nicety — macOS's own renderer draws nothing at all
+for such a file, which is how the mistake was found.
+
+Two things it will not do. A document that had to be **repaired** to be read has
+no cross-reference section worth pointing back at, so it is refused rather than
+added to. A document that is **encrypted** has every string and stream in it
+written through a key, and this does not yet write into one.
+
+Measured on a real form: `fw9.pdf` filled in every field, written out, read
+back with every value in place, and rendered by macOS — which drew what we
+drew, in the same places, comb cells and ticks included.
+
+⚠ **Filling a form is the only verb that keeps one.** Every other verb here
+takes the pages apart and builds a new document round them, and a form is tied
+into a document by object number in a dozen places at once — so merging,
+splitting or rotating a form's pages loses the form. Use `fill` on the file
+itself.
diff --git a/cmd/pdfops/run.go b/cmd/pdfops/run.go
index 5f3dad8..85fc136 100644
--- a/cmd/pdfops/run.go
+++ b/cmd/pdfops/run.go
@@ -58,6 +58,8 @@ var commands = []command{
{"permissions", "", "say how the file is protected and what it allows", runPermissions},
{"text", "[-pages ] [-layout] ", "read the text off the pages", runText},
{"images", "[-pages ] ", "write out the pictures the pages place", runImages},
+ {"fields", "", "list what a form asks for and what it holds", runFields},
+ {"fill", "-set = [-set ...] ", "fill in a form and save it", runFill},
}
// run is the whole program, so that the tests can drive it.
@@ -899,3 +901,107 @@ func (c *context) read(path string) (*reader.Document, error) {
}
return reader.OpenWithPassword(b, c.password)
}
+
+// runFields lists a form's fields: what each is called, what sort of thing it
+// is, and what it holds. A name is what fill takes, so this is how anybody
+// finds out what to type.
+func runFields(c *context, args []string) error {
+ fs := flags("fields")
+ if err := fs.Parse(args); err != nil {
+ return err
+ }
+ if err := wantArgs(fs, 1, ""); err != nil {
+ return err
+ }
+ b, err := os.ReadFile(fs.Arg(0))
+ if err != nil {
+ return err
+ }
+ filling, ok, err := ops.OpenFormWithPassword(b, c.password)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ fmt.Fprintln(c.out, "the file has no form in it")
+ return nil
+ }
+ form := filling.Form()
+ if form.HasXFA() {
+ fmt.Fprintln(c.out, "note: the file also carries an XFA form, which is not read; the standard one is.")
+ }
+ for _, f := range form.Fields() {
+ marks := ""
+ if f.ReadOnly {
+ marks += " read-only"
+ }
+ if f.Required {
+ marks += " required"
+ }
+ if f.MaxLen > 0 {
+ marks += fmt.Sprintf(" max=%d", f.MaxLen)
+ }
+ fmt.Fprintf(c.out, "%-40s %-9s %q%s\n", f.Name, f.Kind, f.Value, marks)
+ for _, o := range f.Options {
+ fmt.Fprintf(c.out, "%-40s row %q\n", "", o.Value)
+ }
+ if len(f.States()) > 0 {
+ fmt.Fprintf(c.out, "%-40s buttons %v\n", "", f.States())
+ }
+ }
+ return nil
+}
+
+// runFill fills a form in and writes the result.
+//
+// The file it writes is the one it read with the changes appended after it,
+// which is how everything that saves a form saves one: nothing already in the
+// file is rewritten, so whatever this does not understand survives.
+func runFill(c *context, args []string) error {
+ fs := flags("fill")
+ var set stringList
+ fs.Var(&set, "set", "a field to fill, as =; may be given more than once")
+ if err := fs.Parse(args); err != nil {
+ return err
+ }
+ if err := wantArgs(fs, 2, " "); err != nil {
+ return err
+ }
+ if len(set) == 0 {
+ return fmt.Errorf("nothing to fill in: give at least one -set =")
+ }
+ b, err := os.ReadFile(fs.Arg(0))
+ if err != nil {
+ return err
+ }
+ filling, ok, err := ops.OpenFormWithPassword(b, c.password)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ return fmt.Errorf("%s has no form in it", fs.Arg(0))
+ }
+ for _, pair := range set {
+ name, value, found := strings.Cut(pair, "=")
+ if !found {
+ return fmt.Errorf("-set wants =, not %q", pair)
+ }
+ if err := filling.Fill(name, value); err != nil {
+ return err
+ }
+ }
+ out, err := filling.Bytes()
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(fs.Arg(1), out, 0o644)
+}
+
+// A stringList is a flag that may be given more than once.
+type stringList []string
+
+func (s *stringList) String() string { return strings.Join(*s, ",") }
+
+func (s *stringList) Set(v string) error {
+ *s = append(*s, v)
+ return nil
+}
diff --git a/cmd/pdfops/run_test.go b/cmd/pdfops/run_test.go
index f9bf5db..9ebbb5d 100644
--- a/cmd/pdfops/run_test.go
+++ b/cmd/pdfops/run_test.go
@@ -846,3 +846,257 @@ func TestEveryWayAPictureIsNamed(t *testing.T) {
}
}
}
+
+// pageWithForm writes a one-page document with a form on it: a box to type in,
+// a box to tick with two buttons, and a list to choose from.
+func pageWithForm(t *testing.T, needAppearances bool) string {
+ t.Helper()
+ w := reader.NewWriter("1.7")
+ pagesRef := w.Reserve()
+ pageRef := w.Reserve()
+ blank := w.Add(&reader.Stream{Dict: reader.Dict{"BBox": reader.Array{
+ reader.Integer(0), reader.Integer(0), reader.Integer(12), reader.Integer(12)}},
+ Raw: []byte("")})
+ text := w.Add(reader.Dict{
+ "FT": reader.Name("Tx"), "T": reader.String("name"),
+ "Type": reader.Name("Annot"), "Subtype": reader.Name("Widget"), "P": pageRef,
+ "Rect": reader.Array{reader.Integer(20), reader.Integer(150),
+ reader.Integer(180), reader.Integer(175)},
+ "MaxLen": reader.Integer(40),
+ "Ff": reader.Integer(1 << 1),
+ })
+ tick := w.Add(reader.Dict{
+ "FT": reader.Name("Btn"), "T": reader.String("agree"),
+ "Type": reader.Name("Annot"), "Subtype": reader.Name("Widget"), "P": pageRef,
+ "Rect": reader.Array{reader.Integer(20), reader.Integer(120),
+ reader.Integer(32), reader.Integer(132)},
+ "AP": reader.Dict{"N": reader.Dict{"Off": blank, "Yes": blank}},
+ })
+ list := w.Add(reader.Dict{
+ "FT": reader.Name("Ch"), "T": reader.String("where"),
+ "Ff": reader.Integer(1 << 17),
+ "Type": reader.Name("Annot"), "Subtype": reader.Name("Widget"), "P": pageRef,
+ "Rect": reader.Array{reader.Integer(20), reader.Integer(80),
+ reader.Integer(180), reader.Integer(100)},
+ "Opt": reader.Array{
+ reader.Array{reader.String("FR"), reader.String("France")},
+ reader.Array{reader.String("BE"), reader.String("Belgique")},
+ },
+ })
+ locked := w.Add(reader.Dict{
+ "FT": reader.Name("Tx"), "T": reader.String("serial"),
+ "Ff": reader.Integer(1), "V": reader.String("A-1756"),
+ "Type": reader.Name("Annot"), "Subtype": reader.Name("Widget"), "P": pageRef,
+ "Rect": reader.Array{reader.Integer(20), reader.Integer(50),
+ reader.Integer(180), reader.Integer(70)},
+ })
+ w.Put(pageRef, reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef,
+ "MediaBox": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(200), reader.Integer(200)},
+ "Annots": reader.Array{text, tick, list, locked},
+ "Contents": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte("")})})
+ w.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"),
+ "Kids": reader.Array{pageRef}, "Count": reader.Integer(1)})
+ form := reader.Dict{
+ "Fields": reader.Array{text, tick, list, locked},
+ "DA": reader.String("/Helv 0 Tf 0 g"),
+ "DR": reader.Dict{"Font": reader.Dict{"Helv": w.Add(reader.Dict{
+ "Type": reader.Name("Font"), "Subtype": reader.Name("Type1"),
+ "BaseFont": reader.Name("Helvetica"),
+ "Encoding": reader.Name("WinAnsiEncoding")})}},
+ }
+ if needAppearances {
+ form["NeedAppearances"] = reader.Bool(true)
+ }
+ root := w.Add(reader.Dict{"Type": reader.Name("Catalog"), "Pages": pagesRef,
+ "AcroForm": w.Add(form)})
+ out, err := w.Finish(reader.Dict{"Root": root})
+ if err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(t.TempDir(), "form.pdf")
+ if err := os.WriteFile(path, out, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ return path
+}
+
+func TestFieldsVerb(t *testing.T) {
+ in := pageWithForm(t, true)
+ code, out, errOut := exec("fields", in)
+ if code != 0 {
+ t.Fatalf("fields said %d: %s", code, errOut)
+ }
+ for _, want := range []string{
+ "name", "text", "agree", "checkbox", "where", "combo",
+ "row \"FR\"", "buttons [Yes]", "read-only", "required", "max=40", "\"A-1756\"",
+ } {
+ if !strings.Contains(out, want) {
+ t.Errorf("the listing does not hold %q:\n%s", want, out)
+ }
+ }
+ for _, args := range [][]string{
+ {"fields"},
+ {"fields", "nowhere.pdf"},
+ {"fields", "-nonsense", in},
+ } {
+ if code, _, _ := exec(args...); code == 0 {
+ t.Errorf("%v was allowed", args)
+ }
+ }
+}
+
+func TestFieldsVerbOnAFileWithNoForm(t *testing.T) {
+ code, out, errOut := exec("fields", pageWithText(t))
+ if code != 0 {
+ t.Fatalf("fields said %d: %s", code, errOut)
+ }
+ if !strings.Contains(out, "no form") {
+ t.Errorf("said %q", out)
+ }
+}
+
+func TestFieldsVerbSaysWhenThereIsAlsoAnXFAForm(t *testing.T) {
+ w := reader.NewWriter("1.7")
+ pagesRef := w.Reserve()
+ field := w.Add(reader.Dict{"FT": reader.Name("Tx"), "T": reader.String("a"),
+ "Subtype": reader.Name("Widget"),
+ "Rect": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(10), reader.Integer(10)}})
+ pageRef := w.Add(reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef,
+ "MediaBox": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(200), reader.Integer(200)},
+ "Annots": reader.Array{field},
+ "Contents": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte("")})})
+ w.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"),
+ "Kids": reader.Array{pageRef}, "Count": reader.Integer(1)})
+ out, err := w.Finish(reader.Dict{"Root": w.Add(reader.Dict{
+ "Type": reader.Name("Catalog"), "Pages": pagesRef,
+ "AcroForm": w.Add(reader.Dict{"Fields": reader.Array{field},
+ "XFA": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte("")})})})})
+ if err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(t.TempDir(), "xfa.pdf")
+ if err := os.WriteFile(path, out, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ _, printed, _ := exec("fields", path)
+ if !strings.Contains(printed, "XFA") {
+ t.Errorf("said nothing about the XFA form:\n%s", printed)
+ }
+}
+
+func TestFillVerb(t *testing.T) {
+ in := pageWithForm(t, false)
+ out := filepath.Join(t.TempDir(), "filled.pdf")
+ code, _, errOut := exec("fill",
+ "-set", "name=Wolfgang Amadeus Mozart",
+ "-set", "agree=yes",
+ "-set", "where=FR",
+ in, out)
+ if code != 0 {
+ t.Fatalf("fill said %d: %s", code, errOut)
+ }
+ _, listed, _ := exec("fields", out)
+ for _, want := range []string{"\"Wolfgang Amadeus Mozart\"", "\"Yes\"", "\"FR\""} {
+ if !strings.Contains(listed, want) {
+ t.Errorf("the filled file does not hold %s:\n%s", want, listed)
+ }
+ }
+}
+
+func TestFillVerbRefusesWhatItCannotDo(t *testing.T) {
+ in := pageWithForm(t, false)
+ out := filepath.Join(t.TempDir(), "filled.pdf")
+ for _, c := range []struct {
+ why string
+ args []string
+ }{
+ {"no arguments at all", []string{"fill"}},
+ {"a flag that does not exist", []string{"fill", "-nonsense", in, out}},
+ {"nothing to set", []string{"fill", in, out}},
+ {"a setting that is not name=value", []string{"fill", "-set", "name", in, out}},
+ {"a field that does not exist", []string{"fill", "-set", "nowhere=x", in, out}},
+ {"a field the document says may not be changed",
+ []string{"fill", "-set", "serial=x", in, out}},
+ {"a file that is not there", []string{"fill", "-set", "name=x", "nowhere.pdf", out}},
+ {"a file with no form", []string{"fill", "-set", "name=x", pageWithText(t), out}},
+ {"somewhere to write that is not writable",
+ []string{"fill", "-set", "name=x", in, filepath.Join(out, "no", "such")}},
+ } {
+ if code, _, _ := exec(c.args...); code == 0 {
+ t.Errorf("%s was allowed", c.why)
+ }
+ }
+}
+
+func TestFillVerbOnAFileThatCannotBeAddedTo(t *testing.T) {
+ // A file the reader had to repair has no cross-reference section worth
+ // pointing back at.
+ in := pageWithForm(t, false)
+ b, err := os.ReadFile(in)
+ if err != nil {
+ t.Fatal(err)
+ }
+ broken := bytes.Replace(b, []byte("startxref"), []byte("startxrEf"), 1)
+ path := filepath.Join(t.TempDir(), "broken.pdf")
+ if err := os.WriteFile(path, broken, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if code, _, _ := exec("fill", "-set", "name=x", path,
+ filepath.Join(t.TempDir(), "out.pdf")); code == 0 {
+ t.Error("a file with no usable table was filled in anyway")
+ }
+ if code, _, _ := exec("fields", path); code == 0 {
+ t.Error("a file with no usable table was listed anyway")
+ }
+}
+
+func TestTheListOfSettings(t *testing.T) {
+ var s stringList
+ if got := s.String(); got != "" {
+ t.Errorf("an empty list says %q", got)
+ }
+ if err := s.Set("a=1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.Set("b=2"); err != nil {
+ t.Fatal(err)
+ }
+ if got := s.String(); got != "a=1,b=2" {
+ t.Errorf("the list says %q", got)
+ }
+}
+
+func TestFillVerbOnAFieldWithNowhereToBeWritten(t *testing.T) {
+ // A field written into the list rather than as an object of its own can
+ // be filled in and cannot be written back, and saying so is better than
+ // writing a file whose value and whose drawing disagree.
+ w := reader.NewWriter("1.7")
+ pagesRef := w.Reserve()
+ pageRef := w.Add(reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef,
+ "MediaBox": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(200), reader.Integer(200)},
+ "Contents": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte("")})})
+ w.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"),
+ "Kids": reader.Array{pageRef}, "Count": reader.Integer(1)})
+ out, err := w.Finish(reader.Dict{"Root": w.Add(reader.Dict{
+ "Type": reader.Name("Catalog"), "Pages": pagesRef,
+ "AcroForm": w.Add(reader.Dict{"DA": reader.String("/Helv 0 Tf 0 g"),
+ "Fields": reader.Array{reader.Dict{
+ "FT": reader.Name("Tx"), "T": reader.String("inline"),
+ "Rect": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(100), reader.Integer(20)}}}})})})
+ if err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(t.TempDir(), "inline.pdf")
+ if err := os.WriteFile(path, out, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if code, _, _ := exec("fill", "-set", "inline=x", path,
+ filepath.Join(t.TempDir(), "out.pdf")); code == 0 {
+ t.Error("a field with nowhere to be written was written anyway")
+ }
+}
diff --git a/fill.go b/fill.go
new file mode 100644
index 0000000..ef0d142
--- /dev/null
+++ b/fill.go
@@ -0,0 +1,485 @@
+package ops
+
+import (
+ "bytes"
+ "fmt"
+ "sort"
+ "strconv"
+
+ "github.com/go-pdfkit/forms"
+ "github.com/go-pdfkit/reader"
+)
+
+// Filling in a form is not like the other verbs. The rest of this package
+// takes pages apart and puts them together again, and what it does not
+// understand it leaves behind. A form cannot be treated that way: it is tied
+// into the document by object number in a dozen places at once — the field
+// tree, the widget annotations on the pages, the resources the appearances
+// name — and a document rebuilt around it would have to rebuild all of that
+// correctly or quietly break it.
+//
+// So a filled form is written as an **incremental update**: the original file,
+// byte for byte, with the objects that changed appended after it and a new
+// cross-reference section pointing back at the old one. That is how every
+// program that saves a form saves one, and it is the safest thing a program
+// can do to somebody's document. Nothing that was already there is rewritten,
+// so everything this package does not understand survives untouched — a
+// signature, an embedded file, a piece of XFA — and if the update is wrong,
+// the original is still the first part of the file and can be recovered.
+
+// A Filling is a document's form, opened so that it can be filled in and
+// written back.
+type Filling struct {
+ src []byte
+ doc *reader.Document
+ form *forms.Form
+ // next is the object number to hand out for anything new.
+ next int
+ // prev is where the file's own cross-reference section begins, which the
+ // update has to point back at. A file that does not say is one the reader
+ // had to repair, and those are refused before this is ever set.
+ prev int
+ // formRef is where the AcroForm dictionary lives, when it lives somewhere
+ // of its own rather than written into the catalogue.
+ formRef reader.Ref
+ // streams are the appearances drawn for this filling, by the number each
+ // was given.
+ streams map[int]reader.Object
+}
+
+// OpenForm reads a document's form. It reports false, with no error, for a
+// document that simply has none — including one carrying an AcroForm
+// dictionary a producer left behind with an empty field list, which 561 of the
+// figure corpus's 118 833 files do.
+func OpenForm(b []byte) (*Filling, bool, error) {
+ d, err := reader.Open(b)
+ if err != nil {
+ return nil, false, err
+ }
+ return openFormIn(b, d)
+}
+
+// OpenFormWithPassword is the same for a document that is protected.
+func OpenFormWithPassword(b []byte, password string) (*Filling, bool, error) {
+ d, err := reader.OpenWithPassword(b, password)
+ if err != nil {
+ return nil, false, err
+ }
+ return openFormIn(b, d)
+}
+
+func openFormIn(b []byte, d *reader.Document) (*Filling, bool, error) {
+ form, ok := forms.Read(d)
+ if !ok {
+ return nil, false, nil
+ }
+ // A document written through a key has every string and every stream in
+ // it written through that key too, so anything appended would have to be
+ // as well. That is not done here, and writing an update in the clear
+ // beside encrypted objects makes a file nothing can read.
+ if d.Encrypted() {
+ return nil, false, fmt.Errorf("ops: the document is encrypted, and this does not yet write into one")
+ }
+ // Where the file says its own cross-reference section begins is what the
+ // update points back at. This is asked before anything else about the
+ // shape of the file, because a file that does not say cannot be added to
+ // at all, whatever else is wrong with it.
+ prev, ok := lastStartxref(b)
+ if !ok {
+ return nil, false, fmt.Errorf("ops: the file does not say where its cross-reference table is, so nothing can be appended to it")
+ }
+ // A file this package has had to repair has no cross-reference section
+ // worth pointing back at, however confidently it says where one is: an
+ // update appended to it would name offsets into a table that was never
+ // right. Such a file is filled in by writing it out whole, which is not
+ // what this does.
+ if d.Repaired() {
+ return nil, false, fmt.Errorf("ops: the file had to be repaired to be read, so it cannot be added to")
+ }
+ f := &Filling{src: b, doc: d, form: form, prev: prev}
+ f.next = f.highestObject() + 1
+ if catalog, err := d.Catalog(); err == nil {
+ if ref, ok := catalog.Get("AcroForm").(reader.Ref); ok {
+ f.formRef = ref
+ }
+ }
+ return f, true, nil
+}
+
+// Form is what was read, to be asked about its fields and told what they hold.
+func (f *Filling) Form() *forms.Form { return f.form }
+
+// Fill sets one field by name, which is what a command line or a map of
+// answers wants.
+func (f *Filling) Fill(name, value string) error { return f.form.Fill(name, value) }
+
+// highestObject is the largest object number the file already uses, so that
+// nothing written now lands on top of something already there. The trailer's
+// own count is believed only when the objects agree with it: a file may say
+// anything, and one that says too little would have this package overwrite
+// what it names.
+func (f *Filling) highestObject() int {
+ high := 0
+ if size, ok := reader.ToInt(f.doc.Trailer().Get("Size")); ok && size > 0 {
+ high = int(size) - 1
+ }
+ // Every object the form knows about is checked against that, since those
+ // are the ones whose numbers this is about to write beside.
+ for _, fld := range f.form.Fields() {
+ if ref, ok := fld.Ref(); ok && ref.Num > high {
+ high = ref.Num
+ }
+ for _, w := range fld.Widgets {
+ if ref, ok := w.Ref(); ok && ref.Num > high {
+ high = ref.Num
+ }
+ }
+ }
+ return high
+}
+
+// Bytes writes the original file with the changes appended to it.
+func (f *Filling) Bytes() ([]byte, error) {
+ changed := f.form.Changed()
+ if len(changed) == 0 {
+ // Nothing was filled in, so the file is what it was. Handing back the
+ // original rather than an update that says nothing is both smaller and
+ // truer.
+ return append([]byte(nil), f.src...), nil
+ }
+
+ written := map[int]reader.Object{}
+ for _, fld := range changed {
+ if err := f.update(fld, written); err != nil {
+ return nil, err
+ }
+ }
+ // The document said its appearances wanted drawing again; they have been.
+ if f.form.NeedAppearances() && f.formRef != (reader.Ref{}) {
+ dict := copyDict(f.form.Dict())
+ delete(dict, "NeedAppearances")
+ written[f.formRef.Num] = dict
+ }
+ return f.append(written)
+}
+
+// update works out what has to be written for one field that was filled in.
+func (f *Filling) update(fld *forms.Field, written map[int]reader.Object) error {
+ ref, ok := fld.Ref()
+ if !ok {
+ return fmt.Errorf("ops: %q is written inside another object and cannot be changed on its own", fld.Name)
+ }
+ dict := dictOf(written, ref, fld.Dict())
+ dict["V"] = f.valueOf(fld)
+
+ switch fld.Kind {
+ case forms.Checkbox, forms.Radio:
+ // A button's picture is already in the file; which of them shows is
+ // what changes. Every widget of the group is told, since only the one
+ // whose own name matches the value is on.
+ for _, w := range fld.Widgets {
+ wRef, ok := w.Ref()
+ if !ok {
+ continue
+ }
+ target := dictOf(written, wRef, w.Dict())
+ if w.On != "" && w.On == fld.Value {
+ target["AS"] = reader.Name(fld.Value)
+ } else {
+ target["AS"] = reader.Name("Off")
+ }
+ }
+ default:
+ // Everything else has to have its picture drawn, because a value is
+ // not what gets drawn and a field filled in without one shows nothing.
+ for _, w := range fld.Widgets {
+ app, ok := fld.Appearance(w)
+ if !ok {
+ continue
+ }
+ wRef, ok := w.Ref()
+ if !ok {
+ continue
+ }
+ stream := f.appearanceStream(app)
+ target := dictOf(written, wRef, w.Dict())
+ target["AP"] = reader.Dict{"N": stream}
+ delete(target, "AS")
+ written[stream.Num] = f.streams[stream.Num]
+ }
+ }
+ return nil
+}
+
+// valueOf is what a field's value looks like written down: a name for a
+// button, since that is what a state is, and text for everything else.
+func (f *Filling) valueOf(fld *forms.Field) reader.Object {
+ switch fld.Kind {
+ case forms.Checkbox, forms.Radio:
+ return reader.Name(fld.Value)
+ case forms.ListBox, forms.ComboBox:
+ if len(fld.Values) > 1 {
+ out := make(reader.Array, 0, len(fld.Values))
+ for _, v := range fld.Values {
+ out = append(out, textString(v))
+ }
+ return out
+ }
+ }
+ return textString(fld.Value)
+}
+
+// appearanceStream writes one drawing out as a new object and gives back its
+// reference.
+func (f *Filling) appearanceStream(app forms.Appearance) reader.Ref {
+ if f.streams == nil {
+ f.streams = map[int]reader.Object{}
+ }
+ resources := reader.Dict{}
+ if app.Font != nil {
+ resources["Font"] = reader.Dict{reader.Name(app.FontName): app.Font}
+ } else {
+ // The document does not carry the font its own field named, so a
+ // standard one is put in under that name: a stream naming a font
+ // nothing can find draws nothing at all.
+ resources["Font"] = reader.Dict{reader.Name(app.FontName): reader.Dict{
+ "Type": reader.Name("Font"), "Subtype": reader.Name("Type1"),
+ "BaseFont": reader.Name("Helvetica"), "Encoding": reader.Name("WinAnsiEncoding"),
+ }}
+ }
+ ref := reader.Ref{Num: f.next}
+ f.next++
+ f.streams[ref.Num] = &reader.Stream{
+ Dict: reader.Dict{
+ "Type": reader.Name("XObject"), "Subtype": reader.Name("Form"),
+ "BBox": boxArray(app.BBox),
+ "Resources": resources,
+ "Length": reader.Integer(len(app.Content)),
+ },
+ Raw: app.Content,
+ }
+ return ref
+}
+
+// dictOf gives the copy of an object being changed, making one the first time
+// it is asked for so that a field with several widgets does not lose the first
+// change when the second is made.
+func dictOf(written map[int]reader.Object, ref reader.Ref, from reader.Dict) reader.Dict {
+ if have, ok := written[ref.Num]; ok {
+ if dict, ok := have.(reader.Dict); ok {
+ return dict
+ }
+ }
+ dict := copyDict(from)
+ written[ref.Num] = dict
+ return dict
+}
+
+// copyDict copies one level, which is all that is changed: everything deeper
+// is left pointing where it pointed.
+func copyDict(d reader.Dict) reader.Dict {
+ out := make(reader.Dict, len(d))
+ for k, v := range d {
+ out[k] = v
+ }
+ return out
+}
+
+// textString writes a value the way a document holds text somebody typed:
+// as bytes where the eight-bit alphabet has them all, and as UTF-16 with a
+// mark at the front where it does not — which is what anything but the
+// plainest English needs.
+func textString(s string) reader.String {
+ simple := true
+ for _, r := range s {
+ if r > 0xFF {
+ simple = false
+ break
+ }
+ }
+ if simple {
+ out := make([]byte, 0, len(s))
+ for _, r := range s {
+ out = append(out, byte(r))
+ }
+ return reader.String(out)
+ }
+ out := []byte{0xFE, 0xFF}
+ for _, r := range s {
+ if r > 0xFFFF {
+ r -= 0x10000
+ hi := 0xD800 + (r >> 10)
+ lo := 0xDC00 + (r & 0x3FF)
+ out = append(out, byte(hi>>8), byte(hi), byte(lo>>8), byte(lo))
+ continue
+ }
+ out = append(out, byte(r>>8), byte(r))
+ }
+ return reader.String(out)
+}
+
+// append writes the original file, then the objects that changed, then a
+// cross-reference section naming where each of them went and pointing back at
+// the one before it.
+func (f *Filling) append(written map[int]reader.Object) ([]byte, error) {
+ out := append([]byte(nil), f.src...)
+ // A file whose last byte is not a line ending would run its last line into
+ // the first object of the update.
+ if len(out) > 0 && out[len(out)-1] != '\n' && out[len(out)-1] != '\r' {
+ out = append(out, '\n')
+ }
+
+ nums := make([]int, 0, len(written))
+ for num := range written {
+ nums = append(nums, num)
+ }
+ sort.Ints(nums)
+
+ offsets := make(map[int]int, len(nums))
+ for _, num := range nums {
+ offsets[num] = len(out)
+ out = append(out, []byte(strconv.Itoa(num)+" 0 obj\n")...)
+ out = reader.AppendObject(out, written[num])
+ out = append(out, []byte("\nendobj\n")...)
+ }
+
+ // The update has to say where its objects went in the same way the file
+ // already says where its own are. A file whose table is a stream cannot
+ // be followed back to by a plain table: a reader going to /Prev would
+ // find an object where it expected the word "xref", and the strict ones
+ // refuse the whole file. That is not a nicety — macOS's own renderer
+ // draws nothing at all for such a file.
+ start := len(out)
+ if xrefIsStream(f.src, f.prev) {
+ return f.appendStreamTable(out, nums, offsets, start)
+ }
+ out = append(out, []byte("xref\n")...)
+ for _, run := range runsOf(nums) {
+ out = append(out, []byte(fmt.Sprintf("%d %d\n", run[0], len(run)))...)
+ for _, num := range run {
+ out = append(out, []byte(fmt.Sprintf("%010d %05d n \n", offsets[num], 0))...)
+ }
+ }
+ out = append(out, []byte("trailer\n")...)
+ out = reader.AppendObject(out, f.updateTrailer())
+ out = append(out, []byte(fmt.Sprintf("\nstartxref\n%d\n%%%%EOF\n", start))...)
+ return out, nil
+}
+
+// updateTrailer is what the update says about the file as a whole: how many
+// objects there now are, where the section before it begins, and the entries
+// that identify the document, which every section has to repeat.
+func (f *Filling) updateTrailer() reader.Dict {
+ trailer := reader.Dict{
+ "Size": reader.Integer(f.next),
+ "Prev": reader.Integer(f.prev),
+ }
+ for _, key := range []reader.Name{"Root", "Info", "ID"} {
+ if v, named := f.doc.Trailer()[key]; named {
+ trailer[key] = v
+ }
+ }
+ return trailer
+}
+
+// xrefIsStream says whether the section at an offset is a cross-reference
+// stream rather than the plain table the older files use.
+func xrefIsStream(b []byte, at int) bool {
+ if at < 0 || at >= len(b) {
+ return false
+ }
+ rest := b[at:]
+ for len(rest) > 0 && (rest[0] == ' ' || rest[0] == '\r' || rest[0] == '\n' || rest[0] == '\t') {
+ rest = rest[1:]
+ }
+ return !bytes.HasPrefix(rest, []byte("xref"))
+}
+
+// The widths of one entry of a cross-reference stream: a byte saying what sort
+// of entry it is, four for where the object begins, and two for its
+// generation. Four bytes reach four thousand megabytes, which is larger than
+// any PDF anybody should be making.
+const (
+ xrefTypeWidth = 1
+ xrefOffsetWidth = 4
+ xrefGenWidth = 2
+)
+
+// appendStreamTable writes the update's own cross-reference as a stream, which
+// is what a file whose table is already one requires.
+func (f *Filling) appendStreamTable(out []byte, nums []int, offsets map[int]int, start int) ([]byte, error) {
+ // The stream is itself an object, so it needs a number and an offset of
+ // its own, and it has to be in its own table.
+ self := f.next
+ f.next++
+ offsets[self] = start
+ nums = append(nums, self)
+ sort.Ints(nums)
+
+ runs := runsOf(nums)
+ index := reader.Array{}
+ var body []byte
+ for _, run := range runs {
+ index = append(index, reader.Integer(run[0]), reader.Integer(len(run)))
+ for _, num := range run {
+ body = append(body, 1)
+ off := offsets[num]
+ body = append(body, byte(off>>24), byte(off>>16), byte(off>>8), byte(off))
+ body = append(body, 0, 0)
+ }
+ }
+ dict := f.updateTrailer()
+ dict["Type"] = reader.Name("XRef")
+ dict["Size"] = reader.Integer(f.next)
+ dict["Index"] = index
+ dict["W"] = reader.Array{reader.Integer(xrefTypeWidth),
+ reader.Integer(xrefOffsetWidth), reader.Integer(xrefGenWidth)}
+ dict["Length"] = reader.Integer(len(body))
+
+ out = append(out, []byte(strconv.Itoa(self)+" 0 obj\n")...)
+ out = reader.AppendObject(out, &reader.Stream{Dict: dict, Raw: body})
+ out = append(out, []byte("\nendobj\n")...)
+ out = append(out, []byte(fmt.Sprintf("startxref\n%d\n%%%%EOF\n", start))...)
+ return out, nil
+}
+
+// runsOf breaks a sorted list of object numbers into the consecutive runs a
+// cross-reference section is written in.
+func runsOf(nums []int) [][]int {
+ var out [][]int
+ for i := 0; i < len(nums); {
+ j := i + 1
+ for j < len(nums) && nums[j] == nums[j-1]+1 {
+ j++
+ }
+ out = append(out, nums[i:j])
+ i = j
+ }
+ return out
+}
+
+// lastStartxref is where the file says its own cross-reference table begins,
+// which the update has to point back at.
+func lastStartxref(b []byte) (int, bool) {
+ i := bytes.LastIndex(b, []byte("startxref"))
+ if i < 0 {
+ return 0, false
+ }
+ rest := b[i+len("startxref"):]
+ j := 0
+ for j < len(rest) && (rest[j] == ' ' || rest[j] == '\r' || rest[j] == '\n' || rest[j] == '\t') {
+ j++
+ }
+ k := j
+ for k < len(rest) && rest[k] >= '0' && rest[k] <= '9' {
+ k++
+ }
+ if k == j {
+ return 0, false
+ }
+ v, err := strconv.Atoi(string(rest[j:k]))
+ if err != nil || v <= 0 || v >= len(b) {
+ return 0, false
+ }
+ return v, true
+}
diff --git a/fill_test.go b/fill_test.go
new file mode 100644
index 0000000..d64e5c8
--- /dev/null
+++ b/fill_test.go
@@ -0,0 +1,605 @@
+package ops
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+
+ "github.com/go-pdfkit/forms"
+ "github.com/go-pdfkit/reader"
+)
+
+// formFile writes a one-page document with a form in it, either with the plain
+// cross-reference table the older files use or with the stream the newer ones
+// do, since an update has to say where its objects went the same way.
+func formFile(t *testing.T, packed bool, build func(w *reader.Writer, page reader.Ref) (reader.Dict, reader.Array)) []byte {
+ t.Helper()
+ w := reader.NewWriter("1.7")
+ if packed {
+ w = reader.NewPackedWriter("1.7")
+ }
+ pagesRef := w.Reserve()
+ pageRef := w.Reserve()
+ form, annots := build(w, pageRef)
+ page := reader.Dict{
+ "Type": reader.Name("Page"), "Parent": pagesRef,
+ "MediaBox": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(200), reader.Integer(200)},
+ "Contents": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte("")}),
+ }
+ if len(annots) > 0 {
+ page["Annots"] = annots
+ }
+ w.Put(pageRef, page)
+ w.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"),
+ "Kids": reader.Array{pageRef}, "Count": reader.Integer(1)})
+ catalog := reader.Dict{"Type": reader.Name("Catalog"), "Pages": pagesRef}
+ if form != nil {
+ catalog["AcroForm"] = w.Add(form)
+ }
+ out, err := w.Finish(reader.Dict{"Root": w.Add(catalog)})
+ if err != nil {
+ t.Fatal(err)
+ }
+ return out
+}
+
+// oneTextField is the simplest form there is: one box to type in.
+func oneTextField(t *testing.T, packed bool, extra reader.Dict) []byte {
+ t.Helper()
+ return formFile(t, packed, func(w *reader.Writer, page reader.Ref) (reader.Dict, reader.Array) {
+ field := reader.Dict{
+ "FT": reader.Name("Tx"), "T": reader.String("name"),
+ "Type": reader.Name("Annot"), "Subtype": reader.Name("Widget"),
+ "Rect": reader.Array{reader.Integer(20), reader.Integer(100),
+ reader.Integer(180), reader.Integer(130)},
+ "P": page,
+ }
+ for k, v := range extra {
+ field[k] = v
+ }
+ ref := w.Add(field)
+ return reader.Dict{
+ "Fields": reader.Array{ref},
+ "DA": reader.String("/Helv 0 Tf 0 g"),
+ "DR": reader.Dict{"Font": reader.Dict{"Helv": w.Add(reader.Dict{
+ "Type": reader.Name("Font"), "Subtype": reader.Name("Type1"),
+ "BaseFont": reader.Name("Helvetica"), "Encoding": reader.Name("WinAnsiEncoding")})}},
+ }, reader.Array{ref}
+ })
+}
+
+// refill fills a field, writes the file out, and reads it back — which is the
+// only test that means anything: a value written and not readable again is a
+// value that was not written.
+func refill(t *testing.T, src []byte, name, value string) *forms.Form {
+ t.Helper()
+ f, ok, err := OpenForm(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("the document was written with a form and opened without one")
+ }
+ if err := f.Fill(name, value); err != nil {
+ t.Fatal(err)
+ }
+ out, err := f.Bytes()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.HasPrefix(out, src) {
+ t.Fatal("the original file is not the beginning of what was written")
+ }
+ d, err := reader.Open(out)
+ if err != nil {
+ t.Fatalf("what was written could not be read: %v", err)
+ }
+ if d.Repaired() {
+ t.Fatal("what was written had to be repaired to be read")
+ }
+ back, ok := forms.Read(d)
+ if !ok {
+ t.Fatal("what was written has no form in it")
+ }
+ return back
+}
+
+func TestAValueSurvivesBeingWrittenOut(t *testing.T) {
+ // Both ways a file says where its objects are, since an update has to
+ // match: a plain table cannot be pointed back at by one written the other
+ // way, and the strict readers refuse the whole file when it is.
+ for _, packed := range []bool{false, true} {
+ back := refill(t, oneTextField(t, packed, nil), "name", "Mozart")
+ fld, ok := back.Field("name")
+ if !ok {
+ t.Fatal("the field is gone")
+ }
+ if fld.Value != "Mozart" {
+ t.Errorf("packed %v: holds %q", packed, fld.Value)
+ }
+ if _, has := back.Dict()["NeedAppearances"]; has {
+ t.Error("the form still asks for its appearances to be drawn")
+ }
+ w := fld.Widgets[0]
+ if _, drawn := reader.ToDict(w.Dict().Get("AP")); !drawn {
+ t.Error("the field was filled in and nothing was drawn for it")
+ }
+ }
+}
+
+func TestTheDrawingBesideAValueIsWhatShows(t *testing.T) {
+ // A value is not what gets drawn. What is written beside it has to hold
+ // the value, in a stream naming a font the document carries.
+ f, _, err := OpenForm(oneTextField(t, false, nil))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := f.Fill("name", "Mozart"); err != nil {
+ t.Fatal(err)
+ }
+ out, err := f.Bytes()
+ if err != nil {
+ t.Fatal(err)
+ }
+ d, _ := reader.Open(out)
+ back, _ := forms.Read(d)
+ fld, _ := back.Field("name")
+ ap, _ := d.GetDict(fld.Widgets[0].Dict(), "AP")
+ stream, ok := reader.ToStream(mustResolve(t, d, ap.Get("N")))
+ if !ok {
+ t.Fatal("what was drawn is not a stream")
+ }
+ body, _, err := d.DecodeStream(stream)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(body), "(Mozart) Tj") {
+ t.Errorf("the drawing does not hold the value:\n%s", body)
+ }
+ res, ok := d.GetDict(stream.Dict, "Resources")
+ if !ok {
+ t.Fatal("the drawing names no resources")
+ }
+ fonts, ok := d.GetDict(res, "Font")
+ if !ok || len(fonts) == 0 {
+ t.Error("the drawing names no font, so nothing would show")
+ }
+}
+
+func mustResolve(t *testing.T, d *reader.Document, o reader.Object) reader.Object {
+ t.Helper()
+ v, err := d.Resolve(o)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return v
+}
+
+func TestTickingABoxSaysWhichPictureShows(t *testing.T) {
+ // A button's pictures are already in the file; which of them shows is
+ // what changes, and every widget of a group is told, since only the one
+ // whose own name matches the value is on.
+ src := formFile(t, false, func(w *reader.Writer, page reader.Ref) (reader.Dict, reader.Array) {
+ blank := w.Add(&reader.Stream{Dict: reader.Dict{"BBox": reader.Array{
+ reader.Integer(0), reader.Integer(0), reader.Integer(12), reader.Integer(12)}},
+ Raw: []byte("")})
+ one := w.Add(reader.Dict{"Subtype": reader.Name("Widget"), "P": page,
+ "Rect": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(12), reader.Integer(12)},
+ "AP": reader.Dict{"N": reader.Dict{"Off": blank, "Zurich": blank}}})
+ two := w.Add(reader.Dict{"Subtype": reader.Name("Widget"), "P": page,
+ "Rect": reader.Array{reader.Integer(20), reader.Integer(0),
+ reader.Integer(32), reader.Integer(12)},
+ "AP": reader.Dict{"N": reader.Dict{"Off": blank, "Anvers": blank}}})
+ field := w.Add(reader.Dict{"T": reader.String("city"), "FT": reader.Name("Btn"),
+ "Ff": reader.Integer(1 << 15), "Kids": reader.Array{one, two}})
+ return reader.Dict{"Fields": reader.Array{field}}, reader.Array{one, two}
+ })
+ back := refill(t, src, "city", "Anvers")
+ fld, _ := back.Field("city")
+ if fld.Value != "Anvers" {
+ t.Fatalf("the field holds %q", fld.Value)
+ }
+ var states []string
+ for _, w := range fld.Widgets {
+ s, _ := reader.ToName(w.Dict().Get("AS"))
+ states = append(states, string(s))
+ }
+ if len(states) != 2 || states[0] != "Off" || states[1] != "Anvers" {
+ t.Errorf("the buttons are showing %v, wanted the second one only", states)
+ }
+}
+
+func TestAFormThatWasNotTouchedIsHandedBackAsItWas(t *testing.T) {
+ src := oneTextField(t, false, nil)
+ f, ok, err := OpenForm(src)
+ if err != nil || !ok {
+ t.Fatal(err)
+ }
+ out, err := f.Bytes()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(out, src) {
+ t.Errorf("a file nobody filled in came back %d bytes instead of %d", len(out), len(src))
+ }
+}
+
+func TestAValueThatIsNotPlainEnglish(t *testing.T) {
+ // Anything the eight-bit alphabet has no room for is written the other
+ // way, with a mark at the front, and has to come back the same.
+ for _, value := range []string{"Dvořák", "Ω", "😀 Mozart"} {
+ back := refill(t, oneTextField(t, false, nil), "name", value)
+ fld, _ := back.Field("name")
+ if fld.Value != value {
+ t.Errorf("%q came back as %q", value, fld.Value)
+ }
+ }
+}
+
+func TestAFormThatAsksForItsDrawingsToBeMadeAgain(t *testing.T) {
+ src := formFile(t, false, func(w *reader.Writer, page reader.Ref) (reader.Dict, reader.Array) {
+ ref := w.Add(reader.Dict{"FT": reader.Name("Tx"), "T": reader.String("name"),
+ "Subtype": reader.Name("Widget"), "P": page,
+ "Rect": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(100), reader.Integer(20)}})
+ return reader.Dict{"Fields": reader.Array{ref},
+ "NeedAppearances": reader.Bool(true),
+ "DA": reader.String("/Helv 0 Tf 0 g")}, reader.Array{ref}
+ })
+ back := refill(t, src, "name", "Mozart")
+ if back.NeedAppearances() {
+ t.Error("the drawings were made and the file still asks for them")
+ }
+}
+
+func TestADocumentWithNoFormToFill(t *testing.T) {
+ src := formFile(t, false, func(w *reader.Writer, page reader.Ref) (reader.Dict, reader.Array) {
+ return nil, nil
+ })
+ if _, ok, err := OpenForm(src); ok || err != nil {
+ t.Errorf("ok %v err %v", ok, err)
+ }
+}
+
+func TestSomethingThatIsNotADocumentAtAll(t *testing.T) {
+ if _, _, err := OpenForm([]byte("not a PDF")); err == nil {
+ t.Error("nonsense was opened as a document")
+ }
+ if _, _, err := OpenFormWithPassword([]byte("not a PDF"), "x"); err == nil {
+ t.Error("nonsense was opened as a protected document")
+ }
+}
+
+func TestAProtectedDocumentIsOpenedWithItsPassword(t *testing.T) {
+ src := oneTextField(t, false, nil)
+ doc, err := Open(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _ = doc
+ if _, _, err := OpenFormWithPassword(src, ""); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestBreakingTheRunsOfObjectNumbers(t *testing.T) {
+ for _, c := range []struct {
+ in []int
+ runs int
+ }{
+ {nil, 0},
+ {[]int{1}, 1},
+ {[]int{1, 2, 3}, 1},
+ {[]int{1, 3}, 2},
+ {[]int{1, 2, 5, 6, 9}, 3},
+ } {
+ if got := len(runsOf(c.in)); got != c.runs {
+ t.Errorf("%v broke into %d runs, wanted %d", c.in, got, c.runs)
+ }
+ }
+}
+
+func TestFindingWhereTheTableBegins(t *testing.T) {
+ for _, c := range []struct {
+ why string
+ in string
+ want int
+ ok bool
+ }{
+ {"the usual", "%PDF-1.7\nxref\ntrailer\nstartxref\n9\n%%EOF", 9, true},
+ {"nothing saying so", "%PDF-1.7\n", 0, false},
+ {"saying so and then not", "%PDF-1.7\nstartxref\n", 0, false},
+ {"a number past the end of the file", "startxref\n99999\n", 0, false},
+ {"nought, which is nowhere", "%PDF-1.7\nstartxref\n0\n", 0, false},
+ } {
+ got, ok := lastStartxref([]byte(c.in))
+ if ok != c.ok || (ok && got != c.want) {
+ t.Errorf("%s: read %d %v, wanted %d %v", c.why, got, ok, c.want, c.ok)
+ }
+ }
+}
+
+func TestTellingOneSortOfTableFromTheOther(t *testing.T) {
+ if xrefIsStream([]byte("xref\n0 1\n"), 0) {
+ t.Error("a plain table was taken for a stream")
+ }
+ if xrefIsStream([]byte(" \r\nxref\n"), 0) {
+ t.Error("a plain table with space before it was taken for a stream")
+ }
+ if !xrefIsStream([]byte("12 0 obj <>"), 0) {
+ t.Error("a stream was taken for a plain table")
+ }
+ if xrefIsStream([]byte("xref"), 99) {
+ t.Error("an offset past the end of the file said something")
+ }
+}
+
+func TestAFileTheReaderHadToRepairIsNotAddedTo(t *testing.T) {
+ // An update points back at the file's own cross-reference section by
+ // where it begins. A file whose section was wrong enough to be rebuilt
+ // has no such place worth pointing at, and an update naming offsets into
+ // a table that was never right is a file nothing can read.
+ src := oneTextField(t, false, nil)
+ broken := bytes.Replace(src, []byte("startxref"), []byte("startxrEf"), 1)
+ if _, _, err := OpenForm(broken); err == nil {
+ t.Error("a file with no usable table was opened for adding to")
+ }
+
+ i := bytes.LastIndex(src, []byte("startxref"))
+ moved := append([]byte(nil), src[:i]...)
+ moved = append(moved, []byte("startxref\n3\n%%EOF\n")...)
+ if _, _, err := OpenForm(moved); err == nil {
+ t.Error("a file whose table is not where it says was opened for adding to")
+ }
+}
+
+func TestAnEncryptedDocumentIsNotAddedTo(t *testing.T) {
+ // Everything in such a file is written through a key, so anything
+ // appended would have to be too. Writing in the clear beside it makes a
+ // file nothing can read, which is worse than refusing.
+ w := reader.NewWriter("1.7")
+ w.Encrypt(reader.Encryption{OwnerPassword: "secret"})
+ pagesRef := w.Reserve()
+ pageRef := w.Reserve()
+ field := w.Add(reader.Dict{"FT": reader.Name("Tx"), "T": reader.String("name"),
+ "Subtype": reader.Name("Widget"), "P": pageRef,
+ "Rect": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(100), reader.Integer(20)}})
+ w.Put(pageRef, reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef,
+ "MediaBox": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(200), reader.Integer(200)},
+ "Annots": reader.Array{field},
+ "Contents": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte("")})})
+ w.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"),
+ "Kids": reader.Array{pageRef}, "Count": reader.Integer(1)})
+ locked, err := w.Finish(reader.Dict{"Root": w.Add(reader.Dict{
+ "Type": reader.Name("Catalog"), "Pages": pagesRef,
+ "AcroForm": w.Add(reader.Dict{"Fields": reader.Array{field},
+ "DA": reader.String("/Helv 0 Tf 0 g")})})})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, ok, err := OpenFormWithPassword(locked, ""); ok || err == nil {
+ t.Errorf("an encrypted document was opened for adding to: ok %v err %v", ok, err)
+ }
+}
+
+func TestTheFormIsWhatIsAskedAboutAndTold(t *testing.T) {
+ f, ok, err := OpenForm(oneTextField(t, false, nil))
+ if err != nil || !ok {
+ t.Fatal(err)
+ }
+ if got := len(f.Form().Fields()); got != 1 {
+ t.Errorf("the form has %d fields", got)
+ }
+}
+
+func TestAFileWhoseCountOfObjectsIsTooSmall(t *testing.T) {
+ // A file may say anything about how many objects it holds. Believing a
+ // count smaller than the objects that are there would have this write on
+ // top of something already in the file.
+ src := oneTextField(t, false, nil)
+ f, ok, err := OpenForm(src)
+ if err != nil || !ok {
+ t.Fatal(err)
+ }
+ // What the trailer says is believed only when the objects agree with it.
+ // A file claiming to hold two objects while its field is object nine must
+ // not have this write a tenth on top of something.
+ f.doc.Trailer()["Size"] = reader.Integer(2)
+ if got := f.highestObject(); got < 2 {
+ t.Errorf("the highest object is %d, and the form's own field is above that", got)
+ }
+ ref, _ := f.form.Fields()[0].Ref()
+ if got := f.highestObject(); got < ref.Num {
+ t.Errorf("the highest object is %d and the field is object %d", got, ref.Num)
+ }
+}
+
+func TestAFormWrittenIntoTheCatalogueRatherThanBeside(t *testing.T) {
+ // There is then nowhere of its own to write the form back to, so what it
+ // asked for cannot be unasked; the fields are still filled in.
+ w := reader.NewWriter("1.7")
+ pagesRef := w.Reserve()
+ field := w.Add(reader.Dict{"FT": reader.Name("Tx"), "T": reader.String("name"),
+ "Subtype": reader.Name("Widget"),
+ "Rect": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(100), reader.Integer(20)}})
+ pageRef := w.Add(reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef,
+ "MediaBox": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(200), reader.Integer(200)},
+ "Annots": reader.Array{field},
+ "Contents": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte("")})})
+ w.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"),
+ "Kids": reader.Array{pageRef}, "Count": reader.Integer(1)})
+ src, err := w.Finish(reader.Dict{"Root": w.Add(reader.Dict{
+ "Type": reader.Name("Catalog"), "Pages": pagesRef,
+ "AcroForm": reader.Dict{"Fields": reader.Array{field},
+ "NeedAppearances": reader.Bool(true),
+ "DA": reader.String("/Helv 0 Tf 0 g")}})})
+ if err != nil {
+ t.Fatal(err)
+ }
+ back := refill(t, src, "name", "Mozart")
+ fld, _ := back.Field("name")
+ if fld.Value != "Mozart" {
+ t.Errorf("holds %q", fld.Value)
+ }
+}
+
+func TestAFieldWrittenInsideAnotherObjectCannotBeChanged(t *testing.T) {
+ // There is nowhere to write it back to. Saying so is better than writing
+ // a file whose value and whose drawing disagree.
+ w := reader.NewWriter("1.7")
+ pagesRef := w.Reserve()
+ pageRef := w.Add(reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef,
+ "MediaBox": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(200), reader.Integer(200)},
+ "Contents": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte("")})})
+ w.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"),
+ "Kids": reader.Array{pageRef}, "Count": reader.Integer(1)})
+ src, err := w.Finish(reader.Dict{"Root": w.Add(reader.Dict{
+ "Type": reader.Name("Catalog"), "Pages": pagesRef,
+ "AcroForm": w.Add(reader.Dict{"DA": reader.String("/Helv 0 Tf 0 g"),
+ "Fields": reader.Array{reader.Dict{
+ "FT": reader.Name("Tx"), "T": reader.String("inline"),
+ "Rect": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(100), reader.Integer(20)}}}})})})
+ if err != nil {
+ t.Fatal(err)
+ }
+ f, ok, err := OpenForm(src)
+ if err != nil || !ok {
+ t.Fatalf("ok %v err %v", ok, err)
+ }
+ if err := f.Fill("inline", "Mozart"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.Bytes(); err == nil {
+ t.Error("a field with nowhere to be written was written anyway")
+ }
+}
+
+func TestAWidgetWrittenInsideAnotherObject(t *testing.T) {
+ // The field can be changed; the widget cannot, so nothing is drawn for
+ // that one and the rest of the file is still written.
+ src := formFile(t, false, func(w *reader.Writer, page reader.Ref) (reader.Dict, reader.Array) {
+ field := w.Add(reader.Dict{"T": reader.String("name"), "FT": reader.Name("Tx"),
+ "Kids": reader.Array{reader.Dict{"Subtype": reader.Name("Widget"),
+ "Rect": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(100), reader.Integer(20)}}}})
+ return reader.Dict{"Fields": reader.Array{field},
+ "DA": reader.String("/Helv 0 Tf 0 g")}, nil
+ })
+ back := refill(t, src, "name", "Mozart")
+ fld, _ := back.Field("name")
+ if fld.Value != "Mozart" {
+ t.Errorf("holds %q", fld.Value)
+ }
+}
+
+func TestAButtonWidgetWrittenInsideAnotherObject(t *testing.T) {
+ src := formFile(t, false, func(w *reader.Writer, page reader.Ref) (reader.Dict, reader.Array) {
+ field := w.Add(reader.Dict{"T": reader.String("tick"), "FT": reader.Name("Btn"),
+ "Kids": reader.Array{reader.Dict{"Subtype": reader.Name("Widget"),
+ "Rect": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(12), reader.Integer(12)}}}})
+ return reader.Dict{"Fields": reader.Array{field}}, nil
+ })
+ back := refill(t, src, "tick", "yes")
+ fld, _ := back.Field("tick")
+ if fld.Value != "Yes" {
+ t.Errorf("holds %q", fld.Value)
+ }
+}
+
+func TestAFieldOfNoSizeHasNothingDrawnForIt(t *testing.T) {
+ src := oneTextField(t, false, reader.Dict{
+ "Rect": reader.Array{reader.Integer(10), reader.Integer(10),
+ reader.Integer(10), reader.Integer(40)}})
+ back := refill(t, src, "name", "Mozart")
+ fld, _ := back.Field("name")
+ if fld.Value != "Mozart" {
+ t.Errorf("holds %q", fld.Value)
+ }
+}
+
+func TestAListBoxHoldingSeveralRows(t *testing.T) {
+ src := formFile(t, false, func(w *reader.Writer, page reader.Ref) (reader.Dict, reader.Array) {
+ ref := w.Add(reader.Dict{"FT": reader.Name("Ch"), "T": reader.String("where"),
+ "Ff": reader.Integer(1 << 21),
+ "Subtype": reader.Name("Widget"), "P": page,
+ "Rect": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(100), reader.Integer(40)},
+ "Opt": reader.Array{reader.String("FR"), reader.String("BE")}})
+ return reader.Dict{"Fields": reader.Array{ref},
+ "DA": reader.String("/Helv 9 Tf 0 g")}, reader.Array{ref}
+ })
+ f, ok, err := OpenForm(src)
+ if err != nil || !ok {
+ t.Fatal(err)
+ }
+ fld, _ := f.Form().Field("where")
+ if err := fld.Choose("FR", "BE"); err != nil {
+ t.Fatal(err)
+ }
+ out, err := f.Bytes()
+ if err != nil {
+ t.Fatal(err)
+ }
+ d, err := reader.Open(out)
+ if err != nil {
+ t.Fatal(err)
+ }
+ back, _ := forms.Read(d)
+ got, _ := back.Field("where")
+ if len(got.Values) != 2 {
+ t.Errorf("came back holding %v", got.Values)
+ }
+}
+
+func TestAFileWhoseLastLineDoesNotEnd(t *testing.T) {
+ // Its last line would otherwise run into the first object of the update.
+ src := oneTextField(t, false, nil)
+ trimmed := bytes.TrimRight(src, "\r\n")
+ back := refill(t, trimmed, "name", "Mozart")
+ fld, _ := back.Field("name")
+ if fld.Value != "Mozart" {
+ t.Errorf("holds %q", fld.Value)
+ }
+}
+
+func TestAWidgetNumberedAboveItsField(t *testing.T) {
+ // A field is not always written before the widgets that belong to it, and
+ // what the trailer says about how many objects there are is not always
+ // true. Both have to be looked at, or this writes a new object on top of
+ // one already in the file.
+ src := formFile(t, false, func(w *reader.Writer, page reader.Ref) (reader.Dict, reader.Array) {
+ fieldRef := w.Reserve()
+ one := w.Add(reader.Dict{"Subtype": reader.Name("Widget"), "P": page,
+ "Parent": fieldRef,
+ "Rect": reader.Array{reader.Integer(0), reader.Integer(0),
+ reader.Integer(100), reader.Integer(20)}})
+ w.Put(fieldRef, reader.Dict{"T": reader.String("name"), "FT": reader.Name("Tx"),
+ "Kids": reader.Array{one}})
+ return reader.Dict{"Fields": reader.Array{fieldRef},
+ "DA": reader.String("/Helv 0 Tf 0 g")}, reader.Array{one}
+ })
+ f, ok, err := OpenForm(src)
+ if err != nil || !ok {
+ t.Fatal(err)
+ }
+ f.doc.Trailer()["Size"] = reader.Integer(2)
+ fld := f.form.Fields()[0]
+ fieldRef, _ := fld.Ref()
+ widgetRef, _ := fld.Widgets[0].Ref()
+ if widgetRef.Num <= fieldRef.Num {
+ t.Skipf("the widget is object %d and the field %d, which is not the case this is about",
+ widgetRef.Num, fieldRef.Num)
+ }
+ if got := f.highestObject(); got < widgetRef.Num {
+ t.Errorf("the highest object is %d and the widget is object %d", got, widgetRef.Num)
+ }
+}
diff --git a/go.mod b/go.mod
index 2719c57..23a4e51 100644
--- a/go.mod
+++ b/go.mod
@@ -4,10 +4,12 @@ go 1.26.4
require (
github.com/go-pdfkit/extract v0.1.0
- github.com/go-pdfkit/reader v0.4.0
+ github.com/go-pdfkit/reader v0.4.1
)
require (
+ github.com/go-opentype/fonts v0.9.0 // indirect
github.com/go-opentype/opentype v0.9.0 // indirect
- github.com/go-pdfkit/pdffont v0.1.0 // indirect
+ github.com/go-pdfkit/forms v0.2.1 // indirect
+ github.com/go-pdfkit/pdffont v0.2.0 // indirect
)
diff --git a/go.sum b/go.sum
index 0244ea4..544b44a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,10 +1,20 @@
github.com/go-opentype/fonts v0.8.0 h1:77i3VPIH90GbstzNb21mk+an4WvEOe2idC6W+J0n0fw=
github.com/go-opentype/fonts v0.8.0/go.mod h1:C6yQL2apHItfEZ5hztpsHF0S5mlX/hklLlq/Z5fRG/g=
+github.com/go-opentype/fonts v0.9.0 h1:slB6OB3riLyUPrOxqXe0s6/AzdenF1TDvCN8N87hhQk=
+github.com/go-opentype/fonts v0.9.0/go.mod h1:C6yQL2apHItfEZ5hztpsHF0S5mlX/hklLlq/Z5fRG/g=
github.com/go-opentype/opentype v0.9.0 h1:GFgcJ3nwTDp4NJr5O+Paw7lhZx5Jv/R+noZwvhYDlkM=
github.com/go-opentype/opentype v0.9.0/go.mod h1:AOixevJf7XQaH7+WG+OMIOZEbYPXfMqklVk26Y6YTUU=
github.com/go-pdfkit/extract v0.1.0 h1:7IJDJMH43l2wJZD+DkW5StmeAS1NDt2FnT2LBceXTog=
github.com/go-pdfkit/extract v0.1.0/go.mod h1:3ejdi87IrdR60wmjzLswdqzBzHCgFlrLtxkMxRN1KSU=
+github.com/go-pdfkit/forms v0.2.0 h1:qcphzViGdL3vDb66rnyQkwzPw+una5rsF4H84KB2XMI=
+github.com/go-pdfkit/forms v0.2.0/go.mod h1:LORxkdP4FVULFk0/PA0CdPZc1dZF9zTSaHrtfM7IYaw=
+github.com/go-pdfkit/forms v0.2.1 h1:INa2GwAadxEhcXvBe7zJYgapYwaGn9WIaLxUJU/lm6A=
+github.com/go-pdfkit/forms v0.2.1/go.mod h1:LORxkdP4FVULFk0/PA0CdPZc1dZF9zTSaHrtfM7IYaw=
github.com/go-pdfkit/pdffont v0.1.0 h1:mThWvPn3LLETup2adm9xMcNZWECP2Wq3eTSyTW4O3I0=
github.com/go-pdfkit/pdffont v0.1.0/go.mod h1:y4vo5DgT95e57C3XxIWfA/xss+x6RwZwyj6KWdJc86s=
+github.com/go-pdfkit/pdffont v0.2.0 h1:yAp/oR5Z2kkqs4r0GWMalZMC7rc7XSCZXgwIypbpMWM=
+github.com/go-pdfkit/pdffont v0.2.0/go.mod h1:y4vo5DgT95e57C3XxIWfA/xss+x6RwZwyj6KWdJc86s=
github.com/go-pdfkit/reader v0.4.0 h1:qPbNZSO+Xl+4NBvQoV1PYt7HlqHaEAQ1tUc6/IL8JAU=
github.com/go-pdfkit/reader v0.4.0/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8=
+github.com/go-pdfkit/reader v0.4.1 h1:pRxFqRjsn7H/VsGfWb9nYWyFuDgTU2Pjmoq/f5mgVq4=
+github.com/go-pdfkit/reader v0.4.1/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8=