Skip to content

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:

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();
}
}

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.

packages/pdf/test/examples.test.ts
// 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(), or using doc = …), so a long-running server doesn’t wait for the garbage collector to release WebAssembly memory.