2018-08-08 13:12:56 +00:00
|
|
|
/*
|
|
|
|
* 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 (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"math"
|
|
|
|
"strings"
|
|
|
|
|
2019-05-16 23:08:40 +03:00
|
|
|
"github.com/unidoc/unipdf/v3/common"
|
2019-05-16 23:44:51 +03:00
|
|
|
"github.com/unidoc/unipdf/v3/core"
|
2018-08-08 13:12:56 +00:00
|
|
|
)
|
|
|
|
|
2018-10-05 01:59:19 +00:00
|
|
|
// ContentStreamWrapper wraps the Page's contentstream into q ... Q blocks.
|
|
|
|
type ContentStreamWrapper interface {
|
|
|
|
WrapContentStream(page *PdfPage) error
|
|
|
|
}
|
|
|
|
|
2018-09-28 09:50:48 +00:00
|
|
|
// FieldAppearanceGenerator generates appearance stream for a given field.
|
|
|
|
type FieldAppearanceGenerator interface {
|
2018-10-05 01:59:19 +00:00
|
|
|
ContentStreamWrapper
|
2018-09-28 09:50:48 +00:00
|
|
|
GenerateAppearanceDict(form *PdfAcroForm, field *PdfField, wa *PdfAnnotationWidget) (*core.PdfObjectDictionary, error)
|
|
|
|
}
|
|
|
|
|
2018-08-08 13:12:56 +00:00
|
|
|
// FlattenFields flattens the form fields and annotations for the PDF loaded in `pdf` and makes
|
|
|
|
// non-editable.
|
|
|
|
// Looks up all widget annotations corresponding to form fields and flattens them by drawing the content
|
|
|
|
// through the content stream rather than annotations.
|
|
|
|
// References to flattened annotations will be removed from Page Annots array. For fields the AcroForm entry
|
|
|
|
// will be emptied.
|
|
|
|
// When `allannots` is true, all annotations will be flattened. Keep false if want to keep non-form related
|
|
|
|
// annotations intact.
|
2018-09-28 09:50:48 +00:00
|
|
|
// When `appgen` is not nil, it will be used to generate appearance streams for the field annotations.
|
2018-12-09 21:25:30 +02:00
|
|
|
func (r *PdfReader) FlattenFields(allannots bool, appgen FieldAppearanceGenerator) error {
|
2018-08-08 13:12:56 +00:00
|
|
|
// Load all target widget annotations to be flattened into a map.
|
|
|
|
// The bool value indicates whether the annotation has value content.
|
|
|
|
ftargets := map[*PdfAnnotation]bool{}
|
|
|
|
{
|
2019-06-25 22:29:03 +03:00
|
|
|
var fields []*PdfField
|
2018-12-09 21:25:30 +02:00
|
|
|
acroForm := r.AcroForm
|
2019-06-25 22:29:03 +03:00
|
|
|
if acroForm != nil {
|
|
|
|
fields = acroForm.AllFields()
|
2018-08-08 13:12:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, field := range fields {
|
|
|
|
for _, wa := range field.Annotations {
|
2018-09-24 19:50:58 +00:00
|
|
|
// TODO(gunnsth): Check if wa.Flags() has Print flag then include, otherwise exclude.
|
|
|
|
|
2018-10-23 12:03:47 +00:00
|
|
|
// NOTE(gunnsth): May be better to check field.V only if no appearance stream available.
|
|
|
|
ftargets[wa.PdfAnnotation] = field.V != nil
|
2018-09-28 09:50:48 +00:00
|
|
|
|
|
|
|
if appgen != nil {
|
|
|
|
// appgen generates the appearance based on the form/field/annotation and other settings
|
|
|
|
// based on the implementation (for example may only generate appearance if none set).
|
|
|
|
apDict, err := appgen.GenerateAppearanceDict(acroForm, field, wa)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
wa.AP = apDict
|
|
|
|
}
|
2018-08-08 13:12:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If all annotations are to be flattened, add to targets.
|
|
|
|
if allannots {
|
2018-12-09 21:25:30 +02:00
|
|
|
for _, page := range r.PageList {
|
2019-06-25 22:29:03 +03:00
|
|
|
annotations, err := page.GetAnnotations()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, annot := range annotations {
|
2018-08-08 13:12:56 +00:00
|
|
|
ftargets[annot] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Go through all pages and flatten specified annotations.
|
2018-12-09 21:25:30 +02:00
|
|
|
for _, page := range r.PageList {
|
2018-12-09 19:28:50 +02:00
|
|
|
var annots []*PdfAnnotation
|
2018-10-05 01:59:19 +00:00
|
|
|
|
|
|
|
// Wrap the content streams.
|
|
|
|
err := appgen.WrapContentStream(page)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-06-25 22:29:03 +03:00
|
|
|
annotations, err := page.GetAnnotations()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, annot := range annotations {
|
2018-08-08 13:12:56 +00:00
|
|
|
hasV, toflatten := ftargets[annot]
|
|
|
|
if !toflatten {
|
|
|
|
// Not to be flattened.
|
|
|
|
annots = append(annots, annot)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Flatten annotation.
|
|
|
|
// Annotations not requiring an appearance dictionary.
|
|
|
|
switch annot.GetContext().(type) {
|
|
|
|
case *PdfAnnotationPopup:
|
|
|
|
continue
|
|
|
|
case *PdfAnnotationLink:
|
|
|
|
continue
|
|
|
|
case *PdfAnnotationProjection:
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
xform, rect, err := getAnnotationActiveAppearance(annot)
|
|
|
|
if err != nil {
|
|
|
|
if !hasV {
|
2018-09-28 09:50:48 +00:00
|
|
|
common.Log.Trace("Field without V -> annotation without appearance stream - skipping over")
|
2018-08-08 13:12:56 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
common.Log.Debug("ERROR Annotation without appearance stream, err : %v - skipping over", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if xform == nil {
|
|
|
|
// No appearance.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the XForm to Page resources and draw it in the contentstream.
|
|
|
|
name := page.Resources.GenerateXObjectName()
|
|
|
|
page.Resources.SetXObjectFormByName(name, xform)
|
|
|
|
|
2018-10-15 10:58:19 +00:00
|
|
|
// TODO(gunnsth): Take Matrix and potential scaling of annotation Rect and appearance
|
|
|
|
// BBox into account. Have yet to find a case where that actually is required.
|
2018-08-08 13:12:56 +00:00
|
|
|
|
|
|
|
// Placement for XForm.
|
|
|
|
xRect := math.Min(rect.Llx, rect.Urx)
|
|
|
|
yRect := math.Min(rect.Lly, rect.Ury) // Needed for rect in: govdocs 019693.pdf.
|
|
|
|
|
|
|
|
// Generate the content stream to display the XForm.
|
2018-10-15 10:58:19 +00:00
|
|
|
// TODO(gunnsth): Creating the contentstream directly here as cannot import contentstream package into
|
2018-08-08 13:12:56 +00:00
|
|
|
// model (as contentstream depends on model). Consider if we can change the dependency pattern.
|
2018-12-09 19:28:50 +02:00
|
|
|
var ops []string
|
2018-08-08 13:12:56 +00:00
|
|
|
ops = append(ops, "q")
|
|
|
|
ops = append(ops, fmt.Sprintf("%.6f %.6f %.6f %.6f %.6f %.6f cm", 1.0, 0.0, 0.0, 1.0, xRect, yRect))
|
|
|
|
ops = append(ops, fmt.Sprintf("/%s Do", name.String()))
|
|
|
|
ops = append(ops, "Q")
|
|
|
|
contentstr := strings.Join(ops, "\n")
|
|
|
|
|
2018-10-05 04:27:58 +00:00
|
|
|
err = page.AppendContentStream(contentstr)
|
2018-08-08 13:12:56 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Add clever function to merge Resources, renaming and modifying contentstream if conflicts.
|
|
|
|
// Could be based on similar functionality already available in creator, perhaps refactored to an
|
|
|
|
// internal utility package, so can be accessed widely.
|
|
|
|
if xform.Resources != nil {
|
|
|
|
xfontDict, has := core.GetDict(xform.Resources.Font)
|
|
|
|
if has {
|
|
|
|
for _, fname := range xfontDict.Keys() {
|
|
|
|
// Only set if no matching font in page resources.
|
|
|
|
if !page.Resources.HasFontByName(fname) {
|
|
|
|
page.Resources.SetFontByName(fname, xfontDict.Get(fname))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove reference to flattened annotations.
|
|
|
|
if len(annots) > 0 {
|
2019-04-14 22:22:41 +00:00
|
|
|
page.annotations = annots
|
2018-08-08 13:12:56 +00:00
|
|
|
} else {
|
2019-06-25 22:29:03 +03:00
|
|
|
page.annotations = []*PdfAnnotation{}
|
2018-08-08 13:12:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-09 21:25:30 +02:00
|
|
|
r.AcroForm = nil
|
2018-08-08 13:12:56 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getAnnotationActiveAppearance retrieves the active XObject Form for an appearance dictionary.
|
|
|
|
// Default gets the N entry, and if it is a dictionary, picks the entry referred to by AS.
|
|
|
|
// If returned XObject Form is nil (and no errors) it indicates that the annotation has no appearance.
|
|
|
|
func getAnnotationActiveAppearance(annot *PdfAnnotation) (*XObjectForm, *PdfRectangle, error) {
|
|
|
|
// For debugging:
|
|
|
|
//common.Log.Trace("----")
|
|
|
|
//common.Log.Trace("annot: %#v", annot)
|
|
|
|
//common.Log.Trace("context: %#v", annot.GetContext())
|
|
|
|
//common.Log.Trace("obj: %v", annot.GetContainingPdfObject())
|
|
|
|
|
|
|
|
// Appearance dictionary entries (Table 168 p. 397).
|
|
|
|
apDict, has := core.GetDict(annot.AP)
|
|
|
|
if !has {
|
|
|
|
return nil, nil, errors.New("field missing AP dictionary")
|
|
|
|
}
|
2018-09-28 09:50:48 +00:00
|
|
|
if apDict == nil {
|
|
|
|
return nil, nil, nil
|
|
|
|
}
|
2018-08-08 13:12:56 +00:00
|
|
|
|
|
|
|
// Get the Rect specifying the display rectangle.
|
|
|
|
rectArr, has := core.GetArray(annot.Rect)
|
|
|
|
if !has || rectArr.Len() != 4 {
|
|
|
|
return nil, nil, errors.New("rect invalid")
|
|
|
|
}
|
|
|
|
rect, err := NewPdfRectangle(*rectArr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
nobj := core.TraceToDirectObject(apDict.Get("N"))
|
|
|
|
switch t := nobj.(type) {
|
|
|
|
case *core.PdfObjectStream:
|
|
|
|
stream := t
|
|
|
|
xform, err := NewXObjectFormFromStream(stream)
|
|
|
|
return xform, rect, err
|
|
|
|
case *core.PdfObjectDictionary:
|
|
|
|
// An annotation representing multiple fields may have many appearances.
|
|
|
|
// As an example checkbox may have two appearance states On and Off.
|
|
|
|
// Its appearance dictionary would contain /N << /On Ref /Off Ref >>, the choice is
|
|
|
|
// determines by the AS entry in the annotation dictionary.
|
|
|
|
nDict := t
|
|
|
|
|
|
|
|
state, has := core.GetName(annot.AS)
|
|
|
|
if !has {
|
|
|
|
// No appearance (nil).
|
|
|
|
return nil, nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if nDict.Get(*state) == nil {
|
|
|
|
common.Log.Debug("ERROR: AS state not specified in AP dict - ignoring")
|
|
|
|
return nil, nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
stream, has := core.GetStream(nDict.Get(*state))
|
|
|
|
if !has {
|
|
|
|
common.Log.Debug("ERROR: Unable to access appearance stream for %v", state)
|
|
|
|
return nil, nil, errors.New("stream missing")
|
|
|
|
}
|
|
|
|
xform, err := NewXObjectFormFromStream(stream)
|
|
|
|
return xform, rect, err
|
|
|
|
}
|
|
|
|
|
|
|
|
common.Log.Debug("Invalid type for N: %T", nobj)
|
|
|
|
return nil, nil, errors.New("type check error")
|
|
|
|
}
|