From e60a7b6edddb24fed67c85b40512d5dc2f4cc3c5 Mon Sep 17 00:00:00 2001 From: David Delavennat Date: Wed, 26 Aug 2026 16:47:58 +0200 Subject: [PATCH] Fill in a form in the browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A form is the one thing in a PDF meant to be changed by whoever receives it, and the one thing every other verb in this workbench would destroy: rotating a page or laying two on a sheet rebuilds the document, and a form is tied into a document by object number in a dozen places at once. So a form is filled in on the file itself. What was opened is kept, the values go into it, and what is saved is that file with the answers appended after it — which is how everything that saves a form saves one, and which means a form saved here is still a form. The panel is built out of the toolkit's own widgets and nothing else: a box to type in, a square to tick, a list to choose from, and a plain label for a field the document says may not be changed — shown all the same, so somebody can read what it holds and see why they cannot type in it. Each is bound to its field through the observable the widget already publishes, so nothing is copied back and forth every frame; a change arrives when it happens. The tests drive the widgets rather than the setters behind them. Calling Field.SetText from a test proves the setter works and says nothing about whether anything is wired to it — and the first version of these tests did exactly that, and passed while three of the bindings were never once executed. --- form.go | 196 +++++++++++++++++++++++ form_test.go | 436 +++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 +- go.sum | 6 + scene.go | 40 ++++- 5 files changed, 677 insertions(+), 6 deletions(-) create mode 100644 form.go create mode 100644 form_test.go diff --git a/form.go b/form.go new file mode 100644 index 0000000..4aa2b87 --- /dev/null +++ b/form.go @@ -0,0 +1,196 @@ +package main + +import ( + "fmt" + + "github.com/go-pdfkit/forms" + "github.com/go-pdfkit/ops" + "github.com/go-widgets/toolkit" +) + +// A form is the one thing in a PDF that is meant to be changed by whoever +// receives it, and the one thing every other verb in this workbench would +// destroy: rotating a page or laying two on a sheet rebuilds the document, and +// a form is tied into a document by object number in a dozen places at once. +// +// So a form is filled in on the file itself. What was opened is kept, the +// values are put into it, and what is saved is that file with the changes +// appended after it — which is how everything that saves a form saves one. +// +// The panel below is built out of the toolkit's own widgets: a box to type in +// for a text field, a square to tick for a checkbox or a button, and a list to +// choose from for a choice field. Each is bound to the field it stands for +// through the observable it already publishes, so nothing is copied back and +// forth every frame; a change arrives when it happens. + +// filling is the form of the document now open, when it has one. +type filling struct { + // what holds the document, its fields, and the means of writing it back. + what *ops.Filling + // rows is the panel, kept so that it is not built again on every frame. + rows toolkit.Widget + // changed counts the fields somebody has altered, which is what the + // status line says and what decides whether saving means anything. + changed int +} + +// readForm looks for a form in what was just opened. A document without one — +// which is nearly every document — simply has none, and the button that shows +// the panel is not offered. +func (s *state) readForm(data []byte) { + s.form = nil + what, ok, err := openForm(data) + if err != nil || !ok { + return + } + s.form = &filling{what: what} +} + +// openForm is a variable so that a test can watch what happens when a +// document has a form that cannot be written back to. +var openForm = ops.OpenForm + +// showForm puts the panel where the page was, or the page back when it is +// already there. A person filling a form wants to see the fields; a person +// checking their work wants to see the page. +func (s *state) showForm() { + if s.form == nil { + s.fail("this document has no form in it") + return + } + s.showingForm = !s.showingForm + s.refresh() +} + +// panel builds the rows, once. +func (f *filling) panel(s *state) toolkit.Widget { + if f.rows != nil { + return f.rows + } + box := toolkit.NewVBox() + box.Spacing = 6 + for _, field := range f.what.Form().Fields() { + row := f.row(s, field) + if row == nil { + continue + } + box.AddFixed(row, formRowH) + } + f.rows = toolkit.NewScrollView(box) + return f.rows +} + +// formRowH is how tall one labelled field is: enough for its name, the thing +// that holds it, and a line underneath for what is wrong with it. +const formRowH = 56 + +// row is one field: what it is called, and the widget that holds it. +func (f *filling) row(s *state, field *forms.Field) toolkit.Widget { + label := field.Name + if field.ReadOnly { + // A field the document says may not be changed is still worth showing, + // so that somebody can see what it holds and why they cannot type in + // it. + return toolkit.NewFormField(label+" (the document does not allow this to be changed)", + toolkit.NewLabel(field.Value)) + } + switch field.Kind { + case forms.Text: + entry := toolkit.NewEntry(field.Value) + entry.Placeholder = placeholderFor(field) + entry.Text().Subscribe(func(v string) { f.set(s, field, v) }) + return toolkit.NewFormField(label, entry) + + case forms.Checkbox, forms.Radio: + box := toolkit.NewCheckButton(buttonLabel(field), field.Checked()) + box.Checked().Subscribe(func(on bool) { f.tick(s, field, on) }) + return toolkit.NewFormField(label, box) + + case forms.ComboBox, forms.ListBox: + options := make([]string, 0, len(field.Options)) + for _, o := range field.Options { + options = append(options, o.Label) + } + if len(options) == 0 { + return nil + } + drop := toolkit.NewDropDown(options, chosenRow(field)) + drop.Selected().Subscribe(func(i int) { f.choose(s, field, i) }) + return toolkit.NewFormField(label, drop) + } + // A push button does nothing here and a signature is not a thing this + // pretends to make. + return nil +} + +// placeholderFor is the hint shown in an empty box: what the document says it +// will take, when it says anything. +func placeholderFor(field *forms.Field) string { + switch { + case field.Comb && field.MaxLen > 0: + return fmt.Sprintf("%d characters, one to a cell", field.MaxLen) + case field.MaxLen > 0: + return fmt.Sprintf("up to %d characters", field.MaxLen) + case field.Multiline: + return "several lines" + } + return "" +} + +// buttonLabel says which button of a group this is, when the group has more +// than the usual two. +func buttonLabel(field *forms.Field) string { + states := field.States() + if len(states) == 1 { + return states[0] + } + return "" +} + +// chosenRow is which row of a choice field is chosen now, or the first when +// none is. +func chosenRow(field *forms.Field) int { + for i, o := range field.Options { + if o.Value == field.Value { + return i + } + } + return 0 +} + +// set, tick and choose put a value in a field and say so. +func (f *filling) set(s *state, field *forms.Field, v string) { + f.after(s, field.SetText(v)) +} + +func (f *filling) tick(s *state, field *forms.Field, on bool) { + f.after(s, field.SetChecked(on)) +} + +func (f *filling) choose(s *state, field *forms.Field, row int) { + if row < 0 || row >= len(field.Options) { + return + } + f.after(s, field.Choose(field.Options[row].Value)) +} + +// after counts what was changed and says what went wrong, if anything. +func (f *filling) after(s *state, err error) { + if err != nil { + s.note = err.Error() + s.dirty = true + return + } + f.changed = len(f.what.Form().Changed()) + s.note = fmt.Sprintf("%d field(s) filled in", f.changed) + s.dirty = true +} + +// bytes is the file to save: what was opened with the answers appended. +func (f *filling) bytes() ([]byte, string) { + out, err := f.what.Bytes() + if err != nil { + return nil, "this form cannot be saved: " + err.Error() + } + return out, "" +} diff --git a/form_test.go b/form_test.go new file mode 100644 index 0000000..e8faff4 --- /dev/null +++ b/form_test.go @@ -0,0 +1,436 @@ +package main + +import ( + "errors" + "strings" + "testing" + + "github.com/go-pdfkit/forms" + "github.com/go-pdfkit/ops" + "github.com/go-pdfkit/reader" + "github.com/go-widgets/toolkit" +) + +// formPDF writes a document with a form on it: a box to type in, one to tick, +// a list to choose from, one the document says may not be changed, and a push +// button, which is not a thing anybody fills in. +func formPDF(t *testing.T) []byte { + t.Helper() + w := reader.NewWriter("1.7") + pagesRef := w.Reserve() + pageRef := w.Reserve() + blank := w.Add(&reader.Stream{Dict: reader.Dict{"BBox": nums(0, 0, 12, 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": nums(20, 150, 180, 175), "MaxLen": reader.Integer(20), + }) + comb := w.Add(reader.Dict{ + "FT": reader.Name("Tx"), "T": reader.String("code"), + "Ff": reader.Integer(1 << 24), "MaxLen": reader.Integer(5), + "Type": reader.Name("Annot"), "Subtype": reader.Name("Widget"), "P": pageRef, + "Rect": nums(20, 130, 180, 145), + }) + many := w.Add(reader.Dict{ + "FT": reader.Name("Tx"), "T": reader.String("story"), + "Ff": reader.Integer(1 << 12), + "Type": reader.Name("Annot"), "Subtype": reader.Name("Widget"), "P": pageRef, + "Rect": nums(20, 100, 180, 125), + }) + tick := w.Add(reader.Dict{ + "FT": reader.Name("Btn"), "T": reader.String("agree"), + "Type": reader.Name("Annot"), "Subtype": reader.Name("Widget"), "P": pageRef, + "Rect": nums(20, 80, 32, 92), + "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": nums(20, 60, 180, 75), + "Opt": reader.Array{ + reader.Array{reader.String("FR"), reader.String("France")}, + reader.Array{reader.String("BE"), reader.String("Belgique")}, + }, + "V": reader.String("BE"), + }) + empty := w.Add(reader.Dict{ + "FT": reader.Name("Ch"), "T": reader.String("nothing"), + "Ff": reader.Integer(1 << 17), + "Type": reader.Name("Annot"), "Subtype": reader.Name("Widget"), "P": pageRef, + "Rect": nums(20, 40, 180, 55), + }) + 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": nums(20, 20, 180, 35), + }) + press := w.Add(reader.Dict{ + "FT": reader.Name("Btn"), "T": reader.String("print"), + "Ff": reader.Integer(1 << 16), + "Type": reader.Name("Annot"), "Subtype": reader.Name("Widget"), "P": pageRef, + "Rect": nums(150, 20, 180, 35), + }) + fields := reader.Array{text, comb, many, tick, list, empty, locked, press} + w.Put(pageRef, reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef, + "MediaBox": nums(0, 0, 200, 200), "Annots": fields, + "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": fields, + "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 err != nil { + t.Fatal(err) + } + return out +} + +func nums(vs ...float64) reader.Array { + out := make(reader.Array, len(vs)) + for i, v := range vs { + out[i] = reader.Real(v) + } + return out +} + +// openedForm opens the workbench on a document with a form in it. +func openedForm(t *testing.T) (*state, *fakeHost) { + t.Helper() + h := &fakeHost{name: "form.pdf", file: formPDF(t)} + s := newState(surfaceW, surfaceH, h) + s.open() + if s.doc == nil { + t.Fatalf("the document did not open: %q", s.note) + } + if s.form == nil { + t.Fatalf("the document has a form and the workbench did not find it: %q", s.note) + } + return s, h +} + +func TestADocumentWithAFormSaysSoOnOpening(t *testing.T) { + s, _ := openedForm(t) + if !strings.Contains(s.note, "form") { + t.Errorf("the workbench said %q", s.note) + } + if got := len(s.form.what.Form().Fields()); got != 8 { + t.Errorf("found %d fields", got) + } +} + +func TestADocumentWithNoFormOffersNothingToFillIn(t *testing.T) { + s, _ := opened(t, 2) + if s.form != nil { + t.Error("a document with no form was given one") + } + s.showForm() + if !strings.Contains(s.note, "no form") { + t.Errorf("the workbench said %q", s.note) + } + if s.showingForm { + t.Error("the panel was shown for a document with no form") + } +} + +func TestTheFieldsAreShownInsteadOfThePage(t *testing.T) { + s, _ := openedForm(t) + page := buffer() + s.draw(page) + s.showForm() + if !s.showingForm { + t.Fatal("the panel was not shown") + } + panel := buffer() + s.draw(panel) + if inked(panel, s.theme.Background) == 0 { + t.Error("the panel drew nothing at all") + } + same := 0 + for i := range page { + if page[i] == panel[i] { + same++ + } + } + if same == len(page) { + t.Error("the panel is the same picture as the page") + } + if got := s.statusLine()[1]; !strings.Contains(got, "8 fields") { + t.Errorf("the status line says %q", got) + } + // And back to the page. + s.showForm() + if s.showingForm { + t.Error("pressing again did not go back to the page") + } +} + +// inputOf digs the actual input out of a labelled row, since that is what a +// person touches and therefore what a test has to touch. Calling the setters +// behind the panel would prove the setters work and say nothing about whether +// anything is wired to them. +func inputOf(t *testing.T, row toolkit.Widget) toolkit.Widget { + t.Helper() + field, ok := row.(*toolkit.FormField) + if !ok { + t.Fatalf("the row is a %T, not a labelled field", row) + } + if field.Child == nil { + t.Fatal("the row has nothing to type into") + } + return field.Child +} + +func TestTypingIntoTheBoxFillsTheFieldIn(t *testing.T) { + s, _ := openedForm(t) + field, ok := s.form.what.Form().Field("name") + if !ok { + t.Fatal("no such field") + } + entry, ok := inputOf(t, s.form.row(s, field)).(*toolkit.Entry) + if !ok { + t.Fatal("a text field is not offered a box to type in") + } + entry.SetText("Mozart") + if field.Value != "Mozart" { + t.Errorf("the field holds %q", field.Value) + } + if s.form.changed != 1 { + t.Errorf("%d fields counted as changed", s.form.changed) + } + if !strings.Contains(s.note, "1 field") { + t.Errorf("the workbench said %q", s.note) + } +} + +func TestTickingTheBoxTicksTheField(t *testing.T) { + s, _ := openedForm(t) + field, _ := s.form.what.Form().Field("agree") + box, ok := inputOf(t, s.form.row(s, field)).(*toolkit.CheckButton) + if !ok { + t.Fatal("a checkbox is not offered a square to tick") + } + box.Checked().Set(true) + if !field.Checked() { + t.Errorf("the field holds %q", field.Value) + } + box.Checked().Set(false) + if field.Checked() { + t.Errorf("unticked, the field holds %q", field.Value) + } +} + +func TestChoosingFromTheListChoosesInTheField(t *testing.T) { + s, _ := openedForm(t) + field, _ := s.form.what.Form().Field("where") + drop, ok := inputOf(t, s.form.row(s, field)).(*toolkit.DropDown) + if !ok { + t.Fatal("a choice field is not offered a list") + } + drop.Select(0) + if field.Value != "FR" { + t.Errorf("the field holds %q", field.Value) + } + // A row that is not there changes nothing rather than breaking. + before := field.Value + s.form.choose(s, field, 9) + s.form.choose(s, field, -1) + if field.Value != before { + t.Errorf("a row that does not exist changed it to %q", field.Value) + } +} +func TestAFieldTheDocumentWillNotAllowToBeChanged(t *testing.T) { + s, _ := openedForm(t) + locked, _ := s.form.what.Form().Field("serial") + s.form.set(s, locked, "something else") + if locked.Value != "A-1756" { + t.Errorf("a read-only field was changed to %q", locked.Value) + } + if !strings.Contains(s.note, "read-only") { + t.Errorf("the workbench said %q", s.note) + } +} + +func TestSavingAFilledFormKeepsItAForm(t *testing.T) { + // Every other verb here rebuilds the document, and a form does not + // survive that. A filled form is saved as the file it came from with the + // answers appended. + s, h := openedForm(t) + field, _ := s.form.what.Form().Field("name") + s.form.set(s, field, "Mozart") + s.save() + if len(h.saved) == 0 { + t.Fatalf("nothing was saved: %q", s.note) + } + d, err := reader.Open(h.saved) + if err != nil { + t.Fatalf("what was saved cannot be read: %v", err) + } + back, ok := forms.Read(d) + if !ok { + t.Fatal("what was saved has no form in it") + } + got, _ := back.Field("name") + if got.Value != "Mozart" { + t.Errorf("the saved form holds %q", got.Value) + } +} + +func TestSavingAFormNobodyFilledInGoesTheUsualWay(t *testing.T) { + s, h := openedForm(t) + s.save() + if len(h.saved) == 0 { + t.Fatalf("nothing was saved: %q", s.note) + } + if _, err := reader.Open(h.saved); err != nil { + t.Errorf("what was saved cannot be read: %v", err) + } +} + +func TestAFormThatCannotBeSaved(t *testing.T) { + s, _ := openedForm(t) + field, _ := s.form.what.Form().Field("name") + s.form.set(s, field, "Mozart") + // A file the reader had to repair has no cross-reference section worth + // pointing back at, which is what the writing refuses. + s.form.what = brokenFilling(t) + if _, msg := s.form.bytes(); msg == "" { + t.Error("a form that cannot be written said nothing") + } + s.save() + if !strings.Contains(s.note, "cannot") { + t.Errorf("the workbench said %q", s.note) + } +} + +// brokenFilling is a form whose fields cannot be written back, because each is +// written inside the field list rather than as an object of its own. +func brokenFilling(t *testing.T) *ops.Filling { + t.Helper() + w := reader.NewWriter("1.7") + pagesRef := w.Reserve() + pageRef := w.Add(reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef, + "MediaBox": nums(0, 0, 200, 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": nums(0, 0, 100, 20)}}})})}) + if err != nil { + t.Fatal(err) + } + f, ok, err := ops.OpenForm(out) + if err != nil || !ok { + t.Fatalf("ok %v err %v", ok, err) + } + fld, _ := f.Form().Field("inline") + if err := fld.SetText("x"); err != nil { + t.Fatal(err) + } + return f +} + +func TestADocumentWhoseFormCannotBeOpened(t *testing.T) { + // The workbench still opens the document; there is simply nothing to fill + // in, which is the same as any other document. + was := openForm + defer func() { openForm = was }() + openForm = func(b []byte) (*ops.Filling, bool, error) { + return nil, false, errRefused + } + h := &fakeHost{name: "form.pdf", file: formPDF(t)} + s := newState(surfaceW, surfaceH, h) + s.open() + if s.doc == nil { + t.Fatal("the document did not open") + } + if s.form != nil { + t.Error("a form was made out of nothing") + } +} + +func TestWhatEachSortOfFieldOffers(t *testing.T) { + s, _ := openedForm(t) + form := s.form.what.Form() + for _, c := range []struct { + name string + want string + }{ + {"name", "up to 20 characters"}, + {"code", "5 characters, one to a cell"}, + {"story", "several lines"}, + } { + field, ok := form.Field(c.name) + if !ok { + t.Fatalf("no field called %q", c.name) + } + if got := placeholderFor(field); got != c.want { + t.Errorf("%s offers %q, wanted %q", c.name, got, c.want) + } + } + tick, _ := form.Field("agree") + if got := buttonLabel(tick); got != "Yes" { + t.Errorf("the box is labelled %q", got) + } + where, _ := form.Field("where") + if got := chosenRow(where); got != 1 { + t.Errorf("the chosen row is %d, wanted the second", got) + } + // A field holding something that is not one of its rows starts at the + // first, since it has to start somewhere. + where.Value = "Andorre" + if got := chosenRow(where); got != 0 { + t.Errorf("a value that is not a row chose row %d", got) + } + // A plain box with nothing said about it offers no hint, and something + // with no buttons at all is labelled by nothing. + plain, _ := form.Field("story") + plain.Multiline = false + if got := placeholderFor(plain); got != "" { + t.Errorf("a plain box offers %q", got) + } + if got := buttonLabel(where); got != "" { + t.Errorf("something with no buttons is labelled %q", got) + } + // A choice field with no rows in it has nothing to show. + none, _ := form.Field("nothing") + if row := s.form.row(s, none); row != nil { + t.Error("a choice field with no rows was given a control") + } + // A push button is not a thing anybody fills in. + press, _ := form.Field("print") + if row := s.form.row(s, press); row != nil { + t.Error("a push button was given a control") + } + // A field the document locks is shown, so that its value can be read. + locked, _ := form.Field("serial") + if row := s.form.row(s, locked); row == nil { + t.Error("a read-only field was not shown at all") + } +} + +func TestThePanelIsBuiltOnce(t *testing.T) { + s, _ := openedForm(t) + first := s.form.panel(s) + if first != s.form.panel(s) { + t.Error("the panel was built again") + } + if _, ok := first.(*toolkit.ScrollView); !ok { + t.Errorf("the panel is a %T, and a form longer than the window has to scroll", first) + } +} + +// errRefused stands for whatever the verb layer says when it will not open a +// form. +var errRefused = errors.New("refused") diff --git a/go.mod b/go.mod index 49aa98c..817a54a 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/go-pdfkit/app go 1.26.4 require ( - github.com/go-pdfkit/ops v0.4.0 - github.com/go-pdfkit/reader v0.4.0 + github.com/go-pdfkit/ops v0.5.0 + github.com/go-pdfkit/reader v0.4.1 github.com/go-pdfkit/render v0.7.0 github.com/go-widgets/painter v0.11.0 github.com/go-widgets/toolkit v0.250.0 @@ -22,6 +22,7 @@ require ( github.com/go-opentype/fonts v0.9.0 // indirect github.com/go-opentype/opentype v0.9.0 // indirect github.com/go-opentype/shape v0.5.0 // indirect + github.com/go-pdfkit/forms v0.2.1 // indirect github.com/go-pdfkit/pdffont v0.2.0 // indirect github.com/go-richdoc/richdoc v0.2.0 // indirect github.com/go-typeset/bidi v0.3.0 // indirect diff --git a/go.sum b/go.sum index 4a644fd..b35a32b 100644 --- a/go.sum +++ b/go.sum @@ -24,12 +24,18 @@ github.com/go-opentype/opentype v0.9.0 h1:GFgcJ3nwTDp4NJr5O+Paw7lhZx5Jv/R+noZwvh github.com/go-opentype/opentype v0.9.0/go.mod h1:AOixevJf7XQaH7+WG+OMIOZEbYPXfMqklVk26Y6YTUU= github.com/go-opentype/shape v0.5.0 h1:jHNaOMHNBdDj5EixOevlrrsi92svMxvVMKN7GYaPfxo= github.com/go-opentype/shape v0.5.0/go.mod h1:3ImRYNIj6zpwWQ/DV3BWhgMFfmzDITH6XpZw2auQHTo= +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/ops v0.4.0 h1:2WeYwDhN2OyoYtjYCdgm7gWGVNmiu+8OwOKEhyb3Cp0= github.com/go-pdfkit/ops v0.4.0/go.mod h1:X89phxHICCYl6+zn3L6YXH3mRLBlsLdPYuDCryGLXzI= +github.com/go-pdfkit/ops v0.5.0 h1:b1LF7hF9QsqtF2AC0G7HjwulNCXJ0AOE7zaUSzswXrg= +github.com/go-pdfkit/ops v0.5.0/go.mod h1:Is3FBR2NcCUEzNt4eowdsVa+P0SBtGtcxK8lDlOeyrk= 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= github.com/go-pdfkit/render v0.7.0 h1:wTEvYxcJkYFYCXJMzrGcMUo0gVgp4A+VqAsTS/hyFq8= github.com/go-pdfkit/render v0.7.0/go.mod h1:RCleIv5QnEDbh9kILa7J8pNUNDUyhsx5p8hv06lpcLk= github.com/go-richdoc/richdoc v0.2.0 h1:z9cLox9MoInZL6fIlweMzgDT/VqgnB2ZucSIEaRFglY= diff --git a/scene.go b/scene.go index b2af655..bb31c55 100644 --- a/scene.go +++ b/scene.go @@ -60,8 +60,15 @@ type state struct { doc *ops.Doc src *reader.Document name string - at int // the page being shown, counting from one - note string + // raw is the file as it arrived. A form is filled in on the file itself + // rather than on a document rebuilt around it, so the bytes are kept. + raw []byte + // form is what the document asks to be filled in, when it asks anything, + // and showingForm says the panel is up instead of the page. + form *filling + showingForm bool + at int // the page being shown, counting from one + note string // dirty says something has changed since the canvas last showed it. A // file arrives from the browser long after the press that asked for it, @@ -101,6 +108,7 @@ func (s *state) tools() *toolkit.HBox { add("Two up", toolkit.ButtonDefault, s.twoUp) add("Watermark", toolkit.ButtonDefault, s.watermark) add("Sanitize", toolkit.ButtonDefault, s.sanitize) + add("Fill in", toolkit.ButtonDefault, s.showForm) return box } @@ -128,8 +136,14 @@ func (s *state) open() { s.fail("cannot open " + name + ": " + err.Error()) return } - s.doc, s.name, s.at = d, name, 1 + s.doc, s.name, s.at, s.raw = d, name, 1, data s.note = "" + s.showingForm = false + s.readForm(data) + if s.form != nil { + s.note = fmt.Sprintf("this document has a form: %d fields", + len(s.form.what.Form().Fields())) + } s.refresh() }) } @@ -140,7 +154,7 @@ func (s *state) save() { s.fail("there is nothing to save") return } - out, msg := s.reopenBytes() + out, msg := s.saveBytes() if msg != "" { s.fail(msg) return @@ -256,12 +270,26 @@ func (s *state) statusLine() []string { return []string{"no document", s.note, "nothing leaves this tab"} } where := fmt.Sprintf("page %d of %d", s.at, s.doc.PageCount()) + if s.showingForm && s.form != nil { + where = fmt.Sprintf("%d fields", len(s.form.what.Form().Fields())) + } return []string{s.name, where, s.note} } // reopenBytes is the document as it would be saved, or the reason it cannot // be written. It reports that reason rather than an error, because the only // thing to do with it is show it. +// saveBytes is what a press of Save writes. A form that has been filled in is +// saved as the file it came from with the answers appended, because that is +// the only way of saving one that keeps it a form; anything else here rebuilds +// the document, and the form does not survive that. +func (s *state) saveBytes() ([]byte, string) { + if s.form != nil && s.form.changed > 0 { + return s.form.bytes() + } + return s.reopenBytes() +} + func (s *state) reopenBytes() ([]byte, string) { out, err := docBytes(s.doc) if err != nil { @@ -302,6 +330,10 @@ func (s *state) renderPage() { s.view = toolkit.NewFrame(s.empty) return } + if s.showingForm && s.form != nil { + s.view = toolkit.NewFrame(s.form.panel(s)) + return + } src, msg := s.reopen() if msg != "" { s.view = toolkit.NewFrame(toolkit.NewLabel(msg))