These docs describe changes that aren’t released yet. For the newest release, see the released docs.
How Lamina PDF thinks about a PDF
The rest of the docs build on six ideas. The first two follow from one choice: Lamina PDF treats a PDF as bytes, which you give it and it gives back.
Bytes in, bytes out
Section titled “Bytes in, bytes out”Lamina PDF’s core never touches the file system or the network, and it never reads the clock. Your app reads the file, hands Lamina PDF the bytes and stores whatever comes back. The only fetches are the ones you ask for, such as opening a remote file by range requests. That’s why the same code runs in a browser, on a server and inside a Cloudflare Worker.
Same input, same bytes
Section titled “Same input, same bytes”Because Lamina PDF reads no clock, you pass in dates and file IDs yourself. It then writes the same bytes for the same input, on every run and every platform, so you can test, cache and compare its output byte for byte.
The file you opened never changes
Section titled “The file you opened never changes”When you open a document, Lamina PDF keeps its bytes exactly as they were. Every edit goes into an overlay on top of them. Reading the document reads the overlay first, so you see your edits; undo and redo move through them.
Two ways to save
Section titled “Two ways to save”A save writes the overlay out in one of two ways:
- Incremental save: the original bytes, unchanged, followed by a new revision that holds your edits. Signatures over the original stay valid.
- Full rewrite: the whole file written again, smaller and tidier, but no longer the original bytes.
The default, auto, saves incrementally when it can. A signed file saves only incrementally, and a save fails if your edits break its signatures, unless you pass invalidateSignatures: true. Save a PDF has the details.
This example builds a document, opens its bytes, changes the title and saves incrementally. The saved file starts with the original bytes:
// Generate a document, then open its bytes, edit them and save incrementally. The edit changes// the Info dictionary in one transaction, and the incremental save keeps the generated bytes as// they are, with one revision appended.import { Builder, type BuilderOptions, Document, Font } from "@laminapdf/sdk";
/** * The generated file followed by the edit's revision. Needs `init({ text: true })`. * * @param options - can fix the file ID, the producer and the creation date. * @param editedAt - the modification date to record, as a PDF date string. */export function generateThenEdit(options: BuilderOptions = {}, editedAt?: string): Uint8Array<ArrayBuffer> { return edit(generate(options), editedAt);}
/** A one-page draft, saved (a new document is always saved as a full rewrite). */export function generate(options: BuilderOptions = {}): Uint8Array<ArrayBuffer> { using b = new Builder(options); const page = b.page("A4"); page.surface().text("Draft report", { font: Font.standard("Helvetica"), size: 20, at: [72, 96] }); page.finish(); b.metadata({ title: "Draft report" }); using doc = b.finish(); return doc.save();}
/** Opens `bytes`, edits the Info dictionary in one transaction (which `undo` could revert) and saves incrementally. */function edit(bytes: Uint8Array, editedAt?: string): Uint8Array<ArrayBuffer> { using doc = Document.open(bytes); doc.transact((tx) => tx.setInfo({ title: "Final report", author: "Ada Lovelace" }, editedAt)); return doc.save({ mode: "incremental" });}You free what you create
Section titled “You free what you create”In JavaScript, documents and builders live in WebAssembly memory. The garbage collector frees them eventually, but late, so free each one when you’re done: using doc = … frees it at the end of the block, and doc.free() frees it at once. A short script can skip this, but a long-running server shouldn’t.
Three modules in JavaScript
Section titled “Three modules in JavaScript”In JavaScript, Lamina PDF is WebAssembly, split into three modules so a page downloads only what it uses:
- Base: open, read, edit and save; vector and image pages.
- Extract: everything in base, plus text extraction.
- Text: everything in base, plus fonts, shaped text and extraction.
init() loads the base module, and init({ text: true }) or init({ extract: true }) loads one of the others. Call loadText() or loadExtract() to add a module later, when a page first needs it. Rust has one crate, with features instead.