Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/api/form/PDFField.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
}
}
4 changes: 4 additions & 0 deletions src/core/acroform/PDFAcroField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
87 changes: 86 additions & 1 deletion tests/api/form/PDFForm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const getApRefs = (widget: PDFWidgetAnnotation) => {
};

const flatten = <T>(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');
Expand Down Expand Up @@ -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);
});
});