Skip to content
Lamina PDF
Unreleased

These docs describe changes that aren’t released yet. For the newest release, see the released docs.

Save a PDF

You’ll save an edited PDF, either by appending your changes to the original bytes or by writing the whole file again. The choice matters when a file is signed, was damaged, or is password-protected, because each one limits how it can be saved.

This example, from Edit a PDF, asks for an incremental save, which keeps the original bytes at the start of the result, unchanged:

packages/sdk/examples/generate-then-edit.ts
// 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" });
}

Every save either appends a revision to the bytes you opened (an incremental save) or writes the whole file again (a full rewrite). You pick one with the mode option:

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

doc.save(options) returns the bytes, and doc.saveWithReport(options) also says what it wrote. doc.saveJob() does the same work in steps, so a page can save a large file without freezing. The same file with the same edits always saves to the same bytes.

Every save makes a plan before it writes. The plan reads the file’s save constraint: what its signatures, permissions, repairs and encryption allow. Then it picks a mode or refuses:

  • A signed file saves only incrementally, and only with edits its signatures allow. Anything else fails with would-invalidate-signatures. Pass invalidateSignatures: true to save anyway, which removes the broken signatures from the output.
  • A repaired file, one Lamina PDF had to fix as it opened, can’t safely take an appended revision. It needs a full rewrite, so asking for incremental fails with requires-rewrite.
  • An edited encrypted file saves only as a full rewrite without its encryption, and only when you pass removeEncryption: true. Otherwise every mode fails with encrypted-base.
  • An unchanged document always saves incrementally, which returns its bytes unchanged.

Next: open password-protected files.