Skip to content

Examples

Each example exists twice: in Rust (crates/lamina/examples/) and in TypeScript (packages/pdf/examples/). Both write the same bytes, the golden in fixtures/golden/examples/. CI checks that in Rust and in every JavaScript runtime: Node, Chromium, Firefox, WebKit, Deno, Bun and workerd. The source below is those files, and each PDF link is the golden itself.

Run them yourself with cargo run -p lamina --example <name> (written to target/examples-out/), or cargo xtask examples to compare all of them with the goldens.

One A4 page with a line of text in a standard 14 font. Open the PDF.

packages/pdf/examples/hello.ts
// The smallest document: one A4 page with a line of text in a standard 14 font. The TS twin of
// crates/lamina/examples/hello.rs: the same bytes (fixtures/golden/examples/hello.pdf).
import { Builder, Font } from "@lamina/pdf";
/** The document's bytes: the same on every run and every platform. Needs `init({ text: true })`. */
export function hello(): Uint8Array {
// Everything that would come from the environment (file ID, dates) is given.
const b = new Builder({
id: new TextEncoder().encode("lamina-ex-hello!"),
producer: "lamina examples",
creationDate: "D:20261030120000Z",
});
const page = b.page("A4");
// Page space: points from the top-left corner, y down; `at[1]` is the text's baseline.
page.surface().text("Hello, world!", { font: Font.standard("Helvetica"), size: 24, at: [72, 96] });
page.finish();
// Free the document at once (the saved bytes are a copy): a Worker or a timing loop calling
// this many times shouldn't wait for the garbage collector to release WebAssembly memory.
const doc = b.finish();
try {
return doc.save();
} finally {
doc.free();
}
}

An embedded, subset font and a standard 14 one, a coloured band, right-aligned text boxes, hairlines and a link. It’s tagged: headings, a table and a link element, with the decorations marked as artifacts. Open the PDF.

packages/pdf/examples/invoice.ts
// An invoice: an embedded, subset font and a standard 14 one, a coloured band, right-aligned
// text boxes, hairlines and a link, tagged (headings, a table, a link element; the bar and the
// rules as artifacts). The TS twin of crates/lamina/examples/invoice.rs.
import { Builder, Font, type Rect } from "@lamina/pdf";
const ITEMS: readonly (readonly [item: string, qty: number, price: number])[] = [
["Widgets, blue (W-100)", 3, 12.0],
["Gadget with fine-tuned dials", 1, 79.9],
["Café-grade sprockets", 12, 1.25],
["Delivery", 1, 4.5],
];
/**
* The document's bytes. Needs `init({ text: true })`.
*
* @param notoSans - `fixtures/fonts/NotoSans-Regular.ttf` (the caller reads files; the package doesn't).
*/
export function invoice(notoSans: Uint8Array): Uint8Array {
const b = new Builder({
id: new TextEncoder().encode("lamina-ex-invoic"),
producer: "lamina examples",
creationDate: "D:20261030120000Z",
tagged: true,
});
const font = Font.fromBytes(notoSans);
const bold = Font.standard("Helvetica-Bold");
// Registered in the Rust example's order, so the objects are numbered the same.
b.font(font);
b.font(bold);
const page = b.page("A4");
const s = page.surface();
// The accent bar and the hairlines are decoration (artifacts); the rest is the structure a
// screen reader follows: a heading, a paragraph, a table, the total, a link.
s.artifact("layout", (s) =>
s
.fillColor("#1e78c8")
.path((p) => p.rect([0, 0, 595.28, 14]))
.fill()
.fillColor([0]),
);
s.text("Invoice 42", { font: bold, size: 24, at: [56, 80], tag: "H1" });
s.text("Acme Widgets Ltd · 1 Example Street · London", { font, size: 10, at: [56, 100], tag: "P" });
let total = 0;
s.tag("Table", (s) => {
ITEMS.forEach(([item, qty, price], row) => {
const y = 160 + 24 * row;
const amount = qty * price;
total += amount;
s.tag("TR", (s) => {
s.text(item, { font, size: 11, at: [56, y], tag: "TD" });
const cell = (box: Rect, text: string) => s.textBox(text, { font, size: 11, box, align: "end", tag: "TD" });
cell([330, y - 12, 40, 16], String(qty));
cell([439, y - 12, 100, 16], `€ ${amount.toFixed(2)}`);
s.artifact("layout", (s) =>
s
.strokeColor([0.8])
.path((p) => p.moveTo(56, y + 8).lineTo(539, y + 8))
.stroke({ width: 0.5 }),
);
});
});
});
s.textBox(`Total € ${total.toFixed(2)}`, { font, size: 14, box: [339, 262, 200, 20], align: "end", tag: "P" });
s.text("Pay online at example.com/pay/42", {
font,
size: 11,
at: [56, 320],
fill: [0.12, 0.47, 0.78],
link: { target: { type: "uri", uri: "https://example.com/pay/42" }, contents: "Pay invoice 42 online" },
tag: "P",
});
page.finish();
b.metadata({ title: "Invoice 42", author: "Acme Widgets Ltd" }).lang("en-GB");
return b.finish().save();
}

Latin, Arabic, Devanagari, Hebrew and Chinese, each in its own embedded font: a shaped line and a paragraph broken into a narrow box. Open the PDF.

packages/pdf/examples/multilingual.ts
// Five scripts, each with its own embedded font: a shaped line and a paragraph broken into a
// narrow box (right-to-left for Arabic and Hebrew). The TS twin of
// crates/lamina/examples/multilingual.rs.
import { Builder, type Direction, Font } from "@lamina/pdf";
/** Each script's line, its paragraph and the paragraph's direction (fonts in the same order). */
export const SCRIPTS: readonly (readonly [line: string, paragraph: string, direction: Direction])[] = [
[
"Latin: the office staff fill the affine waffle flask",
"A paragraph breaks at spaces and fits the width of its box, line by line.",
"ltr",
],
["مرحبا بالعالم السلام عليكم", "مرحبا بالعالم السلام عليكم مرحبا بالعالم السلام عليكم", "rtl"],
["नमस्ते दुनिया। हिन्दी पाठ का परीक्षण", "नमस्ते दुनिया। हिन्दी पाठ का परीक्षण नमस्ते दुनिया।", "ltr"],
["שלום עולם וברוכים הבאים", "שלום עולם וברוכים הבאים שלום עולם וברוכים הבאים", "rtl"],
["你好世界,这是中文字体子集测试。", "你好世界,这是中文字体子集测试。你好世界,这是中文。", "ltr"],
];
/**
* The document's bytes. Needs `init({ text: true })`.
*
* @param fonts - Noto Sans, Noto Sans Arabic, Devanagari, Hebrew and SC (`fixtures/fonts/`), in
* the order of {@link SCRIPTS}.
*/
export function multilingual(fonts: readonly Uint8Array[]): Uint8Array {
const b = new Builder({
id: new TextEncoder().encode("lamina-ex-multil"),
producer: "lamina examples",
creationDate: "D:20261030120000Z",
});
const loaded = fonts.map((bytes) => Font.fromBytes(bytes));
for (const font of loaded) b.font(font);
const page = b.page("A4");
const s = page.surface();
SCRIPTS.forEach(([line, paragraph, direction], row) => {
const font = loaded[row];
if (font === undefined) throw new Error(`no font for script ${row}`);
const y = 80 + 140 * row;
s.text(line, { font, size: 16, at: [56, y] });
// A narrow box breaks the paragraph into lines; right-to-left paragraphs start at the right.
s.textBox(paragraph, { font, size: 12, box: [56, y + 14, 220, 100], direction, align: "start" });
});
page.finish();
return b.finish().save();
}

Paths, dashed strokes, gray, RGB, CMYK and hex colour, transparency and a blend mode, a clip, a rotated transform, a JPEG and an RGBA image with a soft mask. Open the PDF.

packages/pdf/examples/images-and-vectors.ts
// Vector graphics and images: rectangles, circles, Bézier curves, dashed round-capped strokes,
// gray/RGB/CMYK/hex colour, transparency and a blend mode, a clip, a rotated transform, a JPEG
// passed through and an RGBA image with a soft mask. Needs only the base module. The TS twin of
// crates/lamina/examples/vectors.rs (fixtures/golden/examples/vectors.pdf).
import { Builder, Image, Matrix } from "@lamina/pdf";
/** A 16×16 RGBA gradient whose alpha falls off towards the right. */
function gradient(): Uint8Array {
const out = new Uint8Array(16 * 16 * 4);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
out.set([x * 16, y * 16, 200, 255 - x * 15], (y * 16 + x) * 4);
}
}
return out;
}
/**
* The document's bytes.
*
* @param photo - `fixtures/images/photo-rgb.jpg`.
*/
export function imagesAndVectors(photo: Uint8Array): Uint8Array {
const b = new Builder({
id: new TextEncoder().encode("lamina-ex-vector"),
producer: "lamina examples",
creationDate: "D:20261030120000Z",
});
const jpeg = Image.jpeg(photo);
const glow = Image.rgba(16, 16, gradient());
const page = b.page("A4");
const s = page.surface();
const dashed = { width: 3, cap: "round", dash: [12, 8] } as const;
s.fillColor([0.9, 0.3, 0.2])
.path((p) => p.rect([56, 56, 160, 100]))
.fill();
s.fillColor([0.8, 0.1, 0, 0])
.path((p) => p.circle([330, 106], 50))
.fill();
s.strokeColor("#2a9d8f")
.path((p) => p.moveTo(56, 220).cubicTo(180, 140, 300, 300, 539, 200))
.stroke(dashed);
// Translucent overlapping discs, the second multiplied onto the first.
s.save().opacity(0.6, 1).fillColor([0.3]);
s.path((p) => p.circle([120, 330], 60)).fill();
s.blendMode("multiply").fillColor([1, 0.8, 0]);
s.path((p) => p.circle([190, 330], 60))
.fill()
.restore();
// The photo clipped to an ellipse, and a rotated, alpha-masked gradient.
s.save()
.path((p) => p.ellipse([300, 270, 239, 120]))
.clip();
s.image(jpeg, [300, 250, 239, 160]).restore();
s.save().transform(Matrix.after(Matrix.translate(150, 520), Matrix.rotate(30)));
s.image(glow, [-60, -60, 120, 120]).restore();
page.finish();
return b.finish().save();
}

URI, page and named-destination links, an outline with a heading, colour and styles, page labels, and the outline shown when the file opens. Open the PDF.

packages/pdf/examples/links-and-outlines.ts
// Navigation: URI, page and named-destination links, an outline with a heading, colour and
// styles, page labels and the outline shown on open. The TS twin of
// crates/lamina/examples/links_and_outlines.rs.
import { Builder, Font, type Rect } from "@lamina/pdf";
const TITLES = ["Title page", "Contents", "Chapter 1", "Chapter 2"] as const;
/** The document's bytes. Needs `init({ text: true })`. */
export function linksAndOutlines(): Uint8Array {
const row = (y: number): Rect => [36, y - 15, 220, 20];
const b = new Builder({
id: new TextEncoder().encode("lamina-ex-links!"),
producer: "lamina examples",
creationDate: "D:20261030120000Z",
});
const font = Font.standard("Helvetica");
b.font(font);
b.destination("chapter-2", 3);
TITLES.forEach((title, index) => {
const page = b.page("A5");
const s = page.surface();
s.text(title, { font, size: 18, at: [36, 60] });
if (index === 0) {
s.text("Visit example.com", { font, size: 12, at: [36, 100] });
s.link(row(100), { type: "uri", uri: "https://example.com/" }, "Example");
s.text("Go to chapter 1", { font, size: 12, at: [36, 125] });
s.link(row(125), { type: "page", index: 2 });
s.text("Go to chapter 2 (named)", { font, size: 12, at: [36, 150] });
s.link(row(150), { type: "named", name: "chapter-2" });
} else {
s.text("Back to the title page", {
font,
size: 12,
at: [36, 100],
fill: [0, 0.2, 0.8],
link: { target: { type: "page", index: 0 }, contents: "Title page" },
});
}
page.finish();
});
b.outline([
{ title: "Title page", target: { type: "page", index: 0 } },
{ title: "Contents", target: { type: "page", index: 1 }, color: [0, 0, 0.6] },
{
title: "Chapters",
open: true,
bold: true,
children: [
{ title: "Chapter 1", target: { type: "page", index: 2 } },
{ title: "Chapter 2", target: { type: "named", name: "chapter-2" }, italic: true },
],
},
]);
b.pageLabels([
{ start: 0, style: "lowerRoman" },
{ start: 2, style: "decimal" },
]).viewerPreferences({ pageMode: "outlines" });
return b.finish().save();
}

Generate a file, open its bytes, edit the metadata in one transaction and save incrementally. Open the PDF.

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

A 100-page report with two embedded fonts: the performance case each runtime times. It has no golden file. Every runtime checks the report’s SHA-256 instead, and so does the Rust benchmark that builds the same bytes.

packages/pdf/examples/report.ts
// A 100-page report with two embedded fonts: a heading, a paragraph broken into a box, a table
// with hairlines, a line of Chinese (glyphs the fixture subset has) and a footer on every page.
// It has no Rust golden: it is the §4 performance case (pdf-library plan: a 100-page report in
// ≤ 500 ms, < 64 MB in a Worker) that the runtime matrix (B6c) times in every runtime, checking
// that each produces the same bytes (test/suite/examples.ts). Its Rust twin is the
// `report_100_pages` bench in crates/lamina/benches/report.rs, which checks it builds the same
// bytes (REPORT_SHA256): change both together.
import { Builder, Font } from "@lamina/pdf";
/** The number of pages. */
export const REPORT_PAGES = 100;
const PARAGRAPH =
"Quarterly figures for every region follow. Each table lists the units shipped, the revenue " +
"and the change against the previous quarter; totals are at the end of the report.";
/**
* The document's bytes. Needs `init({ text: true })`.
*
* @param notoSans - `fixtures/fonts/NotoSans-Regular.ttf`.
* @param notoSansSC - `fixtures/fonts/NotoSansSC-Regular.otf`.
*/
export function report(notoSans: Uint8Array, notoSansSC: Uint8Array): Uint8Array {
const b = new Builder({
id: new TextEncoder().encode("lamina-ex-report"),
producer: "lamina examples",
creationDate: "D:20261030120000Z",
});
try {
const font = Font.fromBytes(notoSans);
const chinese = Font.fromBytes(notoSansSC);
b.font(font);
b.font(chinese);
for (let n = 1; n <= REPORT_PAGES; n++) {
const page = b.page("A4");
const s = page.surface();
s.text(`Region ${n}: quarterly report`, { font, size: 20, at: [56, 72] });
s.textBox(PARAGRAPH, { font, size: 11, box: [56, 88, 483, 60] });
s.text("你好世界,这是中文字体子集测试。", { font: chinese, size: 11, at: [56, 166] });
for (let row = 0; row < 20; row++) {
const y = 200 + 24 * row;
const units = (n * 37 + row * 11) % 500;
s.text(`Product ${row + 1}`, { font, size: 10, at: [56, y] });
s.textBox(String(units), { font, size: 10, box: [300, y - 11, 60, 14], align: "end" });
s.textBox(`€ ${(units * 12.5).toFixed(2)}`, { font, size: 10, box: [400, y - 11, 139, 14], align: "end" });
s.strokeColor([0.8])
.path((p) => p.moveTo(56, y + 7).lineTo(539, y + 7))
.stroke({ width: 0.5 });
}
s.text(`Page ${n} of ${REPORT_PAGES}`, { font, size: 9, at: [56, 800] });
page.finish();
}
b.metadata({ title: "Quarterly report", author: "lamina examples" }).lang("en-GB");
// Free the document at once (the saved bytes are a copy): a Worker or a timing loop calling
// this many times shouldn't wait for the garbage collector to release WebAssembly memory.
const doc = b.finish();
try {
return doc.save();
} finally {
doc.free();
}
} finally {
b.free(); // a no-op after finish(); frees the builder now when drawing threw
}
}

A Cloudflare Worker that generates a PDF per request. See the Workers quick start.

Opening someone else’s file with a password and tighter limits, with the outcome returned as a value. It writes no PDF. See Encrypted files.

packages/pdf/examples/open-untrusted.ts
// Open bytes from someone else: a password, tighter limits than the defaults, and the outcome as
// a value rather than an exception (the docs site's encrypted files, limits and errors guides).
// It has no Rust golden: it writes nothing; test/examples.test.ts checks what it reports.
import { Document, isLaminaError } from "@lamina/pdf";
/** What opening a file gave: its page count and warning codes, or why it didn't open. */
export type Opened =
| { readonly ok: true; readonly pages: number; readonly warnings: readonly string[] }
| { readonly ok: false; readonly code: string; readonly hint: string | undefined };
/**
* Opens `bytes` with `password` (empty: none) and limits for an upload endpoint: decoded streams
* of at most 16 MiB each and 64 MiB in all. Needs `init()`.
*/
export function openUntrusted(bytes: Uint8Array, password = ""): Opened {
try {
const doc = Document.open(bytes, {
...(password === "" ? {} : { password }),
limits: { maxStreamDecoded: 16 << 20, maxDecodedTotal: 64 << 20 },
});
try {
// Repairs and dropped values are warnings: the file opened, but not exactly as written.
return { ok: true, pages: doc.pageCount, warnings: doc.warnings.map((w) => w.code) };
} finally {
doc.free();
}
} catch (e) {
// `password-required`, `wrong-password`, `not-pdf`, `limit-exceeded`, …: a coded LaminaError
// whose message and hint never contain text from the file (ADR 0026 §4).
if (isLaminaError(e)) return { ok: false, code: e.code, hint: e.hint };
throw e;
}
}

The same API in a Web Worker: open, count the pages, save with progress, and abort a save. It runs in browsers. See Off the main thread.

packages/pdf/examples/worker-client.ts
// `@lamina/pdf/worker`: the sync API's async twin in a Web Worker, so a large file never blocks
// the page. Open, count the pages, save with progress, and abort a save the user cancels. Runs
// in browsers (test/browser/worker.browser.test.ts).
import { isLaminaError } from "@lamina/pdf";
import { connect } from "@lamina/pdf/worker";
/** What the example saw: the page count, the saved size, and whether the abort was reported. */
export interface WorkerClientResult {
readonly pages: number;
readonly savedBytes: number;
readonly progressEvents: number;
readonly abortedAsCancelled: boolean;
}
/**
* Opens `bytes` in a worker, saves them, then starts a second save and aborts it.
*
* @param bytes - any PDF; it is copied to the worker (`transfer: true` would move it).
*/
export async function workerClient(bytes: Uint8Array): Promise<WorkerClientResult> {
const client = await connect();
try {
await using doc = await client.open(bytes);
const pages = await doc.pageCount();
let progressEvents = 0;
const saved = await doc.save({ mode: "fullRewrite" }, { onProgress: () => progressEvents++ });
// A cancel button: abort the signal and the save stops at its next step.
const controller = new AbortController();
const saving = doc.save({ mode: "fullRewrite" }, { signal: controller.signal });
controller.abort(new Error("the user cancelled"));
let abortedAsCancelled = false;
try {
await saving;
} catch (e) {
abortedAsCancelled = isLaminaError(e, "cancelled");
}
return { pages, savedBytes: saved.byteLength, progressEvents, abortedAsCancelled };
} finally {
client.terminate();
}
}