-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started
modern-pdf-lib is a WASM-accelerated PDF creation engine for JavaScript. It runs in Node.js, Deno, Bun, Cloudflare Workers, and modern browsers — with a single unified ESM API across all runtimes.
Note:
modern-pdf-libis ESM + CJS dual format. Works with bothimportandrequire(). Node.js 25.7 or later is required.
Install with your preferred package manager:
| Package Manager | Command |
|---|---|
| npm | npm install modern-pdf-lib |
| pnpm | pnpm add modern-pdf-lib |
| Bun | bun add modern-pdf-lib |
| Deno | deno add npm:modern-pdf-lib |
All exports are available from the top-level package import:
import { createPdf, PageSizes, rgb } from 'modern-pdf-lib';Five lines to a working PDF:
import { createPdf, PageSizes, rgb } from 'modern-pdf-lib';
const pdf = createPdf();
const page = pdf.addPage(PageSizes.A4);
page.drawText('Hello, World!', { x: 72, y: 720, size: 36, color: rgb(0, 0, 0) });
const bytes = await pdf.save(); // Uint8ArrayThat's it. bytes is a fully valid PDF file you can write to disk, send over the network, or turn into a Blob.
import { writeFile } from 'node:fs/promises';
import { createPdf, PageSizes, rgb } from 'modern-pdf-lib';
const pdf = createPdf();
const page = pdf.addPage(PageSizes.A4);
page.drawText('Hello from Node.js', { x: 72, y: 720, size: 24, color: rgb(0, 0, 0) });
const bytes = await pdf.save();
await writeFile('output.pdf', bytes);
console.log('Saved output.pdf');Use saveAsStream() when you want to pipe a large PDF directly into an HTTP response or a file stream without buffering the entire document in memory.
import { createWriteStream } from 'node:fs';
import { Writable } from 'node:stream';
import { createPdf, PageSizes, rgb } from 'modern-pdf-lib';
const pdf = createPdf();
const page = pdf.addPage(PageSizes.A4);
page.drawText('Streamed PDF', { x: 72, y: 720, size: 24, color: rgb(0.2, 0.4, 0.8) });
const readable = pdf.saveAsStream(); // ReadableStream (Web Streams API)
const dest = createWriteStream('streamed-output.pdf');
await readable.pipeTo(Writable.toWeb(dest));
console.log('Streamed to streamed-output.pdf');Tip:
saveAsStream()returns a standard Web Streams APIReadableStream, so it works identically across Node.js 25.7+, Deno, and browsers.
import { createPdf, PageSizes, rgb } from 'modern-pdf-lib';
async function downloadPdf() {
const pdf = createPdf();
const page = pdf.addPage(PageSizes.A4);
page.drawText('Hello from the Browser', { x: 72, y: 720, size: 24, color: rgb(0, 0, 0) });
const blob = await pdf.saveAsBlob(); // Blob with MIME type 'application/pdf'
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = 'document.pdf';
anchor.click();
URL.revokeObjectURL(url);
}Note:
saveAsBlob()is only available in browser environments. Calling it in Node.js or a Worker runtime will throw an error. Usesave()orsaveAsStream()in those environments instead.
import { createPdf, PageSizes, rgb } from 'modern-pdf-lib';
export default {
async fetch(request) {
const pdf = createPdf();
const page = pdf.addPage(PageSizes.A4);
page.drawText('Generated by Cloudflare Workers', {
x: 72,
y: 720,
size: 20,
color: rgb(0.1, 0.1, 0.1),
});
const stream = pdf.saveAsStream();
return new Response(stream, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="worker-output.pdf"',
},
});
},
};Tip: Streaming with
saveAsStream()avoids materializing the full PDF buffer in the Worker's limited memory — prefer it oversave()for large documents.
import { createPdf, PageSizes, rgb } from 'npm:modern-pdf-lib';
const pdf = createPdf();
const page = pdf.addPage(PageSizes.A4);
page.drawText('Hello from Deno', { x: 72, y: 720, size: 24, color: rgb(0, 0.5, 0.8) });
const bytes = await pdf.save();
await Deno.writeFile('output.pdf', bytes);
console.log('Saved output.pdf');modern-pdf-lib ships with an optional WebAssembly module that accelerates:
- Font subsetting — dramatically reduces file size for embedded fonts
- Text shaping — correct glyph ordering for complex scripts (Arabic, Indic, etc.)
By default, the library initializes the WASM module automatically on first use. No configuration is required for most projects.
// WASM is loaded lazily — no setup needed
const pdf = createPdf();
const page = pdf.addPage(PageSizes.A4);
page.drawText('WASM kicks in automatically', { x: 72, y: 720, size: 18 });
const bytes = await pdf.save();If you need fine-grained control — for example, to pre-warm the WASM engine during application startup or to supply a custom .wasm URL — use initWasm():
import { initWasm, createPdf, PageSizes, rgb } from 'modern-pdf-lib';
// Call once, before any PDF operations.
// Resolves when the WASM module is fully loaded and compiled.
await initWasm({
// Optional: override the default .wasm asset URL.
// Useful when self-hosting assets or enforcing a CDN policy.
wasmUrl: 'https://your-cdn.example.com/modern-pdf-lib/modern-pdf-lib.wasm',
});
const pdf = createPdf();
const page = pdf.addPage(PageSizes.A4);
page.drawText('WASM is ready', { x: 72, y: 720, size: 24, color: rgb(0, 0.6, 0.2) });
const bytes = await pdf.save();Note: Calling
initWasm()more than once is safe — subsequent calls resolve immediately using the already-loaded module.
| Mode | How to use | When to choose |
|---|---|---|
| Auto | Just import and call createPdf()
|
Most projects — simple, zero config |
| Explicit |
await initWasm() before first use |
App startup pre-warming, custom CDN, test isolation |
Once you have a basic document working, explore the rest of the library:
| Topic | Wiki Page |
|---|---|
| Annotations | Annotations |
| Fonts & text | Font Subsetting |
| WASM acceleration | WASM Acceleration |
| Forms (AcroForms) | Forms |
| PDF merging & splitting | Merge and Split |
| Encryption | Security |
| Accessibility (PDF/UA) | Accessibility |
| Digital signatures | Security |
| Architecture | Architecture |
modern-pdf-lib v0.15.1 — ESM + CJS — Node 25.7+ / Deno / Bun / Cloudflare Workers / Browser