Web SDKv1.0.0

Hand a large file between pages without a server

Store the picked file's bytes in IndexedDB on the upload page, navigate, and open them with PDFDocument.fromBytes() on the editor page. Nothing is uploaded until the person saves.
LoadingIndexedDBPDFDocument.fromBytes()createPasswordPrompt()doc.save()viewer.markSaved()
A blob URL dies with the page that made it, and a large file does not fit in session storage. IndexedDB survives a full navigation and holds binary data, so the upload page can hand the bytes to the editor page directly. The store below is host code; the SDK only sees a Uint8Array.

1. A small store

pdfStore.tsTypeScript
const DB = 'my-app-edit', STORE = 'pending-pdf', KEY = 'current';

function open(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB, 1);
    req.onupgradeneeded = () => req.result.createObjectStore(STORE);
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

async function run<T>(mode: IDBTransactionMode, op: (s: IDBObjectStore) => IDBRequest<T>): Promise<T> {
  const db = await open();
  return new Promise((resolve, reject) => {
    const req = op(db.transaction(STORE, mode).objectStore(STORE));
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

export const putPending = (bytes: Uint8Array, name: string) =>
  run('readwrite', (s) => s.put({ bytes, name }, KEY));
export const takePending = () =>
  run<{ bytes: Uint8Array; name: string } | undefined>('readonly', (s) => s.get(KEY));
export const clearPending = () => run('readwrite', (s) => s.delete(KEY));

2. Upload page: store and navigate

upload.tsTypeScript
input.addEventListener('change', async () => {
  const file = input.files?.[0];
  if (!file) return;
  await putPending(new Uint8Array(await file.arrayBuffer()), file.name);
  location.href = '/edit';
});

3. Editor page: open from bytes

PDFDocument.fromBytes(bytes, options?) loads a PDF from a Uint8Array; options is { password? }. Kernstra.init() must complete first. A protected file rejects with PASSWORD_ERROR: createPasswordPrompt asks in place and reopens with the password; resolve false from onUnlock for a wrong one, which shakes the field and says so.
edit.tsTypeScript
import { Kernstra, PDFDocument, PDFViewer, SDKError, createPasswordPrompt } from '@kernstra/pdf-web-sdk';

await Kernstra.init({ license: import.meta.env.KERNSTRA_LICENSE });
const pending = await takePending();
if (!pending) { location.href = '/'; throw new Error('nothing to open'); }

let doc: PDFDocument;
try {
  doc = await PDFDocument.fromBytes(pending.bytes);
} catch (err) {
  if (err instanceof SDKError && err.code === 'PASSWORD_ERROR') {
    const prompt = createPasswordPrompt(container, {
      name: pending.name,
      onUnlock: async (password) => {
        try {
          doc = await PDFDocument.fromBytes(pending.bytes, { password });
          prompt.destroy();
          return true;
        } catch {
          return false; // shakes the field
        }
      },
    });
  }
  throw err;
}

const viewer = new PDFViewer(container, doc, { export: { defaultFilename: pending.name } });

4. Save, then clear the store

doc.save() resolves to the document's bytes, a Uint8Array: every change made through the API and, with a viewer open, the values its form fields hold. A signature drawn on screen is not in it; use viewer.createExporter() for everything the user sees. After saving your own way, call viewer.markSaved() so hasUnsavedChanges is false until the next change, then clear the handoff.
edit.tsTypeScript
saveButton.addEventListener('click', async () => {
  const bytes = await doc.save();
  await fetch('/api/documents', { method: 'POST', body: bytes.slice() });
  viewer.markSaved();
  await clearPending();
});

Gotchas

  • doc.save() throws FEATURE_NOT_LICENSED without export, INVALID_STATE while a progressive document is still downloading, and EXPORT_FAILED when a changed PDF/A file's claim cannot be removed.
  • A document opened with a digital signature is saved as an incremental update: the bytes it was signed over, untouched, then one update with what changed.
  • documentLoaded on Kernstra.events fires for fromBytes like the other factories, with { document, pageCount, hasPassword }.
  • How long the bytes stay in IndexedDB, and clearing them when the person leaves without saving, is your page's decision; the SDK stores nothing of the document itself.