Lector
Feature recipes

PDF forms

Render interactive fields and download a PDF containing the filled values.

Add AnnotationLayer with renderForms to render the PDF's interactive fields. Form state belongs to PDF.js's document annotation storage. Use the document proxy's saveDocument() method when you need PDF bytes with the edits applied.

Fill and download

Use a PDF with actual interactive form fields, such as public/form.pdf, and the pdf-setup.ts from installation.

form-viewer.tsx
"use client";
 
import { AnnotationLayer, CanvasLayer, Page, Pages, Root, TextLayer, usePdf } from "@anaralabs/lector";
import { useState } from "react";
import "./pdf-setup";
 
function DownloadFilledPDF() {
  const document = usePdf((state) => state.pdfDocumentProxy);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);
 
  async function download() {
    setSaving(true);
    setError(null);
    try {
      const bytes = await document.saveDocument();
      const blob = new Blob([new Uint8Array(bytes)], { type: "application/pdf" });
      const url = URL.createObjectURL(blob);
      const link = window.document.createElement("a");
      link.href = url;
      link.download = "filled-form.pdf";
      window.document.body.appendChild(link);
      link.click();
      link.remove();
      window.setTimeout(() => URL.revokeObjectURL(url), 1000);
    } catch {
      setError("Couldn't save this PDF. Please try again.");
    } finally {
      setSaving(false);
    }
  }
 
  return (
    <div>
      <button type="button" disabled={saving} onClick={() => void download()}>
        {saving ? "Preparing download…" : "Download filled PDF"}
      </button>
      {error && <p role="alert">{error}</p>}
    </div>
  );
}
 
export default function FormViewer() {
  return (
    <Root source="/form.pdf" style={{ height: 600, display: "flex", flexDirection: "column" }}>
      <DownloadFilledPDF />
      <div style={{ flex: 1, minHeight: 0 }}>
        <Pages>
          <Page>
            <CanvasLayer />
            <TextLayer />
            <AnnotationLayer renderForms />
          </Page>
        </Pages>
      </div>
    </Root>
  );
}

saveDocument() returns a promise for the edited PDF bytes. Downloading the source URL or calling getData() is not the same operation. See the PDF.js document proxy reference.

Reading values in your application

Do not use new FormData(viewerElement) as a complete document export. Pages virtualizes the document, so offscreen widgets may not exist in the DOM. FormData also follows HTML successful-control rules; unchecked or disabled controls can be absent.

The proxy exposes annotationStorage for PDF.js annotation state and getFieldObjects() for field metadata. If you need a business-domain JSON object, map PDF field identifiers to your schema explicitly and validate the values in your app. Storage is PDF.js state, not a ready-made object keyed by your application's field names.

The live example below inspects currently mounted DOM form controls for demonstration. Use the download recipe above for document-wide PDF output.

Boundaries

This supports interactive PDF form widgets, not arbitrary PDF text editing. A printed blank line in a scanned document is not a field. Test the specific form types your app accepts and verify the downloaded file in another PDF reader.

Lector's custom highlight and comment overlays are separate from PDF.js form storage. saveDocument() does not automatically embed those overlays in the saved PDF.

Live example

Loading...

Filled Form Values

No form values have been entered yet

On this page