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).
Edits are ops in transactions
Section titled “Edits are ops in transactions”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.
Saving
Section titled “Saving”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 passinvalidateSignatures: 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
incrementalfails withrequires-rewrite. - An encrypted file isn’t rewritten in plaintext unless you pass
removeEncryption: true. Otherwise saving fails withencrypted-base. - An unchanged document always saves incrementally, which returns its bytes unchanged.
Output is deterministic: the same edits give the same bytes.
Generate, then edit
Section titled “Generate, then edit”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:
// 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;}//! Generate, then edit: build a document, save it, open the bytes, change the Info//! dictionary with an op (undoable and serialisable), and save incrementally: the original//! bytes stay as they are, with one revision appended.//!//! ```sh//! cargo run -p lamina --example generate_then_edit -- [out-dir] # writes <out-dir>/generate_then_edit.pdf//! ```
use std::error::Error;use std::path::PathBuf;
use lamina::{ BuildOptions, Builder, CancelToken, Document, InfoPatch, Metadata, Op, OpenOptions, PageSize, PointPt, Pt, SaveMode, SaveOptions, SetInfo, Standard14,};
fn main() -> Result<(), Box<dyn Error>> { let out = PathBuf::from(std::env::args().nth(1).unwrap_or_else(|| "target/examples-out".into())); std::fs::create_dir_all(&out)?; std::fs::write(out.join("generate_then_edit.pdf"), build()?)?; Ok(())}
/// The document's bytes: the generated file followed by the edit's revision.pub(crate) fn build() -> Result<Vec<u8>, Box<dyn Error>> { // 1. Generate and save (a new document is always a full rewrite). let options = BuildOptions::default() .with_id(*b"lamina-ex-edit!!") .with_producer("lamina examples") .with_creation_date("D:20261030120000Z".parse()?); let mut b = Builder::new(options); let helvetica = b.standard_font(Standard14::Helvetica)?; let mut page = b.page(PageSize::A4); let _ = page.surface().text( "Draft report", &helvetica, Pt::new(20.0), PointPt::from_points(72.0, 96.0), )?; page.finish()?; b.metadata(Metadata::default().with_title("Draft report")); let generated = b.finish()?.save(&SaveOptions::new(), &CancelToken::new())?.bytes;
// 2. Open the bytes and edit them with an op: one transaction, which `undo` could revert. let mut doc = Document::open_bytes(generated.clone(), OpenOptions::default())?; let patch = InfoPatch::new() .title(Some("Final report")) .author(Some("Ada Lovelace")); doc.apply(vec![Op::from(SetInfo::new(patch, Some("D:20261030130000Z".parse()?)))])?;
// 3. Save incrementally: the generated bytes, unchanged, then the appended revision. let options = SaveOptions::new().with_mode(SaveMode::Incremental); let edited = doc.save(&options, &CancelToken::new())?.bytes; if !edited.starts_with(&generated) || doc.info()?.title.as_deref() != Some("Final report") { return Err("the incremental save did not append to the generated file".into()); } Ok(edited)}