| name | pdflib-impl-form-filling |
| description | Use when filling existing PDF forms or creating form-filling workflows with pdf-lib. Prevents the #1 form mistake: not calling form.updateFieldAppearances() with a custom font before flattening, causing unicode text to disappear. Covers form loading, field filling, checkbox/radio/dropdown, unicode fonts, flattening. Keywords: form.flatten, setText, check, select, updateFieldAppearances, PDFForm, fillable, fill PDF form, auto-fill, populate form fields, lock form after filling, checkbox dropdown.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires pdf-lib 1.x with TypeScript/JavaScript. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
pdf-lib Form Filling
Complete workflow for filling, creating, and flattening PDF forms with pdf-lib.
Quick Reference
import { PDFDocument } from 'pdf-lib'
const pdfDoc = await PDFDocument.load(existingPdfBytes)
const form = pdfDoc.getForm()
const fields = form.getFields()
fields.forEach(f => console.log(`${f.constructor.name}: "${f.getName()}"`))
form.getTextField('Field.Name').setText('value')
form.getCheckBox('Check1').check()
form.getRadioGroup('Group1').select('Option1')
form.getDropdown('Drop1').select('Choice')
const pdfBytes = await pdfDoc.save()
Step-by-Step: Fill an Existing Form
Step 1: Load PDF and Get Form
import { PDFDocument } from 'pdf-lib'
const pdfDoc = await PDFDocument.load(formPdfBytes)
const form = pdfDoc.getForm()
ALWAYS use PDFDocument.load() for existing PDFs. NEVER use PDFDocument.create() when filling an existing form.
Step 2: Enumerate Fields
ALWAYS enumerate fields before filling. Field names are case-sensitive and use fully qualified dot-separated names (e.g., "form.section.fieldName").
const fields = form.getFields()
fields.forEach(field => {
const type = field.constructor.name
const name = field.getName()
console.log(`${type}: "${name}"`)
})
Step 3: Fill Fields by Type
Text fields:
form.getTextField('CharacterName').setText('Mario')
form.getTextField('Age').setText('24')
Checkboxes:
form.getCheckBox('AcceptTerms').check()
form.getCheckBox('Newsletter').uncheck()
Radio groups:
const radio = form.getRadioGroup('ShippingMethod')
console.log(radio.getOptions())
radio.select('Express')
Dropdowns:
const dropdown = form.getDropdown('Country')
console.log(dropdown.getOptions())
dropdown.select('Netherlands')
Option lists:
const list = form.getOptionList('Skills')
list.select(['TypeScript', 'Python'])
Buttons (with images):
const image = await pdfDoc.embedPng(imageBytes)
form.getButton('Photo').setImage(image)
Step 4: Unicode / Custom Font Support
See references/methods.md for font method signatures.
Font Decision Tree for Forms
Setting text in form fields?
+-- ASCII / basic Latin only?
| +-- Default Helvetica works. No extra setup needed.
+-- Extended Latin (diacritics)?
| +-- MUST register fontkit + embed custom font
| +-- MUST call form.updateFieldAppearances(customFont)
+-- Non-Latin (CJK, Arabic, Cyrillic, etc.)?
+-- MUST register fontkit (REQUIRED)
+-- MUST embed font that supports the target script
+-- MUST call form.updateFieldAppearances(customFont)
import fontkit from '@pdf-lib/fontkit'
pdfDoc.registerFontkit(fontkit)
const customFont = await pdfDoc.embedFont(fontBytes)
form.getTextField('name').setText('René Descartes')
form.getTextField('city').setText('Tōkyō')
form.updateFieldAppearances(customFont)
ALWAYS register fontkit before calling embedFont() with custom font bytes. Omitting this step causes a runtime crash.
ALWAYS call form.updateFieldAppearances(customFont) AFTER setting all field values. This applies the custom font to all dirty fields.
Step 5: Flatten or Save Editable
When to Flatten Decision Tree
What is the output purpose?
+-- Final document (invoice, certificate, report)?
| +-- ALWAYS flatten: form.flatten()
+-- Template for re-use?
| +-- NEVER flatten. Save editable.
+-- Distribute to prevent modification?
| +-- ALWAYS flatten: form.flatten()
+-- Need to read values programmatically later?
+-- NEVER flatten. Field data is destroyed.
Flatten all fields (permanent, non-editable):
form.flatten()
const pdfBytes = await pdfDoc.save()
Save as editable form:
const pdfBytes = await pdfDoc.save()
Selective flattening workaround:
form.flatten() flattens ALL fields. To keep some fields editable, remove specific fields before flattening:
const fieldsToFlatten = ['Name', 'Date', 'Signature']
for (const name of fieldsToFlatten) {
const field = form.getField(name)
form.removeField(field)
}
Note: removeField() removes the field entirely (both data and visual widget). This is a workaround, not true selective flattening. See GitHub Issues #1758 and #1367 for progress on native selective flattening.
Step-by-Step: Create a New Form
import { PDFDocument } from 'pdf-lib'
const pdfDoc = await PDFDocument.create()
const page = pdfDoc.addPage([550, 750])
const form = pdfDoc.getForm()
page.drawText('Name:', { x: 50, y: 700, size: 14 })
const nameField = form.createTextField('user.name')
nameField.setText('Default Value')
nameField.addToPage(page, { x: 120, y: 685, width: 200, height: 25 })
page.drawText('Accept terms:', { x: 50, y: 650, size: 14 })
const termsBox = form.createCheckBox('user.acceptTerms')
termsBox.addToPage(page, { x: 170, y: 645, : , : })
termsBox.()
shipping = form.()
page.(, { : , : , : })
shipping.(, page, { : , : , : , : })
page.(, { : , : , : })
shipping.(, page, { : , : , : , : })
shipping.()
country = form.()
country.([, , ])
country.()
country.(page, { : , : , : , : })
pdfBytes = pdfDoc.()
ALWAYS use unique field names. Duplicate names cause a runtime error.
ALWAYS call addToPage() (or addOptionToPage() for radio groups) to make the field visible. Creating a field without adding it to a page makes it invisible.
XFA Forms
Some PDFs use XFA (XML Forms Architecture) instead of AcroForm. pdf-lib works with AcroForm only.
if (form.hasXFA()) {
form.deleteXFA()
}
ALWAYS check for and delete XFA if you encounter forms that do not respond to getTextField() / getCheckBox() calls as expected.
Known Limitations
- Appearance streams — Some viewers show blank fields until clicked. Call
form.updateFieldAppearances() to force appearance stream generation.
- Restricted/encrypted PDFs —
PDFDocument.load() may fail on encrypted PDFs. No built-in decryption support.
- Form fields lost during
copyPages() — AcroForm field definitions may not transfer when copying pages between documents. Field widgets copy but may be non-functional. Workaround: re-create fields after merge.
- No selective flatten API —
form.flatten() flattens ALL fields. See the selective flattening workaround above.
Reference Files