Skip to content
Lamina PDF
Unreleased

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

Generate, then edit

You’ll generate a one-page draft report, then open its bytes, retitle it and save the edit as a revision after them. You need this whenever your app changes a PDF it already has, such as a file a user uploads or one it wrote earlier.

One function builds and saves the draft. Another opens those bytes, changes the title and author, and saves again. In TypeScript the draft needs the text module, init({ text: true }). Both languages write the same bytes:

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" });
}

The result is the draft’s bytes, unchanged, followed by one revision that retitles it “Final report” and names Ada Lovelace as its author: open the reference PDF.

  • A new document has no original bytes to append to, so the draft’s save is a full rewrite.
  • Document.open (Document::open_bytes in Rust) reads the draft. Your edits sit beside its bytes and never change them; see How Lamina PDF thinks about a PDF.
  • setInfo changes the Info dictionary: the title, author and other facts a viewer shows in a file’s properties. transact applies it as one transaction, which undo reverts as a whole. In Rust, apply takes the list of ops as one transaction.
  • mode: "incremental" (SaveMode::Incremental) asks for an incremental save: the draft’s bytes first, then one revision with the edit.
  • editedAt is the modification date to record, a PDF date such as D:20261030120000Z. Leave it out and no date is written, because Lamina PDF never reads the clock.
  • The builder also wrote the draft’s title into its XMP metadata, an XML copy of the same facts. setInfo leaves that copy alone, so it still says “Draft report”.
  • More edits in the same revision: add them inside the callback, such as tx.rotatePage(doc.page(0).id, 90) (RotatePage in Rust). See Edit a PDF.
  • Removing an entry: pass null for it, as in { author: null } (.author(None) in Rust).
  • Your own file: open its bytes in place of the draft’s. A signed, repaired or encrypted file limits how it saves; Save a PDF lists the cases.
  • A whole new file: pass mode: "fullRewrite" (SaveMode::FullRewrite), or leave mode out and let the save choose. See Choose a save mode.
  • Edits stored as JSON: replay a saved log with doc.applyLog({ v: 1, ops }) (apply_log in Rust), as Edit a PDF shows.

Next: a PDF/UA report, an accessible report checked as it’s built.