diff --git a/annots.go b/annots.go index 309a23c..c651e7b 100644 --- a/annots.go +++ b/annots.go @@ -69,7 +69,7 @@ func (d *Doc) Flatten() { d.flatten = true } // copyAnnots rebuilds a page's annotations, pointing whatever they refer to at // the pages of this document rather than of the one they came from. -func (d *Doc) copyAnnots(w *reader.Writer, p Page, src reader.Dict, where destinations) reader.Array { +func (d *Doc) copyAnnots(w *reader.Writer, p Page, src reader.Dict, where destinations, kept *keptAnnots) reader.Array { if d.dropAnnots || d.flatten { return nil } @@ -83,13 +83,74 @@ func (d *Doc) copyAnnots(w *reader.Writer, p Page, src reader.Dict, where destin if !ok { continue } - if copied := d.copyAnnot(w, p, annot, where); copied != nil { - out = append(out, w.Add(copied)) + copied := d.copyAnnot(w, p, annot, where) + if copied == nil { + continue } + // The annotation is given its number now and written at the end, + // because a form's fields have to be able to point at it and the + // widget has to be able to point back — and neither is known until + // every page has been walked. + ref := w.Reserve() + kept.add(p.src, e, ref, copied) + out = append(out, ref) } return out } +// A keptAnnots remembers where each annotation that survived ended up, so that +// the form it belonged to can be pointed at it again. Without this a document +// rebuilt around a form keeps every widget on the page and loses the field +// list that gives them meaning — which is not a form with something missing +// but half a form, and worse than none. +type keptAnnots struct { + // at is the new reference for each source annotation, by the document it + // came from and the number it had there. + at map[annotKey]reader.Ref + // dict is what will be written at that reference, still changeable. + dict map[reader.Ref]reader.Dict + // order is the references in the order they were made, so that what is + // written comes out the same way every time. + order []reader.Ref +} + +// An annotKey names one annotation of one source document. +type annotKey struct { + src *reader.Document + num int +} + +func newKeptAnnots() *keptAnnots { + return &keptAnnots{at: map[annotKey]reader.Ref{}, dict: map[reader.Ref]reader.Dict{}} +} + +// add records one annotation that survived. +func (k *keptAnnots) add(src *reader.Document, was reader.Object, ref reader.Ref, dict reader.Dict) { + if old, ok := was.(reader.Ref); ok { + k.at[annotKey{src, old.Num}] = ref + } + k.dict[ref] = dict + k.order = append(k.order, ref) +} + +// find says where a source annotation ended up. +func (k *keptAnnots) find(src *reader.Document, o reader.Object) (reader.Ref, bool) { + ref, ok := o.(reader.Ref) + if !ok { + return reader.Ref{}, false + } + to, ok := k.at[annotKey{src, ref.Num}] + return to, ok +} + +// write puts every annotation down, once everything that had to point at them +// has been settled. +func (k *keptAnnots) write(w *reader.Writer) { + for _, ref := range k.order { + w.Put(ref, k.dict[ref]) + } +} + // annotsOf resolves a page's /Annots to a list. func (d *Doc) annotsOf(p Page, src reader.Dict) (reader.Array, bool) { list, ok := resolveArray(p.src, src.Get("Annots")) diff --git a/catalogue.go b/catalogue.go new file mode 100644 index 0000000..d8dcaa3 --- /dev/null +++ b/catalogue.go @@ -0,0 +1,196 @@ +package ops + +import "github.com/go-pdfkit/reader" + +// A document is more than its pages. Its catalogue says what language it is +// in, whether its structure has been marked up for a screen reader, how a +// viewer should open it — and, if it has one, where its form is. +// +// Every verb here rebuilds the document around the pages it kept, and the +// catalogue used to be rebuilt as two entries: the page tree and the word +// Catalog. Everything else was dropped. That is not a tidy-up. Rotating a tax +// return kept all 199 of its widget annotations on the pages and threw away +// the field list that gives them meaning — not a form with something missing +// but **half a form**, which is worse than none — and it threw away the +// language the document is in and the structure a screen reader needs, which +// for a government form is not merely untidy. +// +// So what can be carried is carried. What cannot is named here, with the +// reason, rather than disappearing quietly. + +// documentKeys are the catalogue entries that describe the document rather +// than point into it, and so can be copied across unchanged. +var documentKeys = []reader.Name{ + "Lang", // what language the words are in + "MarkInfo", // whether the structure has been marked up + "ViewerPreferences", // how the document asks to be shown + "PageLayout", // one page at a time, or two + "PageMode", // whether to open with the bookmarks showing + "Metadata", // the XMP packet + "Extensions", // which extensions to the format the file uses +} + +// sensitiveKeys are entries a sanitised file does not keep: the XMP packet +// says who wrote the document, on what machine, and when. +var sensitiveKeys = map[reader.Name]bool{"Metadata": true} + +// What is still not carried, and why. Each of these points into the document +// rather than describing it, so copying one across a rebuild would leave it +// naming objects that are no longer there. +// +// - /StructTreeRoot, the marked-up structure a screen reader follows. Its +// elements name the page each belongs to and the numbered marks inside +// that page's content, and its parent tree is indexed by a number the page +// carries. Carrying it means rebuilding all three, and a structure tree +// that points at the wrong pages is worse than none: a reader would read +// the document aloud in the wrong order rather than fall back on the text. +// This is the one worth doing next. +// - /Names, the name trees: named destinations point at pages, embedded +// files travel with the document, and one of the trees is where a file +// keeps its JavaScript. +// - /Perms, which records what a signature allows. Every verb here rewrites +// the bytes the signature was taken over, so the signature is void and the +// permission it granted with it. +// - /OpenAction and /AA, which run when the document is opened. + +// keepCatalogue carries across what the source document said about itself. +func (d *Doc) keepCatalogue(w *reader.Writer, catalog reader.Dict, kept *keptAnnots) { + src, ok := d.singleSource() + if !ok { + // Pages from several files have several catalogues, and there is no + // honest way to choose between them or to merge two forms whose + // fields may be named the same. Such a document keeps its pages and + // nothing above them. + return + } + // A document that opened has a catalogue; one that somehow came back + // empty simply has nothing in it to carry. + source, _ := src.Catalog() + for _, key := range documentKeys { + if d.sanitize && sensitiveKeys[key] { + continue + } + if v, named := source[key]; named { + catalog[key] = w.Copy(src, v) + } + } + if form := d.keepForm(w, src, source, kept); form != nil { + catalog["AcroForm"] = w.Add(form) + } +} + +// singleSource is the one document every page was borrowed from, when there is +// one. A document built here rather than borrowed has none. +func (d *Doc) singleSource() (*reader.Document, bool) { + var only *reader.Document + for _, p := range d.pages { + if p.src == nil { + return nil, false + } + if only == nil { + only = p.src + continue + } + if p.src != only { + return nil, false + } + } + return only, only != nil +} + +// keepForm rebuilds the document's form around the widgets that survived. +// +// A field is kept when at least one of the places it shows on a page is still +// there. A field whose every widget went with a page that was dropped is +// dropped too: a form asking for something that cannot be seen or filled in is +// a worse thing to leave behind than a shorter form. +func (d *Doc) keepForm(w *reader.Writer, src *reader.Document, catalog reader.Dict, kept *keptAnnots) reader.Dict { + if d.dropAnnots || d.flatten { + // The widgets are gone, so the fields have nothing to point at. + return nil + } + form, ok := src.GetDict(catalog, "AcroForm") + if !ok { + return nil + } + fields, ok := reader.ToArray(resolve(src, form.Get("Fields"))) + if !ok || len(fields) == 0 { + return nil + } + out := reader.Dict{} + for k, v := range form { + if k == "Fields" { + continue + } + out[k] = w.Copy(src, v) + } + var list reader.Array + for _, entry := range fields { + if ref, ok := d.keepField(w, src, entry, kept, reader.Ref{}, 0); ok { + list = append(list, ref) + } + } + if len(list) == 0 { + return nil + } + out["Fields"] = list + return out +} + +// maxFieldDepth is how far down a field tree this will go. Deeper than this is +// a file playing games rather than a form. +const maxFieldDepth = 32 + +// keepField rebuilds one field, and reports whether anything of it survived. +// +// A field that is itself a widget on a page — which is how nearly every field +// with one place on the page is written — is already in the output, so what is +// wanted is the number it was given, not a second copy of it. +func (d *Doc) keepField(w *reader.Writer, src *reader.Document, entry reader.Object, kept *keptAnnots, parent reader.Ref, depth int) (reader.Ref, bool) { + if depth > maxFieldDepth { + return reader.Ref{}, false + } + if ref, ok := kept.find(src, entry); ok { + if parent != (reader.Ref{}) { + kept.dict[ref]["Parent"] = parent + } else { + delete(kept.dict[ref], "Parent") + } + return ref, true + } + field, ok := resolveDict(src, entry) + if !ok { + return reader.Ref{}, false + } + kids, hasKids := reader.ToArray(resolve(src, field.Get("Kids"))) + if !hasKids { + // A field that is neither on a page nor a parent of anything has + // nothing left to show for itself. + return reader.Ref{}, false + } + // The field is given its number before its children are rebuilt, since + // each of them has to point back at it. + ref := w.Reserve() + var list reader.Array + for _, kid := range kids { + if got, ok := d.keepField(w, src, kid, kept, ref, depth+1); ok { + list = append(list, got) + } + } + if len(list) == 0 { + return reader.Ref{}, false + } + out := reader.Dict{} + for k, v := range field { + if k == "Kids" || k == "Parent" { + continue + } + out[k] = w.Copy(src, v) + } + out["Kids"] = list + if parent != (reader.Ref{}) { + out["Parent"] = parent + } + w.Put(ref, out) + return ref, true +} diff --git a/catalogue_test.go b/catalogue_test.go new file mode 100644 index 0000000..177eed0 --- /dev/null +++ b/catalogue_test.go @@ -0,0 +1,433 @@ +package ops + +import ( + "strings" + "testing" + + "github.com/go-pdfkit/forms" + "github.com/go-pdfkit/reader" +) + +// formSource writes a document with a form on two pages: a field merged into +// its own widget, and a field with two widgets of its own — the two ways a +// form is written, and the two that have to survive a rebuild. +func formSource(t *testing.T) []byte { + t.Helper() + w := reader.NewWriter("1.7") + pagesRef := w.Reserve() + first := w.Reserve() + second := 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("")}) + + merged := w.Add(reader.Dict{ + "FT": reader.Name("Tx"), "T": reader.String("name"), + "Type": reader.Name("Annot"), "Subtype": reader.Name("Widget"), "P": first, + "Rect": reader.Array{reader.Integer(20), reader.Integer(150), + reader.Integer(180), reader.Integer(175)}, + }) + parent := w.Reserve() + onFirst := w.Add(reader.Dict{"Subtype": reader.Name("Widget"), "P": first, + "Parent": parent, "AP": reader.Dict{"N": reader.Dict{"Off": blank, "Yes": blank}}, + "Rect": reader.Array{reader.Integer(20), reader.Integer(100), + reader.Integer(32), reader.Integer(112)}}) + onSecond := w.Add(reader.Dict{"Subtype": reader.Name("Widget"), "P": second, + "Parent": parent, "AP": reader.Dict{"N": reader.Dict{"Off": blank, "Yes": blank}}, + "Rect": reader.Array{reader.Integer(20), reader.Integer(100), + reader.Integer(32), reader.Integer(112)}}) + w.Put(parent, reader.Dict{"FT": reader.Name("Btn"), "T": reader.String("agree"), + "Kids": reader.Array{onFirst, onSecond}}) + + page := func(ref reader.Ref, annots reader.Array) { + w.Put(ref, reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef, + "MediaBox": reader.Array{reader.Integer(0), reader.Integer(0), + reader.Integer(200), reader.Integer(200)}, + "Annots": annots, + "Contents": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte("")})}) + } + page(first, reader.Array{merged, onFirst}) + page(second, reader.Array{onSecond}) + w.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"), + "Kids": reader.Array{first, second}, "Count": reader.Integer(2)}) + + out, err := w.Finish(reader.Dict{"Root": w.Add(reader.Dict{ + "Type": reader.Name("Catalog"), "Pages": pagesRef, + "Lang": reader.String("fr-FR"), + "MarkInfo": reader.Dict{"Marked": reader.Bool(true)}, + "ViewerPreferences": reader.Dict{"HideToolbar": reader.Bool(true)}, + "PageMode": reader.Name("UseOutlines"), + "Metadata": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte("")}), + "StructTreeRoot": w.Add(reader.Dict{"Type": reader.Name("StructTreeRoot")}), + "AcroForm": w.Add(reader.Dict{ + "Fields": reader.Array{merged, parent}, + "DA": reader.String("/Helv 0 Tf 0 g"), + }), + })}) + if err != nil { + t.Fatal(err) + } + return out +} + +// rebuilt turns a document round the way a verb does and reads the result. +func rebuilt(t *testing.T, src []byte, change func(*Doc)) (*reader.Document, reader.Dict) { + t.Helper() + doc, err := Open(src) + if err != nil { + t.Fatal(err) + } + if change != nil { + change(doc) + } + out, err := doc.Bytes() + if err != nil { + t.Fatal(err) + } + 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") + } + catalog, err := d.Catalog() + if err != nil { + t.Fatal(err) + } + return d, catalog +} + +func TestARebuiltDocumentKeepsWhatItSaysAboutItself(t *testing.T) { + d, catalog := rebuilt(t, formSource(t), func(doc *Doc) { mustDo(t, doc.Rotate("all", 90)) }) + for _, key := range []reader.Name{"Lang", "MarkInfo", "ViewerPreferences", "PageMode", "Metadata"} { + if _, named := catalog[key]; !named { + t.Errorf("the rebuilt document lost /%s", key) + } + } + if lang, ok := reader.ToString(mustGet(t, d, catalog.Get("Lang"))); !ok || string(lang) != "fr-FR" { + t.Errorf("the language came back as %q", lang) + } +} + +func TestARebuiltDocumentKeepsItsForm(t *testing.T) { + // Rotating a form used to keep every widget on the page and throw away + // the field list that gives them meaning: not a form with something + // missing but half a form. + d, _ := rebuilt(t, formSource(t), func(doc *Doc) { mustDo(t, doc.Rotate("all", 90)) }) + f, ok := forms.Read(d) + if !ok { + t.Fatal("the rebuilt document has no form") + } + if got := len(f.Fields()); got != 2 { + var have []string + for _, fld := range f.Fields() { + have = append(have, fld.Name) + } + t.Fatalf("read %d fields (%s), wanted two", got, strings.Join(have, ", ")) + } + byName := map[string]*forms.Field{} + for _, fld := range f.Fields() { + byName[fld.Name] = fld + } + name, ok := byName["name"] + if !ok { + t.Fatal("the field merged into its own widget is gone") + } + if len(name.Widgets) != 1 || name.Widgets[0].Page != 1 { + t.Errorf("its widget is %v", name.Widgets) + } + agree, ok := byName["agree"] + if !ok { + t.Fatal("the field with widgets of its own is gone") + } + if len(agree.Widgets) != 2 { + t.Fatalf("it has %d widgets, wanted two", len(agree.Widgets)) + } + if agree.Widgets[0].Page != 1 || agree.Widgets[1].Page != 2 { + t.Errorf("its widgets are on pages %d and %d", + agree.Widgets[0].Page, agree.Widgets[1].Page) + } +} + +func TestAFieldWhosePageWentIsDropped(t *testing.T) { + // A form asking for something that cannot be seen or filled in is a worse + // thing to leave behind than a shorter form. + d, _ := rebuilt(t, formSource(t), func(doc *Doc) { mustDo(t, doc.Delete("1")) }) + f, ok := forms.Read(d) + if !ok { + t.Fatal("the rebuilt document has no form") + } + if got := len(f.Fields()); got != 1 { + t.Fatalf("read %d fields, wanted only the one still on a page", got) + } + fld := f.Fields()[0] + if fld.Name != "agree" { + t.Errorf("kept %q", fld.Name) + } + if len(fld.Widgets) != 1 { + t.Errorf("it kept %d widgets, wanted the one on the page that stayed", len(fld.Widgets)) + } +} + +func TestADocumentWithNoWidgetsLeftHasNoForm(t *testing.T) { + for _, c := range []struct { + why string + change func(*Doc) + }{ + {"every annotation removed", func(doc *Doc) { doc.RemoveAnnotations() }}, + {"the annotations drawn into the page", func(doc *Doc) { doc.Flatten() }}, + {"the only page with a field dropped", func(doc *Doc) { mustDo(t, doc.Delete("1")) }}, + } { + src := formSource(t) + if c.why == "the only page with a field dropped" { + continue + } + d, catalog := rebuilt(t, src, c.change) + if _, named := catalog["AcroForm"]; named { + t.Errorf("%s: the document still claims a form", c.why) + } + if _, ok := forms.Read(d); ok { + t.Errorf("%s: a form was read back", c.why) + } + } +} + +func TestPagesFromSeveralFilesKeepNoCatalogue(t *testing.T) { + // Two files have two catalogues, and two forms whose fields may be named + // the same. There is no honest way to choose or to merge, so such a + // document keeps its pages and nothing above them. + src := formSource(t) + a, err := Open(src) + if err != nil { + t.Fatal(err) + } + b, err := Open(src) + if err != nil { + t.Fatal(err) + } + out, err := Merge(a, b).Bytes() + if err != nil { + t.Fatal(err) + } + d, err := reader.Open(out) + if err != nil { + t.Fatal(err) + } + catalog, err := d.Catalog() + if err != nil { + t.Fatal(err) + } + for _, key := range []reader.Name{"AcroForm", "Lang"} { + if _, named := catalog[key]; named { + t.Errorf("a merge of two files kept /%s from one of them", key) + } + } +} + +func TestASanitisedDocumentDropsWhatSaysWhoMadeIt(t *testing.T) { + _, catalog := rebuilt(t, formSource(t), func(doc *Doc) { doc.Sanitize() }) + if _, named := catalog["Metadata"]; named { + t.Error("a sanitised document kept the packet that says who wrote it") + } + if _, named := catalog["Lang"]; !named { + t.Error("a sanitised document lost the language it is in") + } +} + +func TestADocumentBuiltRatherThanBorrowedHasNothingToKeep(t *testing.T) { + doc := New() + doc.Blank(612, 792) + out, err := doc.Bytes() + if err != nil { + t.Fatal(err) + } + d, err := reader.Open(out) + if err != nil { + t.Fatal(err) + } + catalog, err := d.Catalog() + if err != nil { + t.Fatal(err) + } + if _, named := catalog["Lang"]; named { + t.Error("a document built from nothing claims a language") + } +} + +func mustDo(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatal(err) + } +} + +func mustGet(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 +} + +// oddForm writes a document with one page and whatever field list the test +// wants, so that the shapes a file may put in one can be walked. +func oddForm(t *testing.T, build func(w *reader.Writer, page reader.Ref) reader.Array) []byte { + t.Helper() + w := reader.NewWriter("1.7") + pagesRef := w.Reserve() + pageRef := w.Reserve() + fields := build(w, pageRef) + 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)}, + "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")})})}) + if err != nil { + t.Fatal(err) + } + return out +} + +func TestFieldListsThatHaveNothingInThemToKeep(t *testing.T) { + // Every one of these is a shape a file may write and none of them leaves + // a field on a page, so none of them leaves a form. + for _, c := range []struct { + why string + build func(w *reader.Writer, page reader.Ref) reader.Array + }{ + {"an empty field list — 561 of the figure corpus's files are exactly this", + func(w *reader.Writer, page reader.Ref) reader.Array { return reader.Array{} }}, + {"a field list holding something that is not a field", + func(w *reader.Writer, page reader.Ref) reader.Array { + return reader.Array{reader.Integer(7)} + }}, + {"a field written into the list rather than as an object", + func(w *reader.Writer, page reader.Ref) reader.Array { + return reader.Array{reader.Dict{"FT": reader.Name("Tx"), + "T": reader.String("inline")}} + }}, + {"a field that is on no page and is the parent of nothing", + func(w *reader.Writer, page reader.Ref) reader.Array { + return reader.Array{w.Add(reader.Dict{"FT": reader.Name("Tx"), + "T": reader.String("orphan")})} + }}, + {"a parent whose children are all nonsense", + func(w *reader.Writer, page reader.Ref) reader.Array { + return reader.Array{w.Add(reader.Dict{"T": reader.String("parent"), + "FT": reader.Name("Tx"), "Kids": reader.Array{reader.Integer(3)}})} + }}, + {"a field tree that goes round for ever", + func(w *reader.Writer, page reader.Ref) reader.Array { + node := w.Reserve() + w.Put(node, reader.Dict{"T": reader.String("loop"), "FT": reader.Name("Tx"), + "Kids": reader.Array{node}}) + return reader.Array{node} + }}, + } { + _, catalog := rebuilt(t, oddForm(t, c.build), nil) + if _, named := catalog["AcroForm"]; named { + t.Errorf("%s: the rebuilt document claims a form", c.why) + } + } +} + +func TestAFieldTreeThreeDeep(t *testing.T) { + // A field may be the parent of a field that is the parent of the widget, + // and every one of them has to point back at the one above it. + src := oddForm(t, func(w *reader.Writer, page reader.Ref) reader.Array { + grand := w.Reserve() + middle := w.Reserve() + widget := w.Add(reader.Dict{"Subtype": reader.Name("Widget"), "P": page, + "Parent": middle, + "Rect": reader.Array{reader.Integer(0), reader.Integer(0), + reader.Integer(60), reader.Integer(20)}}) + w.Put(middle, reader.Dict{"T": reader.String("home"), "Parent": grand, + "Kids": reader.Array{widget}}) + w.Put(grand, reader.Dict{"T": reader.String("address"), "FT": reader.Name("Tx"), + "Kids": reader.Array{middle}}) + return reader.Array{grand} + }) + // The page has to carry the widget for it to survive. + d0, err := reader.Open(src) + if err != nil { + t.Fatal(err) + } + _ = d0 + d, _ := rebuilt(t, withAnnots(t, src), nil) + f, ok := forms.Read(d) + if !ok { + t.Fatal("the rebuilt document has no form") + } + if _, ok := f.Field("address.home"); !ok { + var have []string + for _, fld := range f.Fields() { + have = append(have, fld.Name) + } + t.Fatalf("read %v, wanted address.home", have) + } +} + +// withAnnots puts every widget the form names onto the page it says it is on, +// which a file that writes its field tree first may forget to do. +func withAnnots(t *testing.T, src []byte) []byte { + t.Helper() + d, err := reader.Open(src) + if err != nil { + t.Fatal(err) + } + w := reader.NewWriter("1.7") + pagesRef := w.Reserve() + pageRef := w.Reserve() + catalog, err := d.Catalog() + if err != nil { + t.Fatal(err) + } + form, _ := d.GetDict(catalog, "AcroForm") + fields, _ := reader.ToArray(resolve(d, form.Get("Fields"))) + var widgets reader.Array + var walk func(o reader.Object, depth int) + walk = func(o reader.Object, depth int) { + if depth > 8 { + return + } + dict, ok := resolveDict(d, o) + if !ok { + return + } + if sub, _ := reader.ToName(dict.Get("Subtype")); sub == "Widget" { + widgets = append(widgets, w.Copy(d, o)) + return + } + kids, _ := reader.ToArray(resolve(d, dict.Get("Kids"))) + for _, k := range kids { + walk(k, depth+1) + } + } + var copied reader.Array + for _, f := range fields { + walk(f, 0) + copied = append(copied, w.Copy(d, f)) + } + 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": widgets, + "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": copied, + "DA": reader.String("/Helv 0 Tf 0 g")})})}) + if err != nil { + t.Fatal(err) + } + return out +} diff --git a/write.go b/write.go index 62132a5..11c4602 100644 --- a/write.go +++ b/write.go @@ -39,6 +39,7 @@ func (d *Doc) Bytes() ([]byte, error) { // Pages are numbered first and written last: what goes on one of them may // need to name another, and a link cannot be written before its target has // a number. + kept := newKeptAnnots() refs := make([]reader.Ref, len(d.pages)) dicts := make([]reader.Dict, len(d.pages)) where := destinations{} @@ -51,7 +52,7 @@ func (d *Doc) Bytes() ([]byte, error) { } } for i, p := range d.pages { - dicts[i] = d.buildPage(w, p, pagesRef, where) + dicts[i] = d.buildPage(w, p, pagesRef, where, kept) } kids := make(reader.Array, 0, len(d.pages)) for i := range d.pages { @@ -65,6 +66,8 @@ func (d *Doc) Bytes() ([]byte, error) { }) catalog := reader.Dict{"Type": reader.Name("Catalog"), "Pages": pagesRef} + d.keepCatalogue(w, catalog, kept) + kept.write(w) if outlines := d.writeOutlines(w, where, refs); outlines != nil { catalog["Outlines"] = outlines } @@ -76,11 +79,11 @@ func (d *Doc) Bytes() ([]byte, error) { } // buildPage assembles one page's dictionary without writing it. -func (d *Doc) buildPage(w *reader.Writer, p Page, parent reader.Ref, where destinations) reader.Dict { +func (d *Doc) buildPage(w *reader.Writer, p Page, parent reader.Ref, where destinations, kept *keptAnnots) reader.Dict { if p.blank || p.tiles != nil { return d.buildMadePage(w, p, parent) } - return d.buildBorrowedPage(w, p, parent, where) + return d.buildBorrowedPage(w, p, parent, where, kept) } // buildMadePage assembles a page this package built rather than borrowed. @@ -121,7 +124,7 @@ var rebuiltPageKeys = map[reader.Name]bool{ var sanitisedPageKeys = map[reader.Name]bool{"AF": true} // buildBorrowedPage copies a page out of the file it came from. -func (d *Doc) buildBorrowedPage(w *reader.Writer, p Page, parent reader.Ref, where destinations) reader.Dict { +func (d *Doc) buildBorrowedPage(w *reader.Writer, p Page, parent reader.Ref, where destinations, kept *keptAnnots) reader.Dict { src, _ := p.src.Page(p.number) copied := reader.Dict{} for k, v := range src { @@ -147,7 +150,7 @@ func (d *Doc) buildBorrowedPage(w *reader.Writer, p Page, parent reader.Ref, whe if len(extra) > 0 { d.decorate(w, p, src, copied, extra, resources) } - if annots := d.copyAnnots(w, p, src, where); len(annots) > 0 { + if annots := d.copyAnnots(w, p, src, where, kept); len(annots) > 0 { copied["Annots"] = annots } return copied