@quorbe/xfa.js
v0.4.1
Published
The first open source XFA (XML Forms Architecture) renderer for the web. Parse, render, script, and fill Adobe XFA dynamic PDF forms in JavaScript/TypeScript.
Maintainers
Readme
xfa.js
The first open source XFA forms renderer for the web.
XFA (XML Forms Architecture) is Adobe's format for dynamic PDF forms. It is used extensively by governments, military, and security organizations worldwide — yet no working open source implementation has existed. xfa.js fills that gap: parse, render, script, and fill XFA forms entirely in JavaScript/TypeScript, in the browser or Node.
npm install @quorbe/xfa.js⚠️ Early days. Phases 1 (parser), 2 (React renderer), 3 (writer), and 4 (visual document viewer) are implemented and tested — the full round-trip works: parse → render → fill → download a completed PDF, in a flat form or in faithful document layout. Reflow/pagination lands next; see the roadmap.
Quick start
Parse an XFA PDF into a clean, typed JSON model:
import { XfaParser, parseXfaPdf } from '@quorbe/xfa.js';
// In the browser
const bytes = new Uint8Array(await file.arrayBuffer());
const form = await parseXfaPdf(bytes);
console.log(form.locale); // "en_US"
console.log(form.fields.length); // 42
for (const field of form.fields) {
console.log(field.somExpression, field.fieldType, '=', field.value);
// form1.header.firstName text = Ada
}Need finer control, or want the raw packets?
const parser = await XfaParser.fromPdf(bytes);
parser.getPacket('template'); // raw template XML
parser.getPacket('datasets'); // raw datasets XML (current values)
const form = parser.parse();Already have the XML (e.g. from another extractor)?
const form = XfaParser.fromXml({ template, datasets }).parse();
// or a single <xdp:xdp> document:
const form2 = XfaParser.fromXml(xdpString).parse();Detect whether a PDF is XFA at all:
import { isXfaPdf } from '@quorbe/xfa.js';
if (await isXfaPdf(bytes)) { /* … */ }Render an interactive form (Phase 2)
The @quorbe/xfa.js/renderer entry turns a parsed form into native, interactive HTML —
not a PDF viewer. React is an optional peer dependency, so parser-only users
never pull it in.
import { parseXfaPdf } from '@quorbe/xfa.js';
import { XfaForm } from '@quorbe/xfa.js/renderer';
import '@quorbe/xfa.js/styles.css';
const form = await parseXfaPdf(bytes);
function App() {
return (
<XfaForm
form={form}
onSubmit={(values) => console.log(values)}
onChange={(key, value) => console.log(key, '→', value)}
/>
);
}What the renderer does today:
- Maps every field type to a real control (text, numeric, date/time, checkbox, radio groups from exclusion groups, dropdown/listbox, signature, etc.).
- Repeatable sections (
<occur>): add/remove instances, with values kept per-instance and re-indexed on removal. - Show/hide: respects the
presenceattribute, plus anisVisiblehook so dynamic visibility can be driven externally (and by the Phase 3 engine). - Controlled or uncontrolled value state; per-field render overrides via
renderField; read-only mode. See docs/renderer.md.
Visual document mode (Phase 4)
XfaForm renders a flat list of fields. XfaViewer renders the form as the
document it is — preserving the original layout — while staying fully
interactive. Submitted values are identical, so it drops straight into the same
fill-and-download cycle.
import { XfaViewer } from '@quorbe/xfa.js/renderer';
import '@quorbe/xfa.js/styles.css';
<XfaViewer form={form} width={816} onSubmit={handleSubmit}>
<button type="submit">Fill & download PDF</button>
</XfaViewer>;
// Or flip the existing component into visual mode (backward compatible):
<XfaForm form={form} visualMode />;How it works: real XFA forms are laid out by flow, not absolute coordinates
(on DS-7801, ~80% of nodes carry no x/y at all). So XfaViewer maps each
container's XFA layout onto CSS — tb→column, lr-tb→row+wrap,
table/row→table flow, position→absolute — and lets the browser place and
wrap children. Fidelity comes from correct flow, not a pixel overlay. The page
frame (size, margins) is read from the template's <medium>/<contentArea>.
Scope: Phase 4 renders the form as a single continuous, zoomable sheet. True multi-page reflow/pagination (breaking content across physical pages, measuring heights) is Phase 5.
Fill & save a PDF (Phase 3)
@quorbe/xfa.js/writer writes submitted values back into the PDF's XFA
datasets stream, producing a filled PDF that opens with the values populated.
It takes exactly what XfaForm's onSubmit produces — closing the loop:
import { XfaWriter } from '@quorbe/xfa.js/writer';
const writer = new XfaWriter();
const filledPdf = await writer.write(originalPdfBuffer, values); // Uint8Array
// In the browser: download it
const url = URL.createObjectURL(new Blob([filledPdf], { type: 'application/pdf' }));
// optionally make it non-editable for archiving:
const flat = await writer.flatten(filledPdf);Full cycle, wired through the renderer:
<XfaForm
form={form}
onSubmit={async (values) => {
const filled = await new XfaWriter().write(originalPdfBuffer, values);
/* download / upload `filled` */
}}
>
<button type="submit">Fill & download</button>
</XfaForm>Guarantees: the original buffer is never mutated, repeatable-section instances
(alias[0], alias[1], …) and missing nodes are created as needed, and on any
failure the original PDF is returned unchanged. See docs/writer.md.
What you get back
parse() returns an XfaForm:
interface XfaForm {
root: XfaSubform; // the form tree (subforms → fields/exclGroups/draws)
fields: XfaField[]; // every field, flattened, in document order
locale: string | null;
packets: XfaPackets; // raw XML, in case you need the source
}Each XfaField is normalized into a renderer-friendly shape — Adobe's many
<ui> widgets collapse into a small fieldType set (text, numeric,
date, checkbox, radio, dropdown, listbox, …), with caption,
value (merged from the datasets packet), items, readOnly, required,
geometry, and a SOM-style somExpression path. See
docs/parser.md for the full model.
Why this is hard (and why it didn't exist)
XFA is not "PDF with form fields" (that's AcroForm). It is a complete XML
application embedded in the PDF: a template grammar, a separate live data DOM,
a layout engine, and two scripting languages (FormCalc + JavaScript). Adobe
deprecated it, browsers never supported it, and pdf.js explicitly does not
render it. xfa.js rebuilds the stack piece by piece — starting with a faithful
parser. See docs/xfa-format.md for a primer.
Roadmap
| Phase | Module | Status |
| ----- | ------------- | ----------------- |
| 1 | Parser | ✅ Implemented |
| 2 | Renderer (React) | ✅ Implemented |
| 3 | Writer (fill & save) | ✅ Implemented |
| 4 | Visual viewer (CSS-flow layout) | ✅ Implemented — XfaViewer |
| 5 | Reflow & pagination | 🚧 Next — multi-page layout, content measurement |
| — | Scripting (FormCalc + JS) | Planned — dynamic show/hide, calculations |
Development
npm install
npm test # vitest
npm run typecheck # tsc --noEmit (strict)
npm run build # vite lib build + .d.ts
npm run demo # Next.js visual playground (see demo/)Inspect a local XFA PDF from the CLI:
npx tsx scripts/inspect.ts samples/your-form.pdf
npx tsx scripts/inspect.ts samples/your-form.pdf --jsonTech stack
TypeScript (strict) · Vite (lib build) · Vitest · React (peer, for the
renderer) · fast-xml-parser
· pdf-lib. No other heavy dependencies.
License
MIT © quorbe
