pdfRendererLast updated on
Last updated on
Fill PDF forms with AcroForm field support
Fill PDF forms with Paradoc's dependency-light PDF implementation. It supports AcroForm text fields, checkboxes, dropdowns, and radio buttons with automatic field formatting, plus coordinate-based text and image overlays.
Example
import { p } from '@paradoc/core'
import { pdfRenderer } from '@paradoc/render/pdf'
import { createFsResolver } from '@paradoc/resolvers/fs'
import { writeFileSync } from 'node:fs'
const resolver = createFsResolver({ root: process.cwd() })
const form = p.form({
name: 'w9',
fields: {
name: { type: 'text', label: 'Name' },
businessName: { type: 'text', label: 'Business Name' },
taxClassification: { type: 'enum', enum: [{ value: 'individual' }, { value: 'llc' }, { value: 'corporation' }] },
address: { type: 'text', label: 'Address' },
ssn: { type: 'text', label: 'SSN' }
},
layers: {
pdf: {
kind: 'file',
mimeType: 'application/pdf',
path: '/templates/w9-form.pdf',
bindings: {
// Map PDF field names to form field expressions
'topmostSubform[0].Page1[0].f1_1[0]': 'name',
'topmostSubform[0].Page1[0].f1_2[0]': 'businessName',
'topmostSubform[0].Page1[0].c1_1[0]': 'taxClassification:individual',
'topmostSubform[0].Page1[0].c1_1[1]': 'taxClassification:llc',
'topmostSubform[0].Page1[0].f1_5[0]': 'address'
}
}
},
defaultLayer: 'pdf'
}, { resolver })
// Fill the form with data
const filled = form.fill({
fields: {
name: 'John Smith',
businessName: 'Smith Consulting',
taxClassification: 'llc',
address: '123 Main St, New York, NY 10001'
}
})
// The resolver was bound when the form was constructed, so every instance
// derived from it — filled included — carries it. render() doesn't take one.
const output = await filled.render({
renderer: pdfRenderer()
})
// output is Uint8Array - write to file
writeFileSync('w9-filled.pdf', output)Import
From the unified package's PDF entry point:
import { pdfRenderer } from '@paradoc/render/pdf'API
pdfRenderer()
Factory function that creates a configured PDF renderer instance.
function pdfRenderer(options?: PdfRendererOptions): ParadocRenderer<
RendererLayer & { type: 'pdf'; content: Uint8Array },
Uint8Array
>Parameters
PdfRendererOptions
Returns
Returns an ParadocRenderer object with:
renderPdf()
Low-level function for direct PDF rendering without the full renderer interface.
async function renderPdf(options: RenderPdfOptions): Promise<Uint8Array>RenderPdfOptions
A PdfOverlay is either text or an image placed at page (one-based), x,
and y (PDF points from the bottom-left corner). A text overlay carries text
or a field path to read from the data (with or without the fields. prefix), plus optional fontSize, width,
height, color, and font. Overlay text follows the same font order and failure
rules as field values, after the overlay's own font when it names one, so one
render can draw a heading in a bold font and body text in a regular one. A text overlay with a width fails with an
overflow error when its text does not fit at 6 points. An image overlay
carries image bytes, mediaType ('image/png' or 'image/jpeg'), width,
height, and optional fit.
Returns
Returns a Promise<Uint8Array> containing the filled PDF document.
inspectAcroFormFields()
Utility function to inspect AcroForm fields in a PDF template.
async function inspectAcroFormFields(
template: Uint8Array,
options?: InspectOptions
): Promise<PdfFieldInfo[]>Parameters
InspectOptions
PdfFieldInfo
extractPdfData()
Reads a filled PDF back into artifact data through a layer's bindings. form.extract() calls it after choosing the layer with selectPdfExtractionLayer(layers, name?).
async function extractPdfData(options: ExtractPdfDataOptions): Promise<PdfExtraction>Returns { data, report }. Failures throw PdfExtractionError with a code. An encrypted PDF throws PdfEncryptedError.
Inspecting PDF Fields
Use inspectAcroFormFields() to discover field names in a PDF template:
import { inspectAcroFormFields } from '@paradoc/render/pdf'
import fs from 'fs'
const template = fs.readFileSync('form.pdf')
const fields = await inspectAcroFormFields(template)
console.log(fields)
// [
// { name: 'f1_1', type: 'text', required: true },
// { name: 'c1_1', type: 'checkbox', value: false },
// { name: 'dropdown1', type: 'dropdown', value: ['Option A'] }
// ]PDF utilities
@paradoc/render/pdf exports dependency-light helpers for working with PDF
bytes directly.
| Function | Description |
|---|---|
inspectPdf(pdf) | Returns { pageCount, pages }, with each page's one-based number, width, and height in PDF points |
flattenPdf(pdf) | Burns AcroForm field appearances into the pages and removes the interactive controls. A widget a viewer does not show (Hidden or NoView) is removed without being drawn. A PDF with no AcroForm returns unchanged |
selectPdfPages(pdf, pages) | Returns a new PDF with only the given one-based pages, in source order. Dropped pages, and form fields that sit only on them, are not in the file. Pages must be unique positive integers that exist |
mergePdfs(pdfs) | Concatenates PDFs into one, keeping every page in order. A single source returns unchanged |
import { flattenPdf, inspectPdf, mergePdfs, selectPdfPages } from '@paradoc/render/pdf'
const { pageCount } = await inspectPdf(filled)
const flat = await flattenPdf(filled)
const firstTwo = await selectPdfPages(flat, [1, 2])
const packet = await mergePdfs([firstTwo, coverLetter])mergePdfs throws PdfMergeError when a source has no catalog or pages, or
cannot be read. Its source property is the zero-based index of that source.
Every function that reads or writes a PDF, from renderPdf to mergePdfs,
refuses an encrypted PDF with PdfEncryptedError, whose code is
encrypted_pdf. An encrypted file stores its strings and streams enciphered,
so a filled or copied value would be unreadable. Remove the encryption first.
Digital signature fields
Three helpers write and read the file structure a digital signature lives in.
They do not sign: you produce the signature container (for example a CMS
SignedData) with your own keys.
| Function | Description |
|---|---|
preparePdfSignature(pdf, options) | Appends an incremental update with an invisible signature field on the first page, an AcroForm with /SigFlags 3, and a signature dictionary whose /Contents reserves contentsSize bytes. Returns { bytes, byteRange } with the byte range already written. name, reason, location and contactInfo may use any script |
signedBytes(prepared) | The bytes the byte range covers: what your container signs |
embedPdfSignature(prepared, container) | Writes the container into /Contents. Throws when it is larger than the reserved size |
readPdfSignature(pdf) | Reads the newest signature dictionary: its container, byte range, signed bytes, text entries and signing time. coversWholeFile is false when any byte outside the container is unsigned, such as an update appended after signing |
import { embedPdfSignature, preparePdfSignature, signedBytes } from '@paradoc/render/pdf'
const prepared = await preparePdfSignature(pdf, { contentsSize: 16384, signingTime: new Date(), name: 'Ada Lovelace' })
const signed = embedPdfSignature(prepared, createCmsSignature(signedBytes(prepared)))The same entry point exports the signature placement helpers the SDK
re-exports: locate, locator, LocateError, extractFieldsFromPdf,
UnknownMarkerError, and the marker encode and decodeAll functions.
extractFieldsFromPdf and locate throw UnknownMarkerError, naming the page,
signer, and field type code, when a marker's field type is not signature or
initials.
Binding Syntax
PDF bindings map AcroForm field names to Paradoc paths, with special syntax for complex cases. A path may carry the fields. prefix (fields.taxType is taxType), and space around : and , is ignored. Filling, extraction, the fit check, and validation all parse a binding value this way, and all read a split part only from a whole number from 1.
Every binding key must name a form field of the template. renderPdf fails with PdfBindingKeyError, naming each key the template does not have, instead of filling the rest. A binding value may name only paths the fill data carries: fields, parties, defs, annexes, and the signing roots _signers and _captures.
Simple Binding
bindings: {
'pdfFieldName': 'formFieldName'
}Enum Checkboxes
For PDF checkboxes that represent enum values, use :value syntax:
bindings: {
'checkbox_individual': 'taxType:individual', // Check if taxType === 'individual'
'checkbox_llc': 'taxType:llc', // Check if taxType === 'llc'
'checkbox_corp': 'taxType:corporation' // Check if taxType === 'corporation'
}Split Fields
For fields split across multiple PDF inputs (like SSN), use :partNumber syntax:
bindings: {
'ssn_part1': 'ssn:1', // First part: "123" from "123-45-6789"
'ssn_part2': 'ssn:2', // Second part: "45"
'ssn_part3': 'ssn:3' // Third part: "6789"
}Combined Fields
Combine multiple form fields into one PDF field:
bindings: {
'cityStateZip': 'city,state,zipCode' // "San Francisco, CA, 94105"
}A part of a combined binding takes no : qualifier. 'ssn:1, ssn:2' fails with PdfBindingSyntaxError, as does an empty path or qualifier.
Nested Paths
Access nested data using dot notation:
bindings: {
'buyer_name': 'parties.buyer.name'
}Reading bindings back
Extraction reverses simple, nested, enum checkbox, and split bindings exactly. A combined binding cannot be split back, so its fields are reported as not recoverable.
Automatic Serialization
Field values are automatically serialized based on their schema types:
// Money fields formatted as "$50,000.00"
// Date fields formatted as "Apr 1, 2024"
// Phone fields formatted as "(555) 123-4567"Field appearance
Filling draws each value the way the form's designer set up the field:
- Size: the font size in the field's default appearance, or automatic sizing when that size is zero.
- Alignment: left, centered, or right, from the field's quadding.
- Color: the color in the field's default appearance.
- Comb: one character per box across the field's
MaxLenboxes, so a TIN lands one digit per box. - Multiline: text wraps within the field and keeps explicit line breaks.
- Choice: a combo box shows the selected option's display text; a list box shows every selected value, one per line.
A value that does not fit is reduced in size, down to MIN_FONT_SIZE (6 points). Below that, filling fails instead of clipping the value. Checkbox and radio fields are unchanged.
Checking bindings before filling
validate() fails a binding whose value is not a known Paradoc path, and validateLayers() fails a binding whose key is not an AcroForm field in the template. Both errors name the layer and the key.
validateLayers(), paradoc validate, and the validate_artifact AI tool also check each binding of a form's PDF layer to a text field before any data exists. The check builds values the bound field accepts and lays them out with the same code filling uses, in the font filling would choose: the layer's declared font, then the form's own font for the field, then Helvetica. A font supplied only at render time is not known to the check.
A text, email, uuid, or uri field is checked at its length bound: maxLength characters, or the length its pattern bounds, whichever is shorter. A uuid field without a pattern takes its 36-character canonical form. Two values of that length are laid out:
- A typical value is made of the repeated reference text
The quick brown fox jumps over the lazy dog(exported asTYPICAL_TEXT). It holds every lowercase letter, one capital, and word spaces at about the rate English prose has them. Its mean advance in Helvetica, 456 units per 1000, sits just above that of letter-frequency-weighted English text (441), so a box that holds it holds ordinary answers. A position the pattern constrains holds the allowed character nearest the allowed characters' mean width. If the typical value cannot fit at 6 points, it is an error. - The widest value holds the widest character each position allows: for an open position, the widest Latin letter or digit in the font (
Win Helvetica). If only this value cannot fit, it is a warning: ordinary answers fill, and an answer made mostly of wide capitals does not.
Other bindings are checked exactly, and a failure is an error:
- Comb fields: a value longer than the field's boxes (
comb-length). - Split bindings (
ssn:3): the matching hyphen-separated part of the pattern. When the pattern does not fix where the hyphens fall, the part is bounded by the whole value'smaxLength. - Enum fields: the widest option value, since a PDF text box writes the value. Multiselect fields: the widest
maxoption values, or all of them, joined with,. - Combined bindings: each path's value, joined with
,.
A bound text field with nothing bounding its length is a warning (unbounded). validateLayers() returns warnings in result.warnings; they do not fail validation. Composite parts such as address.line1, party values, and computed values are not checked.
The check is also exported on its own:
import { checkPdfBindingFit } from '@paradoc/render/pdf'
const issues = await checkPdfBindingFit({ template: pdfBytes, form, bindings })
// [{ field: 'amountSource', binding: 'fields.amountSource', paths: ['fields.amountSource'], severity: 'error', reason: 'overflow', value: 'typical', limit: 6, length: 200, message: '...' }]Fonts
For each text value, filling uses the first font that can draw every character of the value:
- a font supplied at render time;
- the font the artifact's PDF layer declares;
- the form's own embedded font for that field, when it has every glyph the value needs;
- standard Helvetica, for Latin-1 (WinAnsi) text only.
Most official forms need no font configuration: their values are Latin-1 and fill in Helvetica. For names in Latin Extended, Greek, Cyrillic, or CJK scripts, declare a TrueType font (.ttf) on the PDF layer. The font is read through the same resolver as the layer's PDF, so it travels with the artifact:
"pdf": {
"kind": "file",
"mimeType": "application/pdf",
"path": "w-9.pdf",
"font": { "path": "fonts/NotoSans-Regular.ttf" }
}A host application can supply a font at render time, for example a licensed corporate font. It wins over the layer's font:
import { createLayerRenderer } from '@paradoc/render'
const output = await filled.render({
renderer: createLayerRenderer({ pdfFont: { bytes: corporateFont, source: 'corporate.ttf' } }),
})pdfRenderer({ font }) and renderPdf({ font, layerFont }) take the same PdfFont: the font's bytes and a source that errors name it by. Every font used to draw is embedded in the output, so the PDF displays the same in any viewer. Only TrueType-outline fonts are supported; an OpenType font with CFF outlines, a font collection, or a WOFF file is refused.
To lay out overlay text yourself, measure it first. pdfFontMetrics(font) reads a PdfFont and returns canDraw(text), true when every character has a glyph and none needs shaping, and width(text, size), the width in points that an overlay draws:
import { pdfFontMetrics } from '@paradoc/render/pdf'
const metrics = pdfFontMetrics(font)
if (metrics.canDraw(name) && metrics.width(name, 10) <= 495) {
// draw `name` as one overlay line
}Filling draws one glyph per character, left to right. It does not shape text, so Arabic, Hebrew, Devanagari, Thai, and other scripts that need shaping or right-to-left ordering are refused, whatever font is available. Render documents in those scripts from a React composition layer.
Errors
A value that cannot be drawn faithfully fails with PdfFieldFillError. It names the field (or the text overlay) and the reason:
reason | When | Detail |
|---|---|---|
overflow | The value does not fit at the minimum size | limit: the minimum size in points |
comb-length | The value has more characters than the comb field has boxes | limit: the number of boxes |
missing-glyph | No available font can draw a character | character: the character. Declare a font that covers it. |
unsupported-script | The value is in a script that needs shaping | script: the script, such as Arabic |
A supplied or declared font that cannot be used fails with PdfFontError, naming its source and the problem: unreadable, unsupported-format, or not-embeddable (its license forbids embedding). A declared font the resolver cannot find fails the same way a missing layer file does.
import { PdfFieldFillError, PdfFontError, renderPdf } from '@paradoc/render/pdf'
try {
await renderPdf({ template, data, bindings })
} catch (error) {
if (error instanceof PdfFieldFillError) console.error(error.field, error.reason)
else if (error instanceof PdfFontError) console.error(error.source, error.problem)
else throw error
}Related
- Renderers - Renderer interface overview
- Formatting - Value formatting
Artifact formatting
Pass formatter to the artifact's render(), prepareSeal(), or seal() call to use the same presentation on every pass. Direct renderPdf() and pdfRenderer() also accept a formatter. Artifact context overrides the renderer default.
When the template pre-prints the currency symbol beside each money box, as IRS 1099 forms do, the layer declares it:
"pdfCopyA": {
"kind": "file",
"mimeType": "application/pdf",
"path": "1099-nec-A.pdf",
"format": { "money": { "currencyDisplay": "none" } }
}Every render of that layer then prints 12,000.00 instead of $12,000.00, with no formatter to configure. The layer's format applies over the formatter the render uses, so its locale and digits still apply; only the symbol goes. The amount keeps its currency's fraction digits. Other layers of the artifact, such as its Markdown, keep the symbol, and a layer that reuses these bindings through bindingsFrom declares its own format.
Only PDF layers take format, and currencyDisplay: 'none' is the only value. Declare the money field's currency too: extraction reads an amount printed without a symbol in that currency.
Formatting applies to indexed fields, computed values, parties, and field overlays. Invalid values and unknown binding sources reject with field diagnostics. Checkboxes and selection controls retain their raw value identity.
Text outside Latin-1 needs a font that covers it; see Fonts. Without one, filling fails naming the field and the character.