These docs describe changes that aren’t released yet. For the newest release, see the released docs.
Edit a PDF
You’ll open a PDF, change its title and author, and save the edit after the original bytes. You need this whenever your app changes a file someone gave it: a corrected title, a rotated scan, a report marked final.
This example generates a draft report, opens its bytes and retitles it in one transaction. Then it saves incrementally, which keeps the draft’s bytes at the start of the result, unchanged:
// 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" });}//! Generate a document, then open its bytes, edit them and save incrementally.//!//! The edit changes the Info dictionary with an op (undoable and serialisable), and the//! incremental save keeps the generated bytes as they are, with one revision appended.//!//! ```sh//! cargo run -p laminapdf --example generate_then_edit -- [out-dir] # writes <out-dir>/generate_then_edit.pdf//! ```
use std::error::Error;use std::path::PathBuf;
use laminapdf::{ BuildOptions, Builder, Document, InfoPatch, Metadata, Op, OpenOptions, PageSize, PdfDate, 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)?; let bytes = build(BuildOptions::new(), None)?; std::fs::write(out.join("generate_then_edit.pdf"), bytes)?; Ok(())}
/// The generated file followed by the edit's revision. `edited_at` is recorded as the/// modification date; `options` can fix the file ID, the producer and the creation date.pub(crate) fn build(options: BuildOptions, edited_at: Option<PdfDate>) -> Result<Vec<u8>, Box<dyn Error>> { let generated = generate(options)?; edit(generated, edited_at)}
/// A one-page draft, saved (a new document is always saved as a full rewrite).pub(crate) fn generate(options: BuildOptions) -> Result<Vec<u8>, Box<dyn Error>> { let mut b = Builder::new(options); let helvetica = b.standard_font(Standard14::Helvetica)?; let mut page = b.page(PageSize::A4); 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")); Ok(b.finish()?.save(&SaveOptions::new())?.bytes)}
/// Opens `bytes`, edits the Info dictionary in one transaction (which `undo` could revert) and/// saves incrementally: `bytes` unchanged, then the appended revision.fn edit(bytes: Vec<u8>, edited_at: Option<PdfDate>) -> Result<Vec<u8>, Box<dyn Error>> { let mut doc = Document::open_bytes(bytes, OpenOptions::new())?; let patch = InfoPatch::new() .title(Some("Final report")) .author(Some("Ada Lovelace")); doc.apply(vec![Op::from(SetInfo::new(patch, edited_at))])?; let options = SaveOptions::new().with_mode(SaveMode::Incremental); Ok(doc.save(&options)?.bytes)}Your edits live beside the bytes you opened and never change them. Nothing reaches a file until you save. How Lamina PDF thinks about a PDF explains why it works this way.
Group edits into a transaction
Section titled “Group edits into a transaction”doc.transact(tx => …) collects edits, such as setInfo and rotatePage, and applies them together when the callback returns. If the callback throws, none of them apply. The callback is synchronous, and editing or saving the same document inside it fails with nested-transaction.
Undo and redo
Section titled “Undo and redo”doc.undo() reverts the last transaction as a whole, and doc.redo() applies it again. A transaction is the unit of undo, so group the edits a user would expect to undo together.
Replay edits stored as JSON
Section titled “Replay edits stored as JSON”Every edit is also a plain JSON object, such as { op: "rotatePage", args: { … } }. So you can store edits, send them to a server and replay them later. doc.apply(ops) applies a list of them as one transaction, and doc.applyLog({ v: 1, ops }) replays a saved log.
Next: save a PDF.