TPTruPDF

Forms

AcroForm + static XFA read/write, FDF / XFDF data round-trip, sandboxed field-script engine (calculate / format / validate).

Field types

KindNotes
textSingle / multi-line; optional regex pattern
checkboxBoolean
radioMutually exclusive groups
choiceCombo (dropdown) and listbox
pushbuttonAction triggers (next-page, JS — JS is sandboxed)
signatureDigital-signature placeholder (see Signatures)

FormManager

import { FormManager } from '@trupdf/viewer';
 
const m = new FormManager({ strict: true });
 
m.add({
  kind: 'text',
  id: 'firstName',
  name: 'firstName',
  page: 1,
  widgets: [{ rect: { x: 100, y: 200, width: 120, height: 20 }, page: 1 }],
  value: 'Bharat',
});
 
m.setValue('firstName', 'Updated');
m.toValueMap(); // { firstName: 'Updated' }
m.fromValueMap({ firstName: 'Loaded' });
m.validate(); // ValidationIssue[]

AcroForm I/O

import { readAcroForm, writeAcroForm } from '@trupdf/viewer';
 
const fields = await readAcroForm(pdfDoc); // the loaded document
const out = await writeAcroForm(srcBytes, fields, {
  flatten: false, // true → static PDF
});

readAcroForm walks every Widget annotation in the document and returns FormFieldRecord[]. writeAcroForm back-writes values directly into the PDF; flattening renders fields as static page content.

Wire formats

import {
  serializeXFDFFormData,
  deserializeXFDFFormData,
  serializeFDF,
  deserializeFDF,
} from '@trupdf/viewer';
 
const xfdf = serializeXFDFFormData(m.list());
const fdf = serializeFDF(m.list());
  • XFDF form data — preferred; same wire format as the annotation XFDF; round-trips with Acrobat.
  • FDF — legacy systems; works.
  • JSON — derived view; not a persistence format.

Static XFA

import { readXFA, unlockStaticXFA, serializeXFADatasets } from '@trupdf/viewer';
 
const fields = readXFA(rootSubform); // flatten nested subforms
unlockStaticXFA(manager); // lift read-only flag
const xml = serializeXFADatasets(manager); // emit <datasets>

Dynamic XFA (with flow markers) is read-only — best-effort render without write-back. Write workflows must convert to AcroForm first.

Field scripts — calculations, formatting, validation

TruPDF executes AcroForm field scripts automatically when a document loads: /AA /C (calculate), /AA /F (display format) and /AA /V (validate) actions, honouring the AcroForm /CO calculation order — the same precedence Acrobat applies. Forms with computed totals, currency formatting and range checks work out of the box in DocViewer with zero configuration.

Sandboxed by design

Field scripts arrive inside untrusted PDFs, so they run through a sandboxed interpreter — never eval, never new Function. Two execution tiers:

1. Acrobat AF* built-ins, recognised by string match:

Built-inBehaviour
AFSimple_CalculateSUM / PRD / AVG / MIN / MAX over a field list
AFRange_ValidateLower / upper bound check
AFNumber_FormatDecimals, separators, currency ($1,234.50)
AFPercent_FormatPercent display (12.5%)
AFDate_Format / AFDate_FormatExAcrobat date pattern table
AFTime_FormatAcrobat time pattern table
AFSpecial_FormatZip, zip+4, phone, SSN

2. Custom calculation scripts in the common shape

var a = this.getField("Qty").value;
var b = this.getField("Price").value;
event.value = a * b;

are compiled by a safe expression parser supporting arithmetic, comparisons, the ternary ?:, allow-listed Math functions, and Number / parseInt / String casts.

Anything outside both tiers is reported and never executed — a malicious PDF cannot run JavaScript inside your app. That's a security feature, not a limitation.

Display formatting

Behaves like Acrobat: the formatted value ($1,234.50, 12.5%, formatted dates) shows when a text field is blurred; focusing the field reveals the raw value for editing. Raw values — never the formatted strings — are what gets saved to the PDF /V.

Programmatic access

Inside DocViewer everything above is automatic. For programmatic control, the engine exposes the script engine:

const js = engine.getFormScriptEngine(); // FormJSEngine | null
 
js?.formatValue('Total'); // '$1,234.50' — or null (no format / not formattable)
js?.runAllCalculations(); // force-run the whole /CO chain
js?.validateField('Qty'); // ValidationIssue | null

Non-DocViewer hosts wire it up themselves:

import { FormJSEngine, readFormScripts } from '@trupdf/forms';
 
const scripts = await readFormScripts(pdf); // loaded document → /AA scripts + /CO order
 
const js = new FormJSEngine(manager, {
  onUnsupported: (fieldId, script, phase) => {
    console.warn(`Unsupported ${phase} script on ${fieldId} — not executed`);
  },
});
js.registerAll(scripts);
js.runAllCalculations();

Unrecognised validation scripts emit a ValidationIssue and never execute:

m.on('form.validation.changed', ({ issues }) => {
  issues.forEach((iss) => console.warn(iss.fieldId, iss.code, iss.message));
});

Signed documents

On documents that already carry an applied digital signature, the initial recalculation on open is skipped so the signature is not invalidated by a value change the user never made. User edits still recalculate normally (and invalidate the signature, exactly as in Acrobat). See Signatures.

React hook

import { useFormValues } from '@trupdf/react';
 
function FormHud({ manager }: { manager: FormManager }) {
  const [values, setValue, fields] = useFormValues(manager);
  return (
    <ul>
      {fields.map((f) => (
        <li key={f.id}>
          <label>{f.name}</label>
          <input
            value={String(values[f.id] ?? '')}
            onChange={(e) => setValue(f.id, e.target.value)}
          />
        </li>
      ))}
    </ul>
  );
}

Pre-load values before document arrives

const manager = new FormManager();
manager.fromValueMap({ firstName: 'Bharat', accept: true });
 
<DocViewer source="…" formManager={manager} licenseManager={license} />;

SDK access

TruPDFSDK.getForms() returns a FormsApi (or null before a document is attached) with list(), getValues(), setValue(id, value), fromValueMap(map), and on / off for field.added, field.updated, field.removed, value.changed, and fields.cleared.

const forms = sdk.getForms();
forms?.setValue('firstName', 'Alice');
forms?.on('value.changed', ({ id, value }) => sync(id, value));

Events

EventPayload
form.field.added{ id, kind }
form.field.updated{ id, kind }
form.value.changed{ id, value }
form.validation.changed{ issues: ValidationIssue[] }