import PDFDocument from '../PDFDocument'; import PDFPage from '../PDFPage'; import PDFField from './PDFField'; import PDFButton from './PDFButton'; import PDFCheckBox from './PDFCheckBox'; import PDFDropdown from './PDFDropdown'; import PDFOptionList from './PDFOptionList'; import PDFRadioGroup from './PDFRadioGroup'; import PDFSignature from './PDFSignature'; import PDFTextField from './PDFTextField'; import { NoSuchFieldError, UnexpectedFieldTypeError, FieldAlreadyExistsError, InvalidFieldNamePartError, } from '../errors'; import PDFFont from '../PDFFont'; import { StandardFonts } from '../StandardFonts'; import { rotateInPlace } from '../operations'; import { drawObject, popGraphicsState, pushGraphicsState, translate, } from '../operators'; import { PDFAcroForm, PDFAcroField, PDFAcroCheckBox, PDFAcroComboBox, PDFAcroListBox, PDFAcroRadioButton, PDFAcroSignature, PDFAcroText, PDFAcroPushButton, PDFAcroNonTerminal, PDFDict, PDFRef, createPDFAcroFields, PDFName, PDFWidgetAnnotation, } from '../../core'; import { assertIs, Cache, assertOrUndefined } from '../../utils'; import { encode } from 'html-entities'; import { collectXfaScripts, collectXfaSignatures, parseXfaTemplate, readXfaTemplatePacket, } from './xfa'; export interface FlattenOptions { updateFieldAppearances: boolean; } /** * Describes a signature field declared inside an XFA form template. */ export interface XFASignatureField { field: string; manifest: string | null; refs: string[]; } /** * Describes a signature field found in a [[PDFDocument]], regardless of whether * it is declared in the AcroForm `/Fields` array or inside an XFA template. */ export interface SignatureField { name: string; source: 'acroform' | 'xfa'; acroField?: PDFSignature; manifest?: string | null; refs?: string[]; } /** * 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 { /** * > **NOTE:** You probably don't want to call this method directly. Instead, * > consider using the [[PDFDocument.getForm]] method, which will create an * > instance of [[PDFForm]] for you. * * Create an instance of [[PDFForm]] from an existing acroForm and embedder * * @param acroForm The underlying `PDFAcroForm` for this form. * @param doc The document to which the form will belong. */ static of = (acroForm: PDFAcroForm, doc: PDFDocument) => 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; private readonly defaultFontCache: Cache; private constructor(acroForm: PDFAcroForm, doc: PDFDocument) { 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 `true` if this [[PDFForm]] has XFA data. Most PDFs with form * fields do not use XFA as it is not widely supported by PDF readers. * * > `pdf-lib` does not support creation, modification, or reading of XFA * > fields. * * For example: * ```js * const form = pdfDoc.getForm() * if (form.hasXFA()) console.log('PDF has XFA data') * ``` * @returns Whether or not this form has XFA data. */ hasXFA(): boolean { 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(): void { this.acroForm.dict.delete(PDFName.of('XFA')); } /** * Get all fields contained in this [[PDFForm]]. For example: * ```js * const form = pdfDoc.getForm() * const fields = form.getFields() * fields.forEach(field => { * const type = field.constructor.name * const name = field.getName() * console.log(`${type}: ${name}`) * }) * ``` * @returns An array of all fields in this form. */ getFields(): PDFField[] { const allFields = this.acroForm.getAllFields(); const fields: PDFField[] = []; 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; } /** * Get the field in this [[PDFForm]] with the given name. For example: * ```js * const form = pdfDoc.getForm() * const field = form.getFieldMaybe('Page1.Foo.Bar[0]') * if (field) console.log('Field exists!') * ``` * @param name A fully qualified field name. * @returns The field with the specified name, if one exists. */ getFieldMaybe(name: string): PDFField | undefined { 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; } /** * Get the field in this [[PDFForm]] with the given name. For example: * ```js * const form = pdfDoc.getForm() * const field = form.getField('Page1.Foo.Bar[0]') * ``` * If no field exists with the provided name, an error will be thrown. * @param name A fully qualified field name. * @returns The field with the specified name. */ getField(name: string): PDFField { assertIs(name, 'name', ['string']); const field = this.getFieldMaybe(name); if (field) return field; throw new NoSuchFieldError(name); } /** * Get the button field in this [[PDFForm]] with the given name. For example: * ```js * const form = pdfDoc.getForm() * const button = form.getButton('Page1.Foo.Button[0]') * ``` * An error will be thrown if no field exists with the provided name, or if * the field exists but is not a button. * @param name A fully qualified button name. * @returns The button with the specified name. */ getButton(name: string): PDFButton { assertIs(name, 'name', ['string']); const field = this.getField(name); if (field instanceof PDFButton) return field; throw new UnexpectedFieldTypeError(name, PDFButton, field); } /** * Get the check box field in this [[PDFForm]] with the given name. * For example: * ```js * const form = pdfDoc.getForm() * const checkBox = form.getCheckBox('Page1.Foo.CheckBox[0]') * checkBox.check() * ``` * An error will be thrown if no field exists with the provided name, or if * the field exists but is not a check box. * @param name A fully qualified check box name. * @returns The check box with the specified name. */ getCheckBox(name: string): PDFCheckBox { assertIs(name, 'name', ['string']); const field = this.getField(name); if (field instanceof PDFCheckBox) return field; throw new UnexpectedFieldTypeError(name, PDFCheckBox, field); } /** * Get the dropdown field in this [[PDFForm]] with the given name. * For example: * ```js * const form = pdfDoc.getForm() * const dropdown = form.getDropdown('Page1.Foo.Dropdown[0]') * const options = dropdown.getOptions() * dropdown.select(options[0]) * ``` * An error will be thrown if no field exists with the provided name, or if * the field exists but is not a dropdown. * @param name A fully qualified dropdown name. * @returns The dropdown with the specified name. */ getDropdown(name: string): PDFDropdown { assertIs(name, 'name', ['string']); const field = this.getField(name); if (field instanceof PDFDropdown) return field; throw new UnexpectedFieldTypeError(name, PDFDropdown, field); } /** * Get the option list field in this [[PDFForm]] with the given name. * For example: * ```js * const form = pdfDoc.getForm() * const optionList = form.getOptionList('Page1.Foo.OptionList[0]') * const options = optionList.getOptions() * optionList.select(options[0]) * ``` * An error will be thrown if no field exists with the provided name, or if * the field exists but is not an option list. * @param name A fully qualified option list name. * @returns The option list with the specified name. */ getOptionList(name: string): PDFOptionList { assertIs(name, 'name', ['string']); const field = this.getField(name); if (field instanceof PDFOptionList) return field; throw new UnexpectedFieldTypeError(name, PDFOptionList, field); } /** * Get the radio group field in this [[PDFForm]] with the given name. * For example: * ```js * const form = pdfDoc.getForm() * const radioGroup = form.getRadioGroup('Page1.Foo.RadioGroup[0]') * const options = radioGroup.getOptions() * radioGroup.select(options[0]) * ``` * An error will be thrown if no field exists with the provided name, or if * the field exists but is not a radio group. * @param name A fully qualified radio group name. * @returns The radio group with the specified name. */ getRadioGroup(name: string): PDFRadioGroup { assertIs(name, 'name', ['string']); const field = this.getField(name); if (field instanceof PDFRadioGroup) return field; throw new UnexpectedFieldTypeError(name, PDFRadioGroup, field); } /** * Get the signature field in this [[PDFForm]] with the given name. * For example: * ```js * const form = pdfDoc.getForm() * const signature = form.getSignature('Page1.Foo.Signature[0]') * ``` * An error will be thrown if no field exists with the provided name, or if * the field exists but is not a signature. * @param name A fully qualified signature name. * @returns The signature with the specified name. */ getSignature(name: string): PDFSignature { assertIs(name, 'name', ['string']); const field = this.getField(name); if (field instanceof PDFSignature) return field; throw new UnexpectedFieldTypeError(name, PDFSignature, field); } /** * Get the signature fields declared inside this form's XFA template (if any). * * Dynamic XFA forms declare signature fields inside the template XML rather * than in the AcroForm `/Fields` array, so [[PDFForm.getSignature]] cannot * see them. Each signature field carries a `` UI element that * references a `` describing which fields the signature covers (its * FieldMDP scope). This method surfaces that information. * * For example: * ```js * const form = pdfDoc.getForm() * form.getXFASignatures().forEach(({ field, manifest, refs }) => { * console.log(`Signature "${field}" (manifest ${manifest}) covers`, refs) * }) * ``` * * @returns An array of [[XFASignatureField]] objects, one per XFA signature field. */ getXFASignatures(): XFASignatureField[] { const result: XFASignatureField[] = []; if (!this.hasXFA()) return result; try { const packet = readXfaTemplatePacket(this.acroForm.dict); if (!packet) return result; const { signatures, manifests } = collectXfaSignatures( parseXfaTemplate(packet.xml), ); for (const sig of signatures) { const refs = sig.manifestUse && manifests.has(sig.manifestUse) ? manifests.get(sig.manifestUse)! : sig.inlineRefs; result.push({ field: sig.field, manifest: sig.manifestUse ?? null, refs, }); } } catch (error) { if (error instanceof Error) { throw new Error(`Failed to parse XFA template: ${error.message}`); } throw error; } return result; } /** * Get all signature fields in this form, including both AcroForm signature * fields and signature fields declared inside an XFA template. * For example: * ```js * const form = pdfDoc.getForm() * const sigFields = form.getSignatureFields() * sigFields.forEach(({ name, source }) => { * console.log(`${source} signature field: ${name}`) * }) * ``` * * @returns An array of [[SignatureField]] describing every signature * field, whether it originates from the AcroForm or from XFA. */ getSignatureFields(): SignatureField[] { const results: SignatureField[] = []; for (const field of this.getFields()) { if (field instanceof PDFSignature) { results.push({ name: field.getName(), source: 'acroform', acroField: field, }); } } for (const xfaSig of this.getXFASignatures()) { results.push({ name: xfaSig.field, source: 'xfa', manifest: xfaSig.manifest, refs: xfaSig.refs, }); } return results; } /** * Get all JavaScript from this form's XFA template. * XFA forms can contain JavaScript in `