Skip to content

fix(form): remove widget refs from page Annots on removeField#1785

Open
cozminv wants to merge 1 commit into
Hopding:masterfrom
cozminv:fix/removefield-page-annots
Open

fix(form): remove widget refs from page Annots on removeField#1785
cozminv wants to merge 1 commit into
Hopding:masterfrom
cozminv:fix/removefield-page-annots

Conversation

@cozminv

@cozminv cozminv commented Jun 12, 2026

Copy link
Copy Markdown

What?

Fix PDFForm.removeField() so it removes the widget annotation ref from each page's /Annots array, instead of passing an appearance-stream ref to removeAnnot().

Before (src/api/form/PDFForm.ts):

const widgetRef = this.findWidgetAppearanceRef(field, widget);
page.node.removeAnnot(widgetRef);

After:

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);

Added unit test "removes widget annotation refs from page Annots arrays" in tests/api/form/PDFForm.spec.ts.

Why?

removeField() currently calls page.node.removeAnnot() with the ref returned by findWidgetAppearanceRef() — an /AP/N appearance stream ref. Page /Annots arrays list widget annotation dict refs. PDFPageLeaf.removeAnnot() matches by exact PDFRef, so the widget entry is never removed. context.delete() then removes the widget object, leaving a dangling /Annots reference.

This was reported in #1001. PR #1002 fixed removal of child widget refs from the document context, but did not fix which ref is passed to removeAnnot().

Impact observed in a form-editor workflow (remove all fields, recreate them):

  • Page /Annots count roughly doubled (274 → 524 on an 8-page template; 250 broken refs)
  • File size grew ~120 KB without logical changes
  • Some createTextField() calls failed with FieldAlreadyExistsError while others left ghost annotations

How?

For each widget returned by field.acroField.getWidgets():

  1. Resolve the widget dict ref with this.doc.context.getObjectRef(widget.dict) (same approach already used in findWidgetPage()).
  2. Call page.node.removeAnnot(widgetRef) with that ref.
  3. Leave the rest of removeField() unchanged (AcroForm removal, kid deletion, context.delete(field.ref)).

Alternatives considered:

findWidgetAppearanceRef() is unchanged and remains used where appearance streams are needed (e.g. updateFieldAppearances, flatten).

Testing?

  • Added "removes widget annotation refs from page Annots arrays" in tests/api/form/PDFForm.spec.ts:
    • Create text field via addToPage()
    • Assert widget ref is present in page /Annots before removal
    • Call form.removeField(tf)
    • Assert widget ref is absent from /Annots and no widget annots remain
  • All existing PDFForm tests pass: npm test -- tests/api/form/PDFForm.spec.ts (12/12)
  • Manual repro (Node, pdf-lib 1.17.1): create field → removeField → widgetRef no longer in page.node.Annots()

New Dependencies?

No

Screenshots

N/A — structural PDF fix; no visual appearance change in viewers when fields are removed correctly.

Suggested Reading?

Yes — PDF 32000 page annotations (/Annots) vs AcroForm field/widget structure; widget annotations use /Subtype /Widget and are listed on the page, separate from /AP appearance streams.

Anything Else?

  • Fixes the root cause described in Partial Appearance Left Behind After Field Removal #1001
  • Closes #____ (link issue after filing)
  • Community fork cantoo-scribe/pdf-lib appears to already use getObjectRef(widget.dict) in removeField(); this PR brings the same correction to Hopding/pdf-lib master
  • Branch: fix/removefield-page-annots
  • Files changed: src/api/form/PDFForm.ts, tests/api/form/PDFForm.spec.ts

Checklist

removeField() passed the appearance stream ref to removeAnnot(), not the
widget annotation ref listed in the page /Annots array. Widget objects
were deleted from the document while stale /Annots entries remained.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cozminv

cozminv commented Jun 12, 2026

Copy link
Copy Markdown
Author

PR for #1784

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants