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()throwsFEATURE_NOT_LICENSEDwithoutexport,INVALID_STATEwhile a progressive document is still downloading, andEXPORT_FAILEDwhen 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.
documentLoadedonKernstra.eventsfires forfromByteslike 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.