# pdfRenderer

Fill PDF forms with AcroForm field support

Canonical URL: https://docs.paradoc.dev/sdk/renderers/pdf/

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

```ts
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:

```ts
import { pdfRenderer } from '@paradoc/render/pdf'
```

## API

### pdfRenderer()

Factory function that creates a configured PDF renderer instance.

```ts
function pdfRenderer(options?: PdfRendererOptions): ParadocRenderer<
  RendererLayer & { type: 'pdf'; content: Uint8Array },
  Uint8Array
>
```

#### Parameters

| Name | Type | Description |
| --- | --- | --- |
| `options?` | `PdfRendererOptions` | Configuration options |

#### PdfRendererOptions

| Name | Type | Description |
| --- | --- | --- |
| `formatter?` | `Formatter` | Artifact value formatter. Defaults to en-US. |
| `font?` | `PdfFont` | A font supplied at render time. It is tried first, before the font the layer declares. See [Fonts](#fonts). |

#### Returns

Returns an `ParadocRenderer` object with:

| Name | Type | Description |
| --- | --- | --- |
| `id` | `"pdf"` | Renderer identifier |

**Methods**

| Name | Type | Description |
| --- | --- | --- |
| `render` | `(request: RenderRequest) => Promise<Uint8Array>` | Async render function that returns the filled PDF |

### renderPdf()

Low-level function for direct PDF rendering without the full renderer interface.

```ts
async function renderPdf(options: RenderPdfOptions): Promise<Uint8Array>
```

#### RenderPdfOptions

| Name | Type | Description |
| --- | --- | --- |
| `template` | `Uint8Array` | PDF template file as binary data |
| `form?` | `Form` | Form schema for field type detection |
| `data` | `Record<string, unknown>` | Data object to populate form fields |
| `bindings?` | `Record<string, string>` | Mapping from PDF field names to form field expressions |
| `formatter?` | `Formatter` | Artifact value formatter. Defaults to en-US. |
| `overlays?` | `PdfOverlay[]` | Text or image overlays drawn at page coordinates |
| `font?` | `PdfFont` | A font supplied at render time, tried first |
| `layerFont?` | `PdfFont` | The font the artifact's PDF layer declares, tried after `font` |
| `format?` | `LayerFormat` | The presentation the artifact's PDF layer declares, applied over `formatter`. See [Artifact formatting](#artifact-formatting) |

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.

```ts
async function inspectAcroFormFields(
  template: Uint8Array,
  options?: InspectOptions
): Promise<PdfFieldInfo[]>
```

#### Parameters

| Name | Type | Description |
| --- | --- | --- |
| `template` | `Uint8Array` | PDF template file as binary data |
| `options?` | `InspectOptions` | Inspection options |

#### InspectOptions

| Name | Type | Description |
| --- | --- | --- |
| `includeButton?` | `boolean` | Include button fields in results. Defaults to false. |
| `includeSignature?` | `boolean` | Include signature fields in results. Defaults to false. |

#### PdfFieldInfo

| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | AcroForm field name |
| `type` | `"text" \| "checkbox" \| "dropdown" \| "radio" \| "button" \| "signature" \| "unknown"` | Field type |
| `value?` | `string \| boolean \| string[]` | Current field value |
| `required?` | `boolean` | Whether field is marked required |
| `page?` | `number` | One-based page the field widget is on |
| `rect?` | `[number, number, number, number]` | Widget rectangle in PDF points |
| `maxLen?` | `number \| null` | Maximum text length, when the field declares one |

### extractPdfData()

Reads a filled PDF back into artifact data through a layer's bindings. [`form.extract()`](/sdk/artifacts/form#extracting-data-from-a-filled-pdf) calls it after choosing the layer with `selectPdfExtractionLayer(layers, name?)`.

```ts
async function extractPdfData(options: ExtractPdfDataOptions): Promise<PdfExtraction>
```

| Name | Type | Description |
| --- | --- | --- |
| `pdf` | `Uint8Array` | The filled PDF. It is read, never changed. |
| `form` | `Form` | The artifact whose layer produced the PDF |
| `bindings` | `Record<string, string>` | The PDF layer bindings |
| `formatter?` | `Formatter` | The formatter the PDF was filled with. Defaults to en-US. |
| `format?` | `LayerFormat` | The PDF layer's declared format, applied over `formatter` as when filling. `selectPdfExtractionLayer` returns it with the bindings |

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:

```ts
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                                                                                                                        |

```ts
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                                                                                                  |

```ts
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

```ts
bindings: {
  'pdfFieldName': 'formFieldName'
}
```

### Enum Checkboxes

For PDF checkboxes that represent enum values, use `:value` syntax:

```ts
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:

```ts
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:

```ts
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:

```ts
bindings: {
  'buyer_name': 'parties.buyer.name'
}
```

### Reading bindings back

[Extraction](/sdk/artifacts/form#extracting-data-from-a-filled-pdf) 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:

```ts
// 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 `MaxLen` boxes, 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 as `TYPICAL_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 (`W` in 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's `maxLength`.
* **Enum fields:** the widest option value, since a PDF text box writes the value. &#x2A;*Multiselect fields:** the widest `max` option 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:

```ts
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:

1. a font supplied at render time;
2. the font the artifact's PDF layer declares;
3. the form's own embedded font for that field, when it has every glyph the value needs;
4. 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:

```json
"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:

```ts
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:

```ts
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](/guides/react-layers).

## 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.

```ts
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](/sdk/renderers) - Renderer interface overview
* [Formatting](/sdk/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:

```json
"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](/sdk/artifacts/form#extracting-data-from-a-filled-pdf) 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](#fonts). Without one, filling fails naming the field and the character.
