EmbedPDF

Getting Started

Install the local engine package:

npm install @embedpdf/engine

Create and use the Engine#

localEngine() returns the engine itself — construction is synchronous and cheap, and it allocates nothing (no Worker, no WASM) until the first operation. The browser implementation runs PDFium in a Web Worker, and the default worker is bundler-portable (Vite, webpack 5 / Next, Rollup, Parcel), so there is no worker wiring to write. The example below opens a PDF from fetched bytes, reads the page list, and cleans up both handles.

Every engine method is async, so nothing changes at the call site whether the engine has booted or not: the first open() (or an explicit engine.warmup()) starts the boot in the background, and pending work simply resolves when PDFium is ready.

Who owns the engine#

Whoever creates the engine owns it and is responsible for engine.destroy(). With a viewer, ownership follows the shape of what you pass:

  • An instance is borrowed. <Viewer engine={engine} /> uses your engine and never destroys it. This is the common path — a module-scope const engine = localEngine() shared across viewers and route changes, alive for the lifetime of the app (so you typically never destroy it). The viewer calls engine.warmup() on mount, overlapping the PDFium boot with the rest of app initialization.
  • A thunk is viewer-owned. <Viewer engine={() => localEngine()} /> makes the viewer create the engine on mount and destroy it on unmount — per-mount isolation with automatic cleanup.

Fallback fonts#

Runtime fonts are boot configuration — pass them to localEngine() and they are guaranteed to be registered before any document work runs (font URLs are fetched in parallel with the worker boot):

const engine = localEngine({
  fallbackFonts: [{ key: 'noto-cjk', familyName: 'Noto Sans CJK', url: '/fonts/NotoSansCJK.ttf' }],
});

fallbackFonts are registered and added to the glyph-fallback chain (used to substitute missing glyphs when rendering and generating appearances). Use fonts for fonts you only reference explicitly (e.g. a FreeText fontFamily) without affecting automatic substitution. Give each font either inline data or a url fetched at boot.

Fonts can also be registered at any later moment — engine.fonts is the live font service on the local engine:

const handle = await engine.fonts.register({ key: 'brand', data: bytes });
await engine.fonts.addFallback(handle);

Local vs cloud#

@cloudpdf/engine exposes a matching cloudEngine() with identical ownership rules, so swapping local for cloud is a one-import change:

import { cloudEngine } from '@cloudpdf/engine';
 
const engine = cloudEngine({ baseUrl: 'https://pdf.example.com', token });

Two deliberate differences surface the split:

  • What open() accepts. Local opens { kind: 'bytes' }; cloud opens { kind: 'id' } / { kind: 'token' }, addressing server-side documents.
  • Fonts. cloudEngine() has no fallbackFonts option — fallback fonts are a server policy on the cloud (engine.fonts is undefined there).

Server-side rendering (Next, Nuxt, SvelteKit)#

An engine constructed at module scope does no work on the server — it allocates no Worker and touches no WASM until something uses it, which only happens in the browser (a viewer’s mount effect, or your own first open() in a client component). So a module-level const engine = localEngine() is SSR-safe. In the Next.js App Router, mark the component that renders the viewer 'use client'.

Custom workers#

For a custom worker setup — a strict CSP, a bundler without new URL worker support, a shared worker — pass your own as a () => Worker thunk (called once, when the engine boots):

localEngine({ worker: () => new Worker(/* … */) });

The raw worker source is also published at @embedpdf/engine/worker-entry (Vite: @embedpdf/engine/worker-entry?worker).

Lifecycle rules#

  1. One engine serves the documents of one application scope; reuse it.
  2. Close each document handle when the document leaves your application.
  3. Destroy the engine when you own it and no longer need PDF services (viewers do this for thunk-created engines automatically; module-scope singletons usually live for the app’s lifetime and never need it).

If you are building a viewer interface, continue with the Headless documentation instead of managing the Engine alone.

Was this page helpful?

Your feedback goes directly to the documentation team.