Opened from the filesystem. The iOS API reference will be blank — it fetches its content and needs a real origin. Run ./serve.sh in this directory.

PDFCora for Web

@pdfcora/webThe core and the viewer component: documents, pages, forms, annotations, signatures, PdfViewerElement @pdfcora/web-aiAnswers, summaries, proposals, alt text, remediation, diff, and the agent tools

Install

npm install @pdfcora/web

The packages are not published yet. Until they are, build from source: see Getting a working checkout.

The .wasm ships inside the package and must be served from the same origin.

Open and render

import { configureEngine, PdfDocument } from '@pdfcora/web';

// Where your app serves the two engine files from the package's vendor/ folder.
configureEngine({
  factory: async (options) => (await import('/pdfcora/pdfium.esm.js')).default(options),
  locateFile: (path) => (path.endsWith('.wasm') ? '/pdfcora/pdfium.esm.wasm' : path),
});

const doc = await PdfDocument.open(bytes);
const page = await doc.page(0);
const image = await page.render({ width: 1024, annotations: true, forms: true });

Survive a hung render

A malformed page can hang PDFium in native code, where it cannot be cancelled. Run it in a worker so the page can recover by terminating it:

import { PdfWorker } from '@pdfcora/web';

const worker = new PdfWorker({
  workerUrl: '/pdfcora/worker/host.js',   // the @pdfcora/web/worker file, dist/worker/host.js
  glueUrl: '/pdfcora/pdfium.esm.js',
  wasmUrl: '/pdfcora/pdfium.esm.wasm',
});
const doc = await worker.open(bytes);   // the same API as PdfDocument
// A call that hangs rejects with engineUnresponsive; worker.terminate() ends it by hand.

Read a signature

for (const signature of await doc.signatures()) {
  if (!signature.coversWholeDocument) {
    // Valid, but content was appended after signing and is not protected.
  }
}

Verify it

const verdict = await doc.verify(signature, { trustAnchors: roots });
if (verdict.kind === 'digestMismatch') {       // the bytes changed after signing
} else if (verdict.kind === 'untrusted') {     // intact, but the issuer is unknown
} else if (verdict.kind === 'valid') {         // both
}

Integrity and trust are separate answers, and revocation is not checked.

Redact it

const report = await doc.redact([
  { kind: 'region', rect, pageIndex: 0 },
  { kind: 'textMatches', query: 'account number' },
]);
const clean = await doc.toBytes('full');   // full, always

Content is removed and the page's content stream regenerated, so the words leave the file — a black box drawn over text would leave every one of them in it. Save with full; an incremental save does not write the regenerated stream at all. Images overlapping the region are removed whole rather than repainted, and a signed document cannot be redacted without breaking its signature.

Sign it

The browser has no CMS API — WebCrypto provides the primitives but not the container — so signing takes an implementation you supply. iOS, Android and Java sign natively and need nothing extra.

const signed = await doc.sign({
  // Return a CAdES-BES CMS, or pass subFilter: 'adbe.pkcs7.detached' for plain PKCS#7.
  signer: { async sign(data) { /* ... */ } },
  reason: 'Approved',
});

Signing appends an incremental update, so a signature already on the document keeps verifying. The first release signs invisibly. Signing uses the bytes the document was opened from, so edits made in memory, such as the redaction above, are not in the signed file. To sign edits, save, reopen, then sign.

Place a control over a form field

const t = PdfViewTransform.fittingWidth(await page.size(), canvas.clientWidth);
for (const field of await page.formFields()) { const box = t.toViewRect(field.rect); }

All platforms