import PDFDocument from 'src/api/PDFDocument' ; import PDFPage from 'src/api/PDFPage' ; import PDFField from 'src/api/form/PDFField' ; import PDFButton from 'src/api/form/PDFButton' ; import PDFCheckBox from 'src/api/form/PDFCheckBox' ; import PDFDropdown from 'src/api/form/PDFDropdown' ; import PDFOptionList from 'src/api/form/PDFOptionList' ; import PDFRadioGroup from 'src/api/form/PDFRadioGroup' ; import PDFSignature from 'src/api/form/PDFSignature' ; import PDFTextField from 'src/api/form/PDFTextField' ; import { NoSuchFieldError , UnexpectedFieldTypeError , FieldAlreadyExistsError , InvalidFieldNamePartError , } from 'src/api/errors' ; import PDFFont from 'src/api/PDFFont' ; import { StandardFonts } from 'src/api/StandardFonts' ; import { rotateInPlace } from 'src/api/operations' ; import { drawObject , popGraphicsState , pushGraphicsState , translate , } from 'src/api/operators' ; import { PDFAcroForm , PDFAcroField , PDFAcroCheckBox , PDFAcroComboBox , PDFAcroListBox , PDFAcroRadioButton , PDFAcroSignature , PDFAcroText , PDFAcroPushButton , PDFAcroNonTerminal , PDFDict , PDFOperator , PDFRef , createPDFAcroFields , PDFName , PDFWidgetAnnotation , } from 'src/core' ; import { assertIs , Cache , assertOrUndefined } from 'src/utils' ; export interface FlattenOptions { updateFieldAppearances : boolean ; } /**
 * Represents the interactive form of a [[PDFDocument]].
 *
 * Interactive forms (sometimes called _AcroForms_) are collections of fields
 * designed to gather information from a user. A PDF document may contains any
 * number of fields that appear on various pages, all of which make up a single,
 * global interactive form spanning the entire document. This means that
 * instances of [[PDFDocument]] shall contain at most one [[PDFForm]].
 *
 * The fields of an interactive form are represented by [[PDFField]] instances.
 */ export default class PDFForm { param acroForm param doc static of = ( acroForm , doc ) => new PDFForm ( acroForm , doc ) ; /** The low-level PDFAcroForm wrapped by this form. */ readonly acroForm : PDFAcroForm ; /** The document to which this form belongs. */ readonly doc : PDFDocument ; private readonly dirtyFields : Set < PDFRef > ; private readonly defaultFontCache : Cache < PDFFont > ; private constructor ( acroForm , doc ) { assertIs ( acroForm , 'acroForm' , [ [ PDFAcroForm , 'PDFAcroForm' ] ] ) ; assertIs ( doc , 'doc' , [ [ PDFDocument , 'PDFDocument' ] ] ) ; this . acroForm = acroForm ; this . doc = doc ; this . dirtyFields = new Set ( ) ; this . defaultFontCache = Cache . populatedBy ( this . embedDefaultFont ) ; } returns hasXFA ( ) { return this . acroForm . dict . has ( PDFName . of ( 'XFA' ) ) ; } /**
     * Disconnect the XFA data from this [[PDFForm]] (if any exists). This will
     * force readers to fallback to standard fields if the [[PDFDocument]]
     * contains any. For example:
     *
     * For example:
     * ```js
     * const form = pdfDoc.getForm()
     * form.deleteXFA()
     * ```
     */ deleteXFA ( ) { this . acroForm . dict . delete ( PDFName . of ( 'XFA' ) ) ; } returns getFields ( ) { const allFields = this . acroForm . getAllFields ( ) ; const fields = [ ] ; for ( let idx = 0 , len = allFields . length ; idx < len ; idx ++ ) { const [ acroField , ref ] = allFields [ idx ] ; const field = convertToPDFField ( acroField , ref , this . doc ) ; if ( field ) fields . push ( field ) ; } return fields ; } param name returns getFieldMaybe ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const fields = this . getFields ( ) ; for ( let idx = 0 , len = fields . length ; idx < len ; idx ++ ) { const field = fields [ idx ] ; if ( field . getName ( ) === name ) return field ; } return undefined ; } param name returns getField ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const field = this . getFieldMaybe ( name ) ; if ( field ) return field ; throw new NoSuchFieldError ( name ) ; } param name returns getButton ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const field = this . getField ( name ) ; if ( field instanceof PDFButton ) return field ; throw new UnexpectedFieldTypeError ( name , PDFButton , field ) ; } param name returns getCheckBox ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const field = this . getField ( name ) ; if ( field instanceof PDFCheckBox ) return field ; throw new UnexpectedFieldTypeError ( name , PDFCheckBox , field ) ; } param name returns getDropdown ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const field = this . getField ( name ) ; if ( field instanceof PDFDropdown ) return field ; throw new UnexpectedFieldTypeError ( name , PDFDropdown , field ) ; } param name returns getOptionList ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const field = this . getField ( name ) ; if ( field instanceof PDFOptionList ) return field ; throw new UnexpectedFieldTypeError ( name , PDFOptionList , field ) ; } param name returns getRadioGroup ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const field = this . getField ( name ) ; if ( field instanceof PDFRadioGroup ) return field ; throw new UnexpectedFieldTypeError ( name , PDFRadioGroup , field ) ; } param name returns getSignature ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const field = this . getField ( name ) ; if ( field instanceof PDFSignature ) return field ; throw new UnexpectedFieldTypeError ( name , PDFSignature , field ) ; } param name returns getTextField ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const field = this . getField ( name ) ; if ( field instanceof PDFTextField ) return field ; throw new UnexpectedFieldTypeError ( name , PDFTextField , field ) ; } param name returns createButton ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const nameParts = splitFieldName ( name ) ; const parent = this . findOrCreateNonTerminals ( nameParts . nonTerminal ) ; const button = PDFAcroPushButton . create ( this . doc . context ) ; button . setPartialName ( nameParts . terminal ) ; addFieldToParent ( parent , [ button , button . ref ] , nameParts . terminal ) ; return PDFButton . of ( button , button . ref , this . doc ) ; } param name returns createCheckBox ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const nameParts = splitFieldName ( name ) ; const parent = this . findOrCreateNonTerminals ( nameParts . nonTerminal ) ; const checkBox = PDFAcroCheckBox . create ( this . doc . context ) ; checkBox . setPartialName ( nameParts . terminal ) ; addFieldToParent ( parent , [ checkBox , checkBox . ref ] , nameParts . terminal ) ; return PDFCheckBox . of ( checkBox , checkBox . ref , this . doc ) ; } param name returns createDropdown ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const nameParts = splitFieldName ( name ) ; const parent = this . findOrCreateNonTerminals ( nameParts . nonTerminal ) ; const comboBox = PDFAcroComboBox . create ( this . doc . context ) ; comboBox . setPartialName ( nameParts . terminal ) ; addFieldToParent ( parent , [ comboBox , comboBox . ref ] , nameParts . terminal ) ; return PDFDropdown . of ( comboBox , comboBox . ref , this . doc ) ; } param name returns createOptionList ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const nameParts = splitFieldName ( name ) ; const parent = this . findOrCreateNonTerminals ( nameParts . nonTerminal ) ; const listBox = PDFAcroListBox . create ( this . doc . context ) ; listBox . setPartialName ( nameParts . terminal ) ; addFieldToParent ( parent , [ listBox , listBox . ref ] , nameParts . terminal ) ; return PDFOptionList . of ( listBox , listBox . ref , this . doc ) ; } param name returns createRadioGroup ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const nameParts = splitFieldName ( name ) ; const parent = this . findOrCreateNonTerminals ( nameParts . nonTerminal ) ; const radioButton = PDFAcroRadioButton . create ( this . doc . context ) ; radioButton . setPartialName ( nameParts . terminal ) ; addFieldToParent ( parent , [ radioButton , radioButton . ref ] , nameParts . terminal ) ; return PDFRadioGroup . of ( radioButton , radioButton . ref , this . doc ) ; } param name returns createTextField ( name ) { assertIs ( name , 'name' , [ 'string' ] ) ; const nameParts = splitFieldName ( name ) ; const parent = this . findOrCreateNonTerminals ( nameParts . nonTerminal ) ; const text = PDFAcroText . create ( this . doc . context ) ; text . setPartialName ( nameParts . terminal ) ; addFieldToParent ( parent , [ text , text . ref ] , nameParts . terminal ) ; return PDFTextField . of ( text , text . ref , this . doc ) ; } /**
     * Flatten all fields in this [[PDFForm]].
     *
     * Flattening a form field will take the current appearance for each of that
     * field's widgets and make them part of their page's content stream. All form
     * fields and annotations associated are then removed. Note that once a form
     * has been flattened its fields can no longer be accessed or edited.
     *
     * This operation is often used after filling form fields to ensure a
     * consistent appearance across different PDF readers and/or printers.
     * Another common use case is to copy a template document with form fields
     * into another document. In this scenario you would load the template
     * document, fill its fields, flatten it, and then copy its pages into the
     * recipient document - the filled fields will be copied over.
     *
     * For example:
     * ```js
     * const form = pdfDoc.getForm();
     * form.flatten();
     * ```
     */ flatten ( options = { updateFieldAppearances : true } ) { if ( options . updateFieldAppearances ) { this . updateFieldAppearances ( ) ; } const fields = this . getFields ( ) ; for ( let i = 0 , lenFields = fields . length ; i < lenFields ; i ++ ) { const field = fields [ i ] ; const widgets = field . acroField . getWidgets ( ) ; for ( let j = 0 , lenWidgets = widgets . length ; j < lenWidgets ; j ++ ) { const widget = widgets [ j ] ; const page = this . findWidgetPage ( widget ) ; const widgetRef = this . findWidgetAppearanceRef ( field , widget ) ; const xObjectKey = page . node . newXObject ( 'FlatWidget' , widgetRef ) ; const rectangle = widget . getRectangle ( ) ; const operators = [ pushGraphicsState ( ) , translate ( rectangle . x , rectangle . y ) , ... rotateInPlace ( { ... rectangle , rotation : 0 } ) , drawObject ( xObjectKey ) , popGraphicsState ( ) , ] . filter ( Boolean ) as PDFOperator [ ] ; page . pushOperators ( ... operators ) ; } this . removeField ( field ) ; } } /**
     * Remove a field from this [[PDFForm]].
     *
     * For example:
     * ```js
     * const form = pdfDoc.getForm();
     * const ageField = form.getFields().find(x => x.getName() === 'Age');
     * form.removeField(ageField);
     * ```
     */ removeField ( field ) { const widgets = field . acroField . getWidgets ( ) ; const pages = new Set ( ) ; for ( let i = 0 , len = widgets . length ; i < len ; i ++ ) { const widget = widgets [ i ] ; const widgetRef = this . findWidgetAppearanceRef ( field , widget ) ; const page = this . findWidgetPage ( widget ) ; pages . add ( page ) ; page . node . removeAnnot ( widgetRef ) ; } pages . forEach ( ( page ) => page . node . removeAnnot ( field . ref ) ) ; this . acroForm . removeField ( field . acroField ) ; const fieldKids = field . acroField . normalizedEntries ( ) . Kids ; const kidsCount = fieldKids . size ( ) ; for ( let childIndex = 0 ; childIndex < kidsCount ; childIndex ++ ) { const child = fieldKids . get ( childIndex ) ; if ( child instanceof PDFRef ) { this . doc . context . delete ( child ) ; } } this . doc . context . delete ( field . ref ) ; } param font updateFieldAppearances ( font ? ) { assertOrUndefined ( font , 'font' , [ [ PDFFont , 'PDFFont' ] ] ) ; font = font ?? this . getDefaultFont ( ) ; const fields = this . getFields ( ) ; for ( let idx = 0 , len = fields . length ; idx < len ; idx ++ ) { const field = fields [ idx ] ; if ( field . needsAppearancesUpdate ( ) ) { field . defaultUpdateAppearances ( font ) ; } } } param fieldRef markFieldAsDirty ( fieldRef ) { assertOrUndefined ( fieldRef , 'fieldRef' , [ [ PDFRef , 'PDFRef' ] ] ) ; this . dirtyFields . add ( fieldRef ) ; } param fieldRef markFieldAsClean ( fieldRef ) { assertOrUndefined ( fieldRef , 'fieldRef' , [ [ PDFRef , 'PDFRef' ] ] ) ; this . dirtyFields . delete ( fieldRef ) ; } param fieldRef returns fieldIsDirty ( fieldRef ) { assertOrUndefined ( fieldRef , 'fieldRef' , [ [ PDFRef , 'PDFRef' ] ] ) ; return this . dirtyFields . has ( fieldRef ) ; } getDefaultFont ( ) { return this . defaultFontCache . access ( ) ; } private findWidgetPage ( widget ) { const pageRef = widget . P ( ) ; let page = this . doc . getPages ( ) . find ( ( x ) => x . ref === pageRef ) ; if ( page === undefined ) { const widgetRef = this . doc . context . getObjectRef ( widget . dict ) ; if ( widgetRef === undefined ) { throw new Error ( 'Could not find PDFRef for PDFObject' ) ; } page = this . doc . findPageForAnnotationRef ( widgetRef ) ; if ( page === undefined ) { throw new Error ( `Could not find page for PDFRef ${ widgetRef }` ) ; } } return page ; } private findWidgetAppearanceRef ( field , widget ) { let refOrDict = widget . getNormalAppearance ( ) ; if ( refOrDict instanceof PDFDict && ( field instanceof PDFCheckBox || field instanceof PDFRadioGroup ) ) { const value = field . acroField . getValue ( ) ; const ref = refOrDict . get ( value ) ?? refOrDict . get ( PDFName . of ( 'Off' ) ) ; if ( ref instanceof PDFRef ) { refOrDict = ref ; } } if ( ! ( refOrDict instanceof PDFRef ) ) { const name = field . getName ( ) ; throw new Error ( `Failed to extract appearance ref for: ${ name }` ) ; } return refOrDict ; } private findOrCreateNonTerminals ( partialNames ) { let nonTerminal = [ this . acroForm , ] ; for ( let idx = 0 , len = partialNames . length ; idx < len ; idx ++ ) { const namePart = partialNames [ idx ] ; if ( ! namePart ) throw new InvalidFieldNamePartError ( namePart ) ; const [ parent , parentRef ] = nonTerminal ; const res = this . findNonTerminal ( namePart , parent ) ; if ( res ) { nonTerminal = res ; } else { const node = PDFAcroNonTerminal . create ( this . doc . context ) ; node . setPartialName ( namePart ) ; node . setParent ( parentRef ) ; const nodeRef = this . doc . context . register ( node . dict ) ; parent . addField ( nodeRef ) ; nonTerminal = [ node , nodeRef ] ; } } return nonTerminal ; } private findNonTerminal ( partialName , parent ) { const fields = parent instanceof PDFAcroForm ? this . acroForm . getFields ( ) : createPDFAcroFields ( parent . Kids ( ) ) ; for ( let idx = 0 , len = fields . length ; idx < len ; idx ++ ) { const [ field , ref ] = fields [ idx ] ; if ( field . getPartialName ( ) === partialName ) { if ( field instanceof PDFAcroNonTerminal ) return [ field , ref ] ; throw new FieldAlreadyExistsError ( partialName ) ; } } return undefined ; } private embedDefaultFont = ( ) : PDFFont => this . doc . embedStandardFont ( StandardFonts . Helvetica ) ; } const convertToPDFField = ( field , ref , doc ) : PDFField | undefined => { if ( field instanceof PDFAcroPushButton ) return PDFButton . of ( field , ref , doc ) ; if ( field instanceof PDFAcroCheckBox ) return PDFCheckBox . of ( field , ref , doc ) ; if ( field instanceof PDFAcroComboBox ) return PDFDropdown . of ( field , ref , doc ) ; if ( field instanceof PDFAcroListBox ) return PDFOptionList . of ( field , ref , doc ) ; if ( field instanceof PDFAcroText ) return PDFTextField . of ( field , ref , doc ) ; if ( field instanceof PDFAcroRadioButton ) { return PDFRadioGroup . of ( field , ref , doc ) ; } if ( field instanceof PDFAcroSignature ) { return PDFSignature . of ( field , ref , doc ) ; } return undefined ; } ; const splitFieldName = ( fullyQualifiedName ) => { if ( fullyQualifiedName . length === 0 ) { throw new Error ( 'PDF field names must not be empty strings' ) ; } const parts = fullyQualifiedName . split ( '.' ) ; for ( let idx = 0 , len = parts . length ; idx < len ; idx ++ ) { if ( parts [ idx ] === '' ) { throw new Error ( `Periods in PDF field names must be separated by at least one character: "${ fullyQualifiedName }"` ) ; } } if ( parts . length === 1 ) return { nonTerminal : [ ] , terminal : parts [ 0 ] } ; return { nonTerminal : parts . slice ( 0 , parts . length - 1 ) , terminal : parts [ parts . length - 1 ] , } ; } ; const addFieldToParent = ( [ parent , parentRef ] , [ field , fieldRef ] , partialName ) => { const entries = parent . normalizedEntries ( ) ; const fields = createPDFAcroFields ( 'Kids' in entries ? entries . Kids : entries . Fields ) ; for ( let idx = 0 , len = fields . length ; idx < len ; idx ++ ) { if ( fields [ idx ] [ 0 ] . getPartialName ( ) === partialName ) { throw new FieldAlreadyExistsError ( partialName ) ; } } parent . addField ( fieldRef ) ; field . setParent ( parentRef ) ; } ;