unipdf/pdf/model/fields.go
2018-07-04 23:02:34 +00:00

756 lines
21 KiB
Go

/*
* 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"
"errors"
"fmt"
"strings"
"github.com/unidoc/unidoc/common"
"github.com/unidoc/unidoc/pdf/core"
)
// FieldFlag represents form field flags. Some of the flags can apply to all types of fields whereas other
// flags are specific.
type FieldFlag uint32
// The following constants define bitwise flag representing different attributes of a form field.
const (
// FieldFlagClear has no flags.
FieldFlagClear FieldFlag = 0
// Flags for all field types.
FieldFlagReadOnly FieldFlag = 1
FieldFlagRequired FieldFlag = (1 << 1)
FieldFlagNoExport FieldFlag = (2 << 1)
// Flags for button fields only.
FieldFlagNoToggleToOff FieldFlag = (1 << 14)
FieldFlagRadio FieldFlag = (1 << 15)
FieldFlagPushbutton FieldFlag = (1 << 16)
FieldFlagRadiosInUnision FieldFlag = (1 << 25)
// Flags for text fields only.
FieldFlagMultiline FieldFlag = (1 << 12)
FieldFlagPassword FieldFlag = (1 << 13)
FieldFlagFileSelect FieldFlag = (1 << 20)
FieldFlagDoNotScroll FieldFlag = (1 << 23)
FieldFlagComb FieldFlag = (1 << 24)
FieldFlagRichText FieldFlag = (1 << 25)
// Flags for text and choice fields.
FieldFlagDoNotSpellCheck FieldFlag = (1 << 22)
// Flags for choice fields only.
FieldFlagCombo FieldFlag = (1 << 17)
FieldFlagEdit FieldFlag = (1 << 18)
FieldFlagSort FieldFlag = (1 << 19)
FieldFlagMultiSelect FieldFlag = (1 << 21)
FieldFlagCommitOnSelChange FieldFlag = (1 << 26)
)
// Mask returns the uin32 bitmask for the specific flag.
func (flag FieldFlag) Mask() uint32 {
return uint32(flag)
}
// Set applies flag fl to the flag's bitmask and returns the combined flag.
func (flag FieldFlag) Set(fl FieldFlag) FieldFlag {
return FieldFlag(flag.Mask() | fl.Mask())
}
// Clear clears flag fl from the flag and returns the resulting flag.
func (flag FieldFlag) Clear(fl FieldFlag) FieldFlag {
return FieldFlag(flag.Mask() &^ fl.Mask())
}
// Has checks if flag fl is set in flag and returns true if so, false otherwise.
func (flag FieldFlag) Has(fl FieldFlag) bool {
return (flag.Mask() & fl.Mask()) > 0
}
// String returns a string representation of what flags are set.
func (flag FieldFlag) String() string {
s := ""
if flag == FieldFlagClear {
s = "Clear"
return s
}
if flag&FieldFlagReadOnly > 0 {
s += "|ReadOnly"
}
if flag&FieldFlagRequired > 0 {
s += "|ReadOnly"
}
if flag&FieldFlagNoExport > 0 {
s += "|NoExport"
}
if flag&FieldFlagNoToggleToOff > 0 {
s += "|NoToggleToOff"
}
if flag&FieldFlagRadio > 0 {
s += "|Radio"
}
if flag&FieldFlagPushbutton > 0 {
s += "|Pushbutton"
}
if flag&FieldFlagRadiosInUnision > 0 {
s += "|RadiosInUnision"
}
if flag&FieldFlagMultiline > 0 {
s += "|Multiline"
}
if flag&FieldFlagPassword > 0 {
s += "|Password"
}
if flag&FieldFlagFileSelect > 0 {
s += "|FileSelect"
}
if flag&FieldFlagDoNotScroll > 0 {
s += "|DoNotScroll"
}
if flag&FieldFlagComb > 0 {
s += "|Comb"
}
if flag&FieldFlagRichText > 0 {
s += "|RichText"
}
if flag&FieldFlagDoNotSpellCheck > 0 {
s += "|DoNotSpellCheck"
}
if flag&FieldFlagCombo > 0 {
s += "|Combo"
}
if flag&FieldFlagEdit > 0 {
s += "|Edit"
}
if flag&FieldFlagSort > 0 {
s += "|Sort"
}
if flag&FieldFlagMultiSelect > 0 {
s += "|MultiSelect"
}
if flag&FieldFlagCommitOnSelChange > 0 {
s += "|CommitOnSelChange"
}
return strings.Trim(s, "|")
}
// PdfField contains the common attributes of a form field. The context object contains the specific field data
// which can represent a button, text, choice or signature.
// The PdfField is typically not used directly, but is encapsulated by the more specific field types such as
// PdfFieldButton etc (i.e. the context attribute).
type PdfField struct {
context PdfModel // Field data
container *core.PdfIndirectObject // Dictionary information stored inside an indirect object.
isTerminal *bool // If set: indicates whether is a terminal field (if null, may not be determined yet).
Parent *PdfField
Annotations []*PdfAnnotationWidget
Kids []*PdfField
FT *core.PdfObjectName
T *core.PdfObjectString
TU *core.PdfObjectString
TM *core.PdfObjectString
Ff *core.PdfObjectInteger
V core.PdfObject
DV core.PdfObject
AA core.PdfObject
}
// FullName returns the full name of the field as in rootname.parentname.partialname.
func (f *PdfField) FullName() (string, error) {
var fn bytes.Buffer
parts := []string{}
if f.T != nil {
parts = append(parts, string(*f.T))
} else {
return fn.String(), errors.New("Field partial name (T) not specified")
}
// Avoid recursive loops by having a list of already traversed nodes.
noscanMap := map[*PdfField]bool{}
noscanMap[f] = true
parent := f.Parent
for parent != nil {
if _, has := noscanMap[parent]; has {
return fn.String(), errors.New("Recursive traversal")
}
if parent.T != nil {
parts = append(parts, string(*f.T))
} else {
return fn.String(), errors.New("Field partial name (T) not specified")
}
noscanMap[parent] = true
parent = parent.Parent
}
for i := len(parts) - 1; i >= 0; i-- {
fn.WriteString(parts[i])
if i > 0 {
fn.WriteString(".")
}
}
return fn.String(), nil
}
// PartialName returns the partial name of the field.
func (f *PdfField) PartialName() string {
partial := ""
if f.T != nil {
partial = string(*f.T)
} else {
common.Log.Debug("Field missing T field (incompatible)")
}
return partial
}
// GetContext returns the PdfField context which is the more specific field data type, e.g. PdfFieldButton
// for a button field.
func (f *PdfField) GetContext() PdfModel {
return f.context
}
// SetContext sets the specific fielddata type, e.g. would be PdfFieldButton for a button field.
func (f *PdfField) SetContext(ctx PdfModel) {
f.context = ctx
}
// GetContainingPdfObject returns the containing object for the PdfField, i.e. an indirect object
// containing the field dictionary.
func (f *PdfField) GetContainingPdfObject() core.PdfObject {
return f.container
}
// String returns a string representation of the field.
func (f *PdfField) String() string {
if obj, ok := f.ToPdfObject().(*core.PdfIndirectObject); ok {
return fmt.Sprintf("%T: %s", f.context, obj.PdfObject.String())
}
return ""
}
// ToPdfObject sets the common field elements.
// Note: Call the more field context's ToPdfObject to set both the generic and non-generic information.
func (f *PdfField) ToPdfObject() core.PdfObject {
container := f.container
d := container.PdfObject.(*core.PdfObjectDictionary)
d.SetIfNotNil("FT", f.FT)
if f.Parent != nil {
d.Set("Parent", f.Parent.GetContainingPdfObject())
}
if f.Kids != nil {
// Create an array of the kids (fields or widgets).
kids := core.MakeArray()
for _, child := range f.Kids {
kids.Append(child.ToPdfObject())
}
d.Set("Kids", kids)
}
if f.Annotations != nil {
_, hasKids := d.Get("Kids").(*core.PdfObjectArray)
if !hasKids {
d.Set("Kids", &core.PdfObjectArray{})
}
// TODO: If only 1 widget annotation, it can be merged in.
kids := d.Get("Kids").(*core.PdfObjectArray)
for _, annot := range f.Annotations {
kids.Append(annot.GetContext().ToPdfObject())
}
}
d.SetIfNotNil("T", f.T)
d.SetIfNotNil("TU", f.TU)
d.SetIfNotNil("TM", f.TM)
d.SetIfNotNil("Ff", f.Ff)
if f.V != nil {
d.Set("V", f.V)
}
if f.DV != nil {
d.Set("DV", f.DV)
}
if f.AA != nil {
d.Set("AA", f.DV)
}
return container
}
// PdfFieldButton represents a button field which includes push buttons, checkboxes, and radio buttons.
type PdfFieldButton struct {
*PdfField
Opt *core.PdfObjectArray
}
// ButtonType represents the subtype of a button field, can be one of:
// - Checkbox (ButtonTypeCheckbox)
// - PushButton (ButtonTypePushButton)
// - RadioButton (ButtonTypeRadioButton)
type ButtonType int
// Definitions for field button types
const (
ButtonTypeCheckbox ButtonType = iota
ButtonTypePush ButtonType = iota
ButtonTypeRadio ButtonType = iota
)
// GetType returns the button field type which returns one of the following
// - PdfFieldButtonPush for push button fields
// - PdfFieldButtonCheckbox for checkbox fields
// - PdfFieldButtonRadio for radio button fields
func (fb *PdfFieldButton) GetType() ButtonType {
btype := ButtonTypeCheckbox
if fb.Ff != nil {
if (uint32(*fb.Ff) & FieldFlagPushbutton.Mask()) > 0 {
btype = ButtonTypePush
} else if (uint32(*fb.Ff) & FieldFlagRadio.Mask()) > 0 {
btype = ButtonTypeRadio
}
}
return btype
}
// IsPush returns true if the button field represents a push button, false otherwise.
func (fb *PdfFieldButton) IsPush() bool {
return fb.GetType() == ButtonTypePush
}
// IsCheckbox returns true if the button field represents a checkbox, false otherwise.
func (fb *PdfFieldButton) IsCheckbox() bool {
return fb.GetType() == ButtonTypeCheckbox
}
// IsRadio returns true if the button field represents a radio button, false otherwise.
func (fb *PdfFieldButton) IsRadio() bool {
return fb.GetType() == ButtonTypeRadio
}
// SetType sets the field button's type. Can be one of:
// - PdfFieldButtonPush for push button fields
// - PdfFieldButtonCheckbox for checkbox fields
// - PdfFieldButtonRadio for radio button fields
// This sets the field's flag appropriately.
func (fb *PdfFieldButton) SetType(btype ButtonType) {
flag := uint32(0)
if fb.Ff != nil {
flag = uint32(*fb.Ff)
}
switch btype {
case ButtonTypePush:
flag |= FieldFlagPushbutton.Mask()
case ButtonTypeRadio:
flag |= FieldFlagRadio.Mask()
}
fb.Ff = core.MakeInteger(int64(flag))
}
// ToPdfObject returns the button field dictionary within an indirect object.
func (fb *PdfFieldButton) ToPdfObject() core.PdfObject {
fb.PdfField.ToPdfObject()
container := fb.container
d := container.PdfObject.(*core.PdfObjectDictionary)
d.Set("FT", core.MakeName("Btn"))
if fb.Opt != nil {
d.Set("Opt", fb.Opt)
}
return container
}
// PdfFieldText represents a text field where user can enter text.
type PdfFieldText struct {
*PdfField
DA *core.PdfObjectString
Q *core.PdfObjectInteger
DS *core.PdfObjectString
RV core.PdfObject
MaxLen *core.PdfObjectInteger
}
// ToPdfObject returns the text field dictionary within an indirect object (container).
func (ft *PdfFieldText) ToPdfObject() core.PdfObject {
ft.PdfField.ToPdfObject()
container := ft.container
d := container.PdfObject.(*core.PdfObjectDictionary)
d.Set("FT", core.MakeName("Tx"))
if ft.DA != nil {
d.Set("DA", ft.DA)
}
if ft.Q != nil {
d.Set("Q", ft.Q)
}
if ft.DS != nil {
d.Set("DS", ft.DS)
}
if ft.RV != nil {
d.Set("RV", ft.RV)
}
if ft.MaxLen != nil {
d.Set("MaxLen", ft.MaxLen)
}
return container
}
// PdfFieldChoice represents a choice field which includes scrollable list boxes and combo boxes.
type PdfFieldChoice struct {
*PdfField
Opt *core.PdfObjectArray
TI *core.PdfObjectInteger
I *core.PdfObjectArray
}
// ToPdfObject returns the choice field dictionary within an indirect object (container).
func (ch *PdfFieldChoice) ToPdfObject() core.PdfObject {
// Set general field attributes
ch.PdfField.ToPdfObject()
container := ch.container
// Handle choice specific attributes
d := container.PdfObject.(*core.PdfObjectDictionary)
d.Set("FT", core.MakeName("Ch"))
if ch.Opt != nil {
d.Set("Opt", ch.Opt)
}
if ch.TI != nil {
d.Set("TI", ch.TI)
}
if ch.I != nil {
d.Set("I", ch.I)
}
return container
}
// PdfFieldSignature signature field represents digital signatures and optional data for authenticating
// the name of the signer and verifying document contents.
type PdfFieldSignature struct {
*PdfField
V *core.PdfIndirectObject
Lock *core.PdfIndirectObject
SV *core.PdfIndirectObject
}
// ToPdfObject returns an indirect object containing the signature field dictionary.
func (sig *PdfFieldSignature) ToPdfObject() core.PdfObject {
// Set general field attributes
sig.PdfField.ToPdfObject()
container := sig.container
// Handle signature field specific attributes
d := container.PdfObject.(*core.PdfObjectDictionary)
d.Set("FT", core.MakeName("Sig"))
if sig.V != nil {
d.Set("V", sig.V)
}
if sig.Lock != nil {
d.Set("Lock", sig.Lock)
}
if sig.SV != nil {
d.Set("SV", sig.SV)
}
return container
}
// NewPdfField returns an initialized PdfField.
func NewPdfField() *PdfField {
field := &PdfField{}
container := core.MakeIndirectObject(core.MakeDict())
field.container = container
return field
}
// inherit traverses through a field and its ancestry for calculation of inherited attributes. The process is
// down-up with lower nodes being assessed first. Typically values are inherited as-is (without merging), so
// the first non-nil entry is generally the one to use.
// The custom provided eval function evaluates field nodes and returns true once a match was found.
// A bool flag is returned to indicate whether there was a match.
func (f *PdfField) inherit(eval func(*PdfField) bool) (bool, error) {
nodeMap := map[*PdfField]bool{}
found := false
// Traverse from the node up to the root.
node := f
for node != nil {
if _, has := nodeMap[node]; has {
return false, errors.New("Recursive traversal")
}
stop := eval(node)
if stop {
found = true
break
}
nodeMap[node] = true
node = node.Parent
}
return found, nil
}
// IsTerminal returns true for terminal fields, false otherwise.
// Terminal fields are fields whose descendants are only widget annotations.
func (f *PdfField) IsTerminal() bool {
if len(f.Kids) == 0 {
return true
}
return false
}
// Flags returns the field flags for the field accounting for any inherited flags.
func (f *PdfField) Flags() FieldFlag {
var flags FieldFlag
found, err := f.inherit(func(node *PdfField) bool {
if node.Ff != nil {
flags = FieldFlag(*f.Ff)
return true
}
return false
})
if err != nil {
common.Log.Debug("Error evaluating flags via inheritance: %v", err)
}
if !found {
common.Log.Trace("No field flags found - assume clear")
}
return flags
}
// 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")
}
field := NewPdfField()
// Field type (required in terminal fields).
// Can be /Btn /Tx /Ch /Sig
// Required for a terminal field (inheritable).
if obj := d.GetDirect("FT"); obj != nil {
name, ok := obj.(*core.PdfObjectName)
if !ok {
return nil, fmt.Errorf("Invalid type of FT field (%T)", obj)
}
field.FT = name
isTerminal := true
field.isTerminal = &isTerminal
} else {
isTerminal := false
field.isTerminal = &isTerminal
}
// Non-terminal field: One whose descendants are fields. Not an actual field, just a container for inheritable
// attributes for descendant terminal fields. Does not logically have a type of its own.
// Terminal field: An actual field with a type.
// Partial field name (Required)
if s, has := d.Get("T").(*core.PdfObjectString); has {
field.T = s
} else {
common.Log.Debug("Invalid - T field missing (required)")
return nil, ErrTypeCheck
}
// Alternate description (Optional)
field.TU, _ = d.Get("TU").(*core.PdfObjectString)
// Mapping name (Optional)
field.TM, _ = d.Get("TM").(*core.PdfObjectString)
// Field flag. (Optional; inheritable)
field.Ff, _ = d.Get("Ff").(*core.PdfObjectInteger)
// Value (Optional; inheritable) - Various types depending on the field type.
field.V = d.Get("V")
// Default value for reset (Optional; inheritable)
field.DV = d.Get("DV")
// Additional actions dictionary (Optional)
field.AA = d.Get("AA")
// Type specific types.
if field.FT != nil {
switch *field.FT {
case "Tx":
ctx, err := newPdfFieldTextFromDict(d)
if err != nil {
return nil, err
}
ctx.PdfField = field
field.context = ctx
case "Ch":
ctx, err := newPdfFieldChoiceFromDict(d)
if err != nil {
return nil, err
}
ctx.PdfField = field
field.context = ctx
case "Btn":
ctx, err := newPdfFieldButtonFromDict(d)
if err != nil {
return nil, err
}
ctx.PdfField = field
field.context = ctx
case "Sig":
ctx, err := newPdfFieldSignatureFromDict(d)
if err != nil {
return nil, err
}
ctx.PdfField = field
field.context = ctx
default:
common.Log.Debug("Unsupported field type %s", *field.FT)
return nil, errors.New("Unsupported field type")
}
}
// Type specific:
// In a non-terminal field, the Kids array shall refer to field dictionaries that are immediate descendants of this field.
// In a terminal field, the Kids array ordinarily shall refer to one or more separate widget annotations that are associated
// with this field. However, if there is only one associated widget annotation, and its contents have been merged into the field
// dictionary, Kids shall be omitted.
// Set ourself?
if parent != nil {
field.Parent = parent
}
field.Annotations = []*PdfAnnotationWidget{}
// Has a merged-in widget annotation?
if name, _ := d.GetDirect("Subtype").(*core.PdfObjectName); name != nil {
if *name == "Widget" {
// Is a merged field / widget dict.
// Note that r.newPdfAnnotationFromIndirectObject acts as a caching mechanism if the annotation
// has been loaded elsewhere already.
annot, err := r.newPdfAnnotationFromIndirectObject(container)
if err != nil {
return nil, err
}
widget, ok := annot.GetContext().(*PdfAnnotationWidget)
if !ok {
return nil, fmt.Errorf("Invalid widget annotation")
}
widget.Parent = field.GetContainingPdfObject()
field.Annotations = append(field.Annotations, widget)
return field, nil
}
}
// Kids can be field and/or widget annotations.
if kids, has := d.GetDirect("Kids").(*core.PdfObjectArray); has {
field.Kids = []*PdfField{}
for _, obj := range *kids {
obj, err := r.traceToObject(obj)
if err != nil {
return nil, err
}
container, isIndirect := obj.(*core.PdfIndirectObject)
if !isIndirect {
return nil, fmt.Errorf("Not an indirect object (form field)")
}
dict, ok := container.Direct().(*core.PdfObjectDictionary)
if !ok {
return nil, ErrTypeCheck
}
// Widget annotations contain key Subtype with value equal to /Widget. Otherwise are assumed to be fields.
if name, has := dict.GetDirect("Subtype").(*core.PdfObjectName); has && *name == "Widget" {
annot, err := r.newPdfAnnotationFromIndirectObject(container)
if err != nil {
common.Log.Debug("Error loading widget annotation for field: %v", err)
return nil, err
}
wa, ok := annot.context.(*PdfAnnotationWidget)
if !ok {
return nil, ErrTypeCheck
}
field.Annotations = append(field.Annotations, wa)
} else {
childf, err := r.newPdfFieldFromIndirectObject(container, field)
if err != nil {
common.Log.Debug("Error loading child field: %v", err)
return nil, err
}
field.Kids = append(field.Kids, childf)
}
}
}
return field, nil
}
// newPdfFieldTextFromDict returns a new PdfFieldText (representing a variable text field) loaded from a dictionary.
// This function loads only text-field specific fields (called by a more generic field loader).
func newPdfFieldTextFromDict(d *core.PdfObjectDictionary) (*PdfFieldText, error) {
textf := &PdfFieldText{}
textf.DA, _ = d.GetDirect("DA").(*core.PdfObjectString)
textf.Q, _ = d.GetDirect("Q").(*core.PdfObjectInteger)
textf.DS, _ = d.GetDirect("DS").(*core.PdfObjectString)
textf.RV = d.Get("RV")
// TODO: MaxLen should be loaded for other fields too?
textf.MaxLen, _ = d.Get("MaxLen").(*core.PdfObjectInteger)
return textf, nil
}
// newPdfFieldChoiceFromDict returns a new PdfFieldChoice (representing a choice field) loaded from a dictionary.
// This function loads only choice-field specific fields (called by a more generic field loader).
func newPdfFieldChoiceFromDict(d *core.PdfObjectDictionary) (*PdfFieldChoice, error) {
choicef := &PdfFieldChoice{}
choicef.Opt, _ = d.GetDirect("Opt").(*core.PdfObjectArray)
choicef.TI, _ = d.GetDirect("TI").(*core.PdfObjectInteger)
choicef.I, _ = d.GetDirect("I").(*core.PdfObjectArray)
return choicef, nil
}
// newPdfFieldButtonFromDict returns a new PdfFieldButton (representing a button field) loaded from a dictionary.
// This function loads only button-field specific fields (called by a more generic field loader).
func newPdfFieldButtonFromDict(d *core.PdfObjectDictionary) (*PdfFieldButton, error) {
buttonf := &PdfFieldButton{}
buttonf.Opt, _ = d.GetDirect("Opt").(*core.PdfObjectArray)
return buttonf, nil
}
// newPdfFieldSignatureFromDict returns a new PdfFieldSignature (representing a signature field) loaded from a dictionary.
// This function loads only the signature-specific fields (called by a more generic field loader).
func newPdfFieldSignatureFromDict(d *core.PdfObjectDictionary) (*PdfFieldSignature, error) {
sigf := &PdfFieldSignature{}
sigf.V, _ = d.Get("V").(*core.PdfIndirectObject)
sigf.Lock, _ = d.Get("Lock").(*core.PdfIndirectObject)
sigf.SV, _ = d.Get("SV").(*core.PdfIndirectObject)
return sigf, nil
}