Node
Node (22 or newer) resolves the node export condition, which reads each .wasm file from @lamina/wasm as bytes. Call await init({ text: true }) once. After that, every call is synchronous.
The smallest document is one page with a line of text in a standard 14 font. Its bytes are the same as those of the Rust hello example:
// 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(); }}This is how the package’s tests run it, with every other example, in Node 22, 24 and 26: init once, then each example’s bytes compared with the Rust golden.
// The TS examples produce the Rust examples' bytes (fixtures/golden/examples/, ADR 0018 §5) in// Node: the shared list (test/suite/examples.ts) every runtime of the B6c matrix runs. The// report's hash and the performance smoke are test/node/measure.test.ts.import { Builder, Font, init } from "@lamina/pdf";import { beforeAll, describe, expect, it } from "vitest";import { openUntrusted } from "../examples/open-untrusted.js";import { read } from "./helpers.js";import { runSuite } from "./suite/examples.js";
beforeAll(() => init({ text: true }));
describe("examples", () => { runSuite("node", read, it, "examples");
it("uses one PDF font for equal fonts created per call", () => { const build = (font: () => Font) => { const b = new Builder({ id: "000102030405060708090a0b0c0d0e0f", creationDate: "D:20261030120000Z" }); const page = b.page("A6"); page.surface().text("one", { font: font(), size: 12, at: [10, 20] }); page.surface().text("two", { font: font(), size: 12, at: [10, 40] }); const noto = Font.fromBytes(read("fixtures/fonts/NotoSans-Regular.ttf")); page.surface().text("three", { font: noto, size: 12, at: [10, 60] }); page .surface() .text("four", { font: Font.fromBytes(read("fixtures/fonts/NotoSans-Regular.ttf")), size: 12, at: [10, 80] }); page.finish(); return b.finish().save(); }; const helvetica = Font.standard("Helvetica"); expect(build(() => Font.standard("Helvetica"))).toEqual(build(() => helvetica)); });
it("opens an encrypted upload with its password and reports a missing or wrong one by code", () => { const file = read("fixtures/encrypted/r6-aes-256-user.pdf"); expect(openUntrusted(file, "Pässwört-6")).toEqual({ ok: true, pages: 2, warnings: [] }); expect(openUntrusted(file)).toMatchObject({ ok: false, code: "password-required" }); expect(openUntrusted(file, "nope")).toMatchObject({ ok: false, code: "wrong-password" }); expect(openUntrusted(new TextEncoder().encode("not a PDF"))).toMatchObject({ ok: false, code: "not-pdf" }); });});- Coordinates are page space: points from the top-left corner of the displayed page, with y going down. A text run’s
at[1]is its baseline. - Free documents you’re done with (
doc.free(), orusing doc = …), so a long-running server doesn’t wait for the garbage collector to release WebAssembly memory.