diff --git a/src/api/form/PDFField.ts b/src/api/form/PDFField.ts index cd3b3efe4..585841886 100644 --- a/src/api/form/PDFField.ts +++ b/src/api/form/PDFField.ts @@ -1,4 +1,5 @@ import PDFDocument from 'src/api/PDFDocument'; +import PDFAcroField from 'src/core/acroform/PDFAcroField'; import PDFFont from 'src/api/PDFFont'; import { AppearanceMapping } from 'src/api/form/appearances'; import { Color, colorToComponents, setFillingColor } from 'src/api/colors'; @@ -518,4 +519,33 @@ export default class PDFField { return appearanceDict; } + + /** + * Renames this field to the new partial name. Note that this only changes the partial name of the field itself, not its fully qualified name. + * For example, if a field's fully qualified name is 'Parent.OldName', calling rename('NewName') will change its fully qualified name to 'Parent.NewName'. + * @param newName The new partial name for this field. + */ + rename(newName: string): void { + assertIs(newName, 'newName', ['string']); + if (newName.indexOf('.') !== -1) { + throw new Error('Field partial names must not contain periods.'); + } + const parent = this.acroField.getParent(); + if (parent) { + const siblings = parent.Kids(); + if (siblings) { + for (let idx = 0, len = siblings.size(); idx < len; idx++) { + const siblingDict = siblings.lookup(idx, PDFDict); + const siblingRef = this.doc.context.getObjectRef(siblingDict); + if (siblingRef) { + const sibling = PDFAcroField.fromDict(siblingDict, siblingRef); + if (sibling.getPartialName() === newName) { + throw new Error(`A field with partial name '${newName}' already exists as a sibling.`); + } + } + } + } + } + this.acroField.setPartialName(newName); + } } diff --git a/src/core/acroform/PDFAcroField.ts b/src/core/acroform/PDFAcroField.ts index 9f4cd364a..39ebc4ad2 100644 --- a/src/core/acroform/PDFAcroField.ts +++ b/src/core/acroform/PDFAcroField.ts @@ -162,6 +162,10 @@ class PDFAcroField { const parent = this.getParent(); if (parent) parent.ascend(visitor); } + + static fromDict(dict: PDFDict, ref: PDFRef): PDFAcroField { + return new PDFAcroField(dict, ref); + } } export default PDFAcroField; diff --git a/tests/api/form/PDFForm.spec.ts b/tests/api/form/PDFForm.spec.ts index 63c8edbcc..bd49aee74 100644 --- a/tests/api/form/PDFForm.spec.ts +++ b/tests/api/form/PDFForm.spec.ts @@ -44,7 +44,7 @@ const getApRefs = (widget: PDFWidgetAnnotation) => { }; const flatten = (arr: T[][]): T[] => - arr.reduce((curr, acc) => [...acc, ...curr], []); + arr.reduce((curr, acc) => acc.concat(curr), []); const fancyFieldsPdfBytes = fs.readFileSync('assets/pdfs/fancy_fields.pdf'); // const sampleFormPdfBytes = fs.readFileSync('assets/pdfs/sample_form.pdf'); @@ -368,3 +368,88 @@ describe(`PDFForm`, () => { // TODO: Add method to remove APs and use `NeedsAppearances`? How would this // work with RadioGroups? Just set the APs to `null`but keep the keys? }); + +describe('PDFForm Field Rename', () => { + let pdfDoc: PDFDocument; + let form: PDFForm; + + beforeEach(async () => { + // Create a new PDF document for each test + pdfDoc = await PDFDocument.create(); + form = pdfDoc.getForm(); + }); + + it('should successfully rename a text field', async () => { + // Create a text field with initial name + const textField = form.createTextField('OldFieldName'); + textField.setText('Test Value'); + + // Verify initial name + expect(textField.getName()).toBe('OldFieldName'); + + // Rename the field + textField.rename('NewFieldName'); + + // Verify new name + expect(textField.getName()).toBe('NewFieldName'); + + // Verify we can get the field by its new name + const renamedField = form.getField('NewFieldName') as PDFTextField; + expect(renamedField.getName()).toBe('NewFieldName'); + + // Verify old name is no longer valid + expect(() => form.getField('OldFieldName')).toThrow(); + }); + + it('should maintain field value after rename', async () => { + // Create and set up initial field + const textField = form.createTextField('OldFieldName'); + const testValue = 'Test Value'; + textField.setText(testValue); + + // Rename the field + textField.rename('NewFieldName'); + + // Verify value is preserved + const renamedField = form.getField('NewFieldName') as PDFTextField; + expect(renamedField.getText()).toBe(testValue); + }); + + it('should throw error when trying to rename to a name with periods', async () => { + // Create field + const textField = form.createTextField('OldFieldName'); + textField.setText('Test Value'); + + // Attempt to rename to a name with periods + expect(() => textField.rename('New.Field.Name')).toThrow('Field partial names must not contain periods'); + }); + + it('should handle rename of fields with special characters', async () => { + // Create field with special characters + const textField = form.createTextField('Old_Field_Name'); + textField.setText('Test Value'); + + // Rename to new name with special characters + textField.rename('New_Field_Name'); + + // Verify new name + expect(textField.getName()).toBe('New_Field_Name'); + expect(form.getField('New_Field_Name')).toBeDefined(); + }); + + it('should preserve field properties after rename', async () => { + // Create field with specific properties + const textField = form.createTextField('OldFieldName'); + textField.setText('Test Value'); + textField.enableMultiline(); + textField.enableReadOnly(); + + // Rename the field + textField.rename('NewFieldName'); + + // Verify properties are preserved + const renamedField = form.getField('NewFieldName') as PDFTextField; + expect(renamedField.isMultiline()).toBe(true); + expect(renamedField.isReadOnly()).toBe(true); + }); +});