Skip to content

Open, edit and save

The bytes you open never change. Every edit goes into an overlay on top of them. A save then either appends a revision (incremental) or writes the whole file again (full rewrite).

doc.transact(tx => …) collects ops (setInfo, rotatePage) and applies them as one transaction when the callback returns. The callback is synchronous. If it throws, nothing is applied. Inside it, another edit or a save of the same document fails with nested-transaction.

doc.undo() and doc.redo() revert and reapply whole transactions. Ops are also plain JSON ({ op: "rotatePage", args: { … } }), so doc.apply(ops) and doc.applyLog({ v: 1, ops }) replay a log.

doc.save(options) returns the bytes, and doc.saveWithReport also says what it wrote. doc.saveJob() does the same work in steps, for UIs that mustn’t block.

mode What it writes
auto (the default) Incremental when that’s allowed, otherwise a full rewrite
incremental The original bytes unchanged, then one appended revision
fullRewrite The whole file again

Every save plans first. The planner reads the file’s save constraint: signatures, DocMDP and UR3 permissions, /Lock, document timestamps, and whether the file needed repair. Then it chooses or refuses:

  • A signed file saves only incrementally, and only edits its signatures allow. Anything else fails with would-invalidate-signatures, unless you pass invalidateSignatures: true, which removes the invalidated signatures from the output.
  • A repaired file can’t be appended to safely, so it needs a full rewrite. Asking for incremental fails with requires-rewrite.
  • An encrypted file isn’t rewritten in plaintext unless you pass removeEncryption: true. Otherwise saving fails with encrypted-base.
  • An unchanged document always saves incrementally, which returns its bytes unchanged.

Output is deterministic: the same edits give the same bytes.

This example generates a file, opens its bytes, edits the metadata in one transaction and saves incrementally. Then it checks that the generated bytes are a prefix of the result:

packages/pdf/examples/generate-then-edit.ts
// Generate a document, then open its bytes and edit them in one transaction, and save
// incrementally: the generated bytes unchanged, then the appended revision. The TS twin of
// crates/lamina/examples/generate_then_edit.rs.
import { Builder, Document, Font } from "@lamina/pdf";
/** The document's bytes: the generated file followed by the edit's revision. Needs `init({ text: true })`. */
export function generateThenEdit(): Uint8Array {
// 1. Generate and save (a new document is always a full rewrite).
const b = new Builder({
id: new TextEncoder().encode("lamina-ex-edit!!"),
producer: "lamina examples",
creationDate: "D:20261030120000Z",
});
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" });
const generated = b.finish().save();
// 2. Open the bytes and edit them: one transaction, which `undo` could revert.
const doc = Document.open(generated);
doc.transact((tx) => tx.setInfo({ title: "Final report", author: "Ada Lovelace" }, "D:20261030130000Z"));
// 3. Save incrementally: the generated bytes, unchanged, then the appended revision.
const edited = doc.save({ mode: "incremental" });
if (!generated.every((byte, i) => edited[i] === byte) || doc.info.title !== "Final report") {
throw new Error("the incremental save did not append to the generated file");
}
doc.free();
return edited;
}

See the output.