diff --git a/pdf/model/annotations.go b/pdf/model/annotations.go index a0c8733d..735112fa 100644 --- a/pdf/model/annotations.go +++ b/pdf/model/annotations.go @@ -306,6 +306,8 @@ type PdfAnnotationWidget struct { AA core.PdfObject BS core.PdfObject Parent core.PdfObject + + parent *PdfField } // PdfAnnotationWatermark represents Watermark annotations. @@ -1796,7 +1798,12 @@ func (widget *PdfAnnotationWidget) ToPdfObject() core.PdfObject { d.SetIfNotNil("A", widget.A) d.SetIfNotNil("AA", widget.AA) d.SetIfNotNil("BS", widget.BS) - d.SetIfNotNil("Parent", widget.Parent) + + if widget.parent != nil { + d.SetIfNotNil("Parent", widget.parent.GetContainingPdfObject()) + } else if widget.Parent != nil { + d.SetIfNotNil("Parent", widget.Parent) + } return container } diff --git a/pdf/model/fields.go b/pdf/model/fields.go index a4a18bf0..d942bd40 100644 --- a/pdf/model/fields.go +++ b/pdf/model/fields.go @@ -547,16 +547,16 @@ func (f *PdfField) SetFlag(flag FieldFlag) { // newPdfFieldFromIndirectObject load a field from an indirect object containing the field dictionary. func (r *PdfReader) newPdfFieldFromIndirectObject(container *core.PdfIndirectObject, parent *PdfField) (*PdfField, error) { - d, isDict := container.PdfObject.(*core.PdfObjectDictionary) - if !isDict { - return nil, fmt.Errorf("PdfField indirect object not containing a dictionary") - } - // If already processed and cached - return processed model. if field, cached := r.modelManager.GetModelFromPrimitive(container).(*PdfField); cached { return field, nil } + d, isDict := core.GetDict(container) + if !isDict { + return nil, fmt.Errorf("PdfField indirect object not containing a dictionary") + } + field := NewPdfField() // Field type (required in terminal fields). @@ -661,7 +661,8 @@ func (r *PdfReader) newPdfFieldFromIndirectObject(container *core.PdfIndirectObj if !ok { return nil, errors.New("invalid widget annotation") } - widget.Parent = field.GetContainingPdfObject() + widget.parent = field + widget.Parent = field.container field.Annotations = append(field.Annotations, widget) @@ -695,6 +696,7 @@ func (r *PdfReader) newPdfFieldFromIndirectObject(container *core.PdfIndirectObj if !ok { return nil, ErrTypeCheck } + wa.parent = field field.Annotations = append(field.Annotations, wa) } else { childf, err := r.newPdfFieldFromIndirectObject(container, field) diff --git a/pdf/model/page.go b/pdf/model/page.go index 43977335..34a64fd4 100644 --- a/pdf/model/page.go +++ b/pdf/model/page.go @@ -495,6 +495,8 @@ func (p *PdfPage) GetPageDict() *core.PdfObjectDictionary { } } d.Set("Annots", arr) + } else if p.Annots != nil { + d.SetIfNotNil("Annots", p.Annots) } return d diff --git a/pdf/model/writer.go b/pdf/model/writer.go index cdbc8c4d..7bfd4b6a 100644 --- a/pdf/model/writer.go +++ b/pdf/model/writer.go @@ -157,7 +157,7 @@ type PdfWriter struct { // for writing. // The map stores the object and the dictionary it is contained in. // Only way so we can access the dictionary entry later. - pendingObjects map[core.PdfObject]*core.PdfObjectDictionary + pendingObjects map[core.PdfObject][]*core.PdfObjectDictionary // Forms. acroForm *PdfAcroForm @@ -179,7 +179,7 @@ func NewPdfWriter() PdfWriter { w.objectsMap = map[core.PdfObject]struct{}{} w.objects = []core.PdfObject{} - w.pendingObjects = map[core.PdfObject]*core.PdfObjectDictionary{} + w.pendingObjects = map[core.PdfObject][]*core.PdfObjectDictionary{} w.traversed = map[core.PdfObject]struct{}{} // PDF Version. Can be changed if using more advanced features in PDF. @@ -466,8 +466,8 @@ func (w *PdfWriter) addObjects(obj core.PdfObject) error { } if hasObj := w.hasObject(v); !hasObj { - common.Log.Debug("Parent obj is missing!! %T %p %v", v, v, v) - w.pendingObjects[v] = dict + common.Log.Debug("Parent obj not added yet!! %T %p %v", v, v, v) + w.pendingObjects[v] = append(w.pendingObjects[v], dict) // Although it is missing at this point, it could be added later... } // How to handle the parent? Make sure it is present? @@ -889,15 +889,17 @@ func (w *PdfWriter) Write(writer io.Writer) error { } // Check pending objects prior to write. - for pendingObj, pendingObjDict := range w.pendingObjects { + for pendingObj, pendingObjDicts := range w.pendingObjects { if !w.hasObject(pendingObj) { - common.Log.Debug("ERROR Pending object %+v %T (%p) never added for writing", pendingObj, pendingObj, pendingObj) - for _, key := range pendingObjDict.Keys() { - val := pendingObjDict.Get(key) - if val == pendingObj { - common.Log.Debug("Pending object found! and replaced with null") - pendingObjDict.Set(key, core.MakeNull()) - break + common.Log.Debug("WARN Pending object %+v %T (%p) never added for writing", pendingObj, pendingObj, pendingObj) + for _, pendingObjDict := range pendingObjDicts { + for _, key := range pendingObjDict.Keys() { + val := pendingObjDict.Get(key) + if val == pendingObj { + common.Log.Debug("Pending object found! and replaced with null") + pendingObjDict.Set(key, core.MakeNull()) + break + } } } } diff --git a/pdf/model/writer_test.go b/pdf/model/writer_test.go new file mode 100644 index 00000000..dbb30bb1 --- /dev/null +++ b/pdf/model/writer_test.go @@ -0,0 +1,102 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package model + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/unidoc/unidoc/pdf/core" +) + +// Tests loading annotations from file, writing back out and reloading. +func TestReadWriteAnnotations(t *testing.T) { + f, err := os.Open(`testdata/OoPdfFormExample.pdf`) + require.NoError(t, err) + defer f.Close() + + reader, err := NewPdfReaderLazy(f) + require.NoError(t, err) + + checkAnnots := func(reader *PdfReader, formExpected bool) { + // Check Acroform and fields. + if formExpected { + require.NotNil(t, reader.AcroForm) + fields := reader.AcroForm.AllFields() + require.Len(t, fields, 17) + require.Nil(t, fields[0].Parent) + } else { + require.Nil(t, reader.AcroForm) + } + + // Check annotations. + numPages, err := reader.GetNumPages() + require.NoError(t, err) + require.Equal(t, 1, numPages) + + page, err := reader.GetPage(1) + require.NoError(t, err) + + require.NotNil(t, page.Annots) + annots, err := page.GetAnnotations() + require.NoError(t, err) + require.Len(t, annots, 17) + + wa, ok := annots[0].GetContext().(*PdfAnnotationWidget) + require.True(t, ok) + if formExpected { + require.NotNil(t, wa.parent) + require.NotNil(t, wa.Parent) + } else { + require.Nil(t, wa.parent) + require.True(t, core.IsNullObject(wa.Parent)) + } + } + checkAnnots(reader, true) + + // Write out and reload. With the AcroForm in place. + { + w := NewPdfWriter() + page, err := reader.GetPage(1) + require.NoError(t, err) + err = w.AddPage(page) + require.NoError(t, err) + err = w.SetForms(reader.AcroForm) + require.NoError(t, err) + + var buf bytes.Buffer + err = w.Write(&buf) + require.NoError(t, err) + + bufReader := bytes.NewReader(buf.Bytes()) + reader, err = NewPdfReaderLazy(bufReader) + require.NoError(t, err) + + checkAnnots(reader, true) + } + + // Write out and reload without setting the AcroForm. + { + w := NewPdfWriter() + page, err := reader.GetPage(1) + require.NoError(t, err) + err = w.AddPage(page) + require.NoError(t, err) + + var buf bytes.Buffer + err = w.Write(&buf) + require.NoError(t, err) + + bufReader := bytes.NewReader(buf.Bytes()) + reader, err = NewPdfReaderLazy(bufReader) + require.NoError(t, err) + + checkAnnots(reader, false) + } +}