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:
// 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)}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.
How it works
Section titled “How it works”- A new document has no original bytes to append to, so the draft’s save is a full rewrite.
Document.open(Document::open_bytesin Rust) reads the draft. Your edits sit beside its bytes and never change them; see How Lamina PDF thinks about a PDF.setInfochanges the Info dictionary: the title, author and other facts a viewer shows in a file’s properties.transactapplies it as one transaction, whichundoreverts as a whole. In Rust,applytakes 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.editedAtis the modification date to record, a PDF date such asD: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.
setInfoleaves that copy alone, so it still says “Draft report”.
What to change
Section titled “What to change”- More edits in the same revision: add them inside the callback, such as
tx.rotatePage(doc.page(0).id, 90)(RotatePagein Rust). See Edit a PDF. - Removing an entry: pass
nullfor 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 leavemodeout 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_login Rust), as Edit a PDF shows.
Next: a PDF/UA report, an accessible report checked as it’s built.