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))