Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,49 @@ Documentation for all of it: <https://go-pdfkit.github.io/docs/>
## 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.
106 changes: 106 additions & 0 deletions cmd/pdfops/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ var commands = []command{
{"permissions", "<in.pdf>", "say how the file is protected and what it allows", runPermissions},
{"text", "[-pages <range>] [-layout] <in.pdf>", "read the text off the pages", runText},
{"images", "[-pages <range>] <in.pdf> <out-directory>", "write out the pictures the pages place", runImages},
{"fields", "<in.pdf>", "list what a form asks for and what it holds", runFields},
{"fill", "-set <name>=<value> [-set ...] <in.pdf> <out.pdf>", "fill in a form and save it", runFill},
}

// run is the whole program, so that the tests can drive it.
Expand Down Expand Up @@ -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, "<in.pdf>"); 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 <name>=<value>; may be given more than once")
if err := fs.Parse(args); err != nil {
return err
}
if err := wantArgs(fs, 2, "<in.pdf> <out.pdf>"); err != nil {
return err
}
if len(set) == 0 {
return fmt.Errorf("nothing to fill in: give at least one -set <name>=<value>")
}
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 <name>=<value>, 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
}
Loading
Loading