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=