What were you trying to do?
Remove all AcroForm fields from an existing PDF and recreate them with new geometry.
This is a form-editor workflow:
load template → read fields → removeField on each field → createTextField / addToPage → save
Expected result:
same number of live page annotations as before
no broken indirect-object references
ability to recreate fields with the same names
How did you attempt to do it?
- Load a PDF that already contains AcroForm fields (or create fields with pdf-lib).
- Call form.getFields() and form.removeField(field) for every field.
- Recreate fields with form.createTextField(name) and field.addToPage(page, options).
- Save with doc.save() and reload or parse the output.
Minimal code path (Node / browser):
import { PDFDocument } from 'pdf-lib';
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage();
const form = pdfDoc.getForm();
const field = form.createTextField('example.text.field');
field.addToPage(page, { x: 50, y: 50, width: 100, height: 20 });
const widgetRef = pdfDoc.context.getObjectRef(
field.acroField.getWidgets()[0].dict
);
form.removeField(field);
// Recreate same field name
form.createTextField('example.text.field').addToPage(
page,
{ x: 60, y: 60, width: 100, height: 20 }
);
On a large production template we used the same pattern for 274 fields across 8 pages.
What actually happened?
After removeField() and save():
-
Page /Annots arrays still contain widget annotation refs that no longer resolve.
On our 274-field / 8-page template:
- /Annots count: 274 → 524
- 250 dangling refs (pypdf: "Object N 0 not defined")
- 274 live /Widget annotations after recreation
-
File size grew without logical content changes
(~458 KB → ~580 KB on first re-export) due to dead objects and stale /Annots refs.
-
Some recreate attempts failed with:
A field already exists with the specified name: ""
(24 fields in our template) while others left ghost /Annots entries.
Root cause in src/api/form/PDFForm.ts, removeField():
const widgetRef = this.findWidgetAppearanceRef(field, widget);
page.node.removeAnnot(widgetRef);
findWidgetAppearanceRef() returns an /AP/N appearance stream ref.
Page /Annots lists the widget annotation dict ref.
PDFPageLeaf.removeAnnot() matches by exact PDFRef, so the widget entry is never removed.
context.delete() then removes the widget object, leaving a dangling /Annots pointer.
Related:
#1001 — same diagnosis (widgetRef not found in page annotations)
PR #1002 — fixed child widget refs in document context, not /Annots pruning
What did you expect to happen?
After form.removeField(field):
- The widget annotation ref is removed from the page /Annots array.
- No dangling indirect-object references remain after save().
- Recreating a field with the same name succeeds.
- Repeated save without logical changes does not accumulate extra /Annots entries.
For the minimal repro, after removeField:
const annotsAfter = page.node.Annots()?.asArray() ?? [];
// widgetRef should NOT still be in annotsAfter
On a large template, /Annots count should equal the number of live widgets,
not roughly double with hundreds of broken refs.
How can we reproduce the issue?
SSCCE — Node, no external PDF required
File: repro-remove-field-annots.mjs
import { PDFDocument } from 'pdf-lib';
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage();
const form = pdfDoc.getForm();
const field = form.createTextField('example.text.field');
field.addToPage(page, { x: 50, y: 50, width: 100, height: 20 });
const widgetRef = pdfDoc.context.getObjectRef(
field.acroField.getWidgets()[0].dict
);
const annotsBefore = page.node.Annots()?.asArray() ?? [];
console.log('before removeField:');
console.log(' widgetRef in /Annots:', annotsBefore.includes(widgetRef));
form.removeField(field);
const annotsAfterRemove = page.node.Annots()?.asArray() ?? [];
console.log('after removeField (before save):');
console.log(' widgetRef still in /Annots:', annotsAfterRemove.includes(widgetRef));
const bytes = await pdfDoc.save();
const reloaded = await PDFDocument.load(bytes);
const reloadedPage = reloaded.getPages()[0];
const reloadedAnnots = reloadedPage.node.Annots()?.asArray() ?? [];
let broken = 0;
for (const ref of reloadedAnnots) {
try {
reloaded.context.lookup(ref);
} catch {
broken += 1;
}
}
console.log('after save + reload:');
console.log(' /Annots entries:', reloadedAnnots.length);
console.log(' broken refs:', broken);
Steps:
- npm install pdf-lib@1.17.1
- Save script as repro-remove-field-annots.mjs
- node repro-remove-field-annots.mjs
Observed on 1.17.1:
widgetRef still in /Annots: true (after removeField)
broken refs > 0 after reload (or pypdf "Object N 0 not defined")
Proposed fix in removeField():
const widgetRef = this.doc.context.getObjectRef(widget.dict);
if (widgetRef === undefined) {
throw new Error('Could not find PDFRef for widget annotation');
}
page.node.removeAnnot(widgetRef);
Instead of:
const widgetRef = this.findWidgetAppearanceRef(field, widget);
page.node.removeAnnot(widgetRef);
Unit test name (in a fork):
removes widget annotation refs from page Annots arrays
File: tests/api/form/PDFForm.spec.ts
Version
1.17.1
What environment are you running pdf-lib in?
Node, Browser
Checklist
Additional Notes
Note: No external PDF is required. The SSCCE creates the document in memory.
Same root cause as #1001: removeAnnot() is called with an appearance stream ref,
not the widget annotation ref in /Annots.
PR #1002 fixed child widget cleanup in the document context but did not fix
which ref is passed to page.node.removeAnnot().
Community fork cantoo-scribe/pdf-lib already uses getObjectRef(widget.dict) in
removeField():
https://github.com/cantoo-scribe/pdf-lib/blob/master/src/api/form/PDFForm.ts
We have a minimal fix + unit test on branch fix/removefield-page-annots:
- src/api/form/PDFForm.ts
- tests/api/form/PDFForm.spec.ts
Happy to open a PR if useful despite maintenance status in #1720.
Also related: #987, #1168.
What were you trying to do?
Remove all AcroForm fields from an existing PDF and recreate them with new geometry.
This is a form-editor workflow:
load template → read fields → removeField on each field → createTextField / addToPage → save
Expected result:
same number of live page annotations as before
no broken indirect-object references
ability to recreate fields with the same names
How did you attempt to do it?
Minimal code path (Node / browser):
On a large production template we used the same pattern for 274 fields across 8 pages.
What actually happened?
After removeField() and save():
Page /Annots arrays still contain widget annotation refs that no longer resolve.
On our 274-field / 8-page template:
File size grew without logical content changes
(~458 KB → ~580 KB on first re-export) due to dead objects and stale /Annots refs.
Some recreate attempts failed with:
A field already exists with the specified name: ""
(24 fields in our template) while others left ghost /Annots entries.
Root cause in src/api/form/PDFForm.ts, removeField():
findWidgetAppearanceRef() returns an /AP/N appearance stream ref.
Page /Annots lists the widget annotation dict ref.
PDFPageLeaf.removeAnnot() matches by exact PDFRef, so the widget entry is never removed.
context.delete() then removes the widget object, leaving a dangling /Annots pointer.
Related:
#1001 — same diagnosis (widgetRef not found in page annotations)
PR #1002 — fixed child widget refs in document context, not /Annots pruning
What did you expect to happen?
After form.removeField(field):
For the minimal repro, after removeField:
On a large template, /Annots count should equal the number of live widgets,
not roughly double with hundreds of broken refs.
How can we reproduce the issue?
SSCCE — Node, no external PDF required
File: repro-remove-field-annots.mjs
Steps:
Observed on 1.17.1:
widgetRef still in /Annots: true (after removeField)
broken refs > 0 after reload (or pypdf "Object N 0 not defined")
Proposed fix in removeField():
Instead of:
Unit test name (in a fork):
removes widget annotation refs from page Annots arrays
File: tests/api/form/PDFForm.spec.ts
Version
1.17.1
What environment are you running pdf-lib in?
Node, Browser
Checklist
Additional Notes
Note: No external PDF is required. The SSCCE creates the document in memory.
Same root cause as #1001: removeAnnot() is called with an appearance stream ref,
not the widget annotation ref in /Annots.
PR #1002 fixed child widget cleanup in the document context but did not fix
which ref is passed to page.node.removeAnnot().
Community fork cantoo-scribe/pdf-lib already uses getObjectRef(widget.dict) in
removeField():
https://github.com/cantoo-scribe/pdf-lib/blob/master/src/api/form/PDFForm.ts
We have a minimal fix + unit test on branch fix/removefield-page-annots:
Happy to open a PR if useful despite maintenance status in #1720.
Also related: #987, #1168.