JDFJDF/ docs
★ New · only in JDF

Forms that fill in the browser, save as JSON.

Embed a .jdf form on any web page. The user types, ticks, picks, signs. Click Save — they download the same .jdf with their answers baked in. No backend, no PDF tooling, no server-side AcroForm pipeline. Just one tag and one file.

Why this is different from PDF forms: a PDF AcroForm needs a viewer that supports interactive widgets (most browsers don't, mobile Safari barely does), and saving the filled form usually requires Adobe Acrobat or a server pipeline. JDF Forms run in any browser — the viewer is 25 kB of plain JavaScript, the file format is JSON, and the "save" is a regular file download.

What's wrong with PDF forms

PDF AcroForm

  • Browsers' built-in PDF viewers handle widgets inconsistently — fill fields, hit refresh, lose everything.
  • "Save filled PDF" usually requires Acrobat Reader or a server with iText / pdftk / qpdf.
  • The filled PDF is still a binary blob — your RAG re-parses every form to extract one new value.
  • Generating the form is a binary-format job. Most stacks reach for a service.
  • Validating user input is on you — the format has no schema for it.

JDF Forms

  • Renders identically everywhere jdf.js loads — Chrome, Safari, Firefox, mobile, embedded webviews.
  • Save is a one-click browser download. No backend, no Acrobat, no plugin.
  • Filled form is plain JSON. Your RAG indexes name → value directly. jq reads it. git diff shows what changed.
  • Generating the form is JSON.stringify(doc). LLMs and code-gen pipelines emit forms natively.
  • JSON Schema validates structure. required, pattern, readonly are first-class.

How it works in three pieces

01

Author the form

One JSON document. Five element types: input, textarea, checkbox, select, signature. Each has a stable name — that's the value lookup key.

02

Embed on your page

One <jdf> tag. Add save-button to drop a button in the corner. jdf.js renders real <input> / <textarea> / <canvas> elements and tracks the user's input live.

03

User saves the filled JDF

Browser downloads customer-form.jdf. Open in a text editor — values are right there. Re-open in jdf.js and the form resumes pre-filled. Export to PDF with jdf export; pipe to RAG with jdf validate.

Try it — fill the form, click Save

↓ Live demo — type, tick, sign, then click Save form

Open the downloaded file in any text editor. Every value you typed is right there next to its field declaration — no binary parsing required.

Element types

TypeRenders asValue fieldUse for
input <input> value: string Single-line text. inputType picks the variant — text / number / email / url / tel / date / time / datetime-local / password / color.
textarea <textarea> value: string Multi-line text. rows sets initial height; users can drag-resize.
checkbox <input type="checkbox"> checked: boolean Boolean toggles, agreement boxes, "subscribe to newsletter".
select <select> value: string or values: string[] when multiple: true Dropdowns, country pickers, multi-tag selectors.
signature <canvas> with pointer drawing value: string (base64 PNG) Drawn signatures. The PNG bytes live inline in the JDF — no external upload, no third-party signature service.

Common field properties

Every form element shares these attributes — encoded the same way as the underlying HTML so authors don't have to learn a new shape:

Hello, JDF Form

A complete fillable form, copy-pasteable:

{
  "$jdf": "1.0.0",
  "meta": { "title": "Customer onboarding", "pageSize": "A4", "unit": "mm" },
  "pages": [{
    "id": "page-1",
    "elements": [
      { "type": "text", "content": "Onboarding", "heading": 1,
        "position": { "x": 20, "y": 15 }, "width": 170 },

      { "type": "input", "name": "fullName", "label": "Full name",
        "inputType": "text", "required": true,
        "position": { "x": 20, "y": 40 }, "width": 170, "height": 12 },

      { "type": "input", "name": "email", "label": "Email",
        "inputType": "email", "required": true,
        "position": { "x": 20, "y": 60 }, "width": 170, "height": 12 },

      { "type": "select", "name": "country", "label": "Country",
        "options": [{"value":"tr", "label":"Turkey"}, {"value":"us", "label":"USA"}],
        "position": { "x": 20, "y": 80 }, "width": 80 },

      { "type": "checkbox", "name": "newsletter",
        "label": "Subscribe to the newsletter",
        "position": { "x": 20, "y": 100 }, "width": 170 },

      { "type": "signature", "name": "signature", "label": "Signature",
        "position": { "x": 20, "y": 120 }, "width": 100, "height": 30 }
    ]
  }]
}

Embed it

<link rel="stylesheet" href="https://unpkg.com/@uurtech/jdf@latest/dist/jdfjs.css">
<script type="module" src="https://unpkg.com/@uurtech/jdf@latest"></script>

<jdf src="customer-form.jdf"
     save-button="Save form"
     save-filename="filled-form.jdf"
     height="640"></jdf>

Embed attributes

Programmatic API

The class form gives you direct access to form values without involving the Save button:

// import the embed factory
import { embed } from "@uurtech/jdf";

const viewer = await embed("#form", "/customer-form.jdf");

// One-shot read
viewer.getFormValues();
// → { fullName: "Jane Doe", email: "jane@x.com", country: "tr",
//    newsletter: true, signature: "data:image/png;base64,iVBOR..." }

viewer.getFormValue("fullName");     // "Jane Doe"

// Live subscription — auto-save, dirty tracking, validation hooks
const v2 = await embed("#form", "/customer-form.jdf", {
  onFormChange: (doc, change) => {
    fetch("/api/draft", { method: "POST", body: JSON.stringify(doc) });
  },
});

// Manual download — wire it to your own button
viewer.downloadJdf("my-form.jdf");

// Or grab the blob — upload it, store it in IndexedDB, do whatever
const blob = viewer.exportJdf();
await fetch("/api/submit", {
  method: "POST",
  body: blob,
  headers: { "Content-Type": "application/jdf+json" },
});

Method reference

MethodReturnsWhat it does
getFormValue(name)string | boolean | string[]Reads one field by its name. Checkboxes return boolean, multi-selects return string[], everything else returns string.
getFormValues(){ [name]: value }Flat map of every form field's current value.
exportJdf({ pretty? })BlobCurrent document as a Blob with mime application/jdf+json. Default is pretty-printed.
toJSON({ pretty? })stringSame content as exportJdf() but as a string — for direct fetch bodies, console logs, etc.
downloadJdf(filename?)voidTrigger a browser download. Without a name, uses <meta.title>.jdf.
document (getter)JdfDocumentLive reference to the current document. Reading it after the user typed reflects the latest values.

PDF AcroForm import

Existing PDFs with form fields convert one-to-one. The CLI walks the PDF's widget annotations and emits matching JDF form elements with their current values intact:

# PDF AcroForm → JDF with form elements + filled values
$ jdf convert w9-form.pdf -o w9-form.jdf
$ jdf validate w9-form.jdf
✓ Valid: w9-form.jdf
  Format:    1.0.0
  Pages:     1
  Elements:  18

# Open in jdf.js — fields are pre-filled with whatever the PDF had
<jdf src="w9-form.jdf" save-button></jdf>
PDF widget typeJDF elementNotes
Tx (text), single-lineinputinputType: "text"; fieldValuevalue.
Tx with multi-line flagtextareaDetected via field flags bit 13.
Btn (checkbox)checkboxchecked = true when fieldValue ≠ "Off".
Btn (radio)(currently dropped)JDF 1.0 doesn't have a native radio element type yet — radio groups are flattened away on import.
Btn (pushbutton)(skipped)No form-value semantics, ignored.
Ch (combo / list)selectcombo flag → dropdown; multiSelect flag → multiple: true with values[].
Sig (signature)signatureExisting signature appearance is dropped; the user re-signs in the embed.

RAG / pipeline ingestion

A filled JDF form is structured data your retriever can chunk per field. No more parsing PDF widget streams or running OCR on filled pages:

# CI gate: every submitted form must pass schema validation
$ jdf validate inbox/*.jdf || exit 1

# Then jq the responses straight into your retriever
$ jq -s 'map(.. | select(.type=="input" or .type=="select") | {name, value})' \
       inbox/*.jdf
[
  { "name": "fullName", "value": "Jane Doe" },
  { "name": "email", "value": "jane@example.com" },
  { "name": "country", "value": "tr" }
]

The same query works on every form in your corpus, regardless of who authored it. There is no per-document parser to write, no font-extraction step, no OCR fallback.

The AI angle: models emit JSON natively, and a JDF form is JSON. So an LLM can generate a fillable form, an agent can fill it programmatically (mutate value fields, write the JSON back to disk), and the same file flows through a viewer for human review or final signature. PDF requires a different tool at every stage.

Reader / desktop parity

The same five element types render in the desktop reader. A form filled in jdf.js opens in the desktop reader with the same values; a form filled in the desktop reader exports to PDF (via Cmd+Shift+E) with the user's values flattened to printable text and an underline for blank fields. Round-tripping is intentional — the document is the source of truth, the runtime is interchangeable.