Getting Started
Install the local engine package:
npm install @embedpdf/engineCreate 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-scopeconst engine = localEngine()shared across viewers and route changes, alive for the lifetime of the app (so you typically never destroy it). The viewer callsengine.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 nofallbackFontsoption — fallback fonts are a server policy on the cloud (engine.fontsis 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#
- One engine serves the documents of one application scope; reuse it.
- Close each document handle when the document leaves your application.
- 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.
Your feedback goes directly to the documentation team.