EmbedPDF

Setup

A headless viewer needs two packages: the one for your framework, and an engine. This page installs them, creates the engine for your whole app, and covers server-side rendering, bundlers and fonts.

Install#

npm install @embedpdf/svelte@next @embedpdf/engine@next

The framework package holds every plugin, one import path each (@embedpdf/svelte/search, @embedpdf/svelte/annotation). A plugin you don’t import isn’t in your bundle.

It’s written for Svelte 5: components that take snippets, and functions that give you reactive state for your runes.

Create the engine once#

Create one engine when your app starts, and share it with every viewer:

// pdf.ts
import { localEngine } from '@embedpdf/engine';
 
export const engine = localEngine();

Creating the engine is instant and allocates nothing. The first viewer that uses it starts it, and PDFium boots in a Web Worker in the background.

<Viewer> can also own the engine. Pass a function, and the viewer creates the engine when it mounts and destroys it when it unmounts:

owned-engine.svelte
<script lang="ts">
  import { Viewer } from '@embedpdf/svelte/runtime';
  import { localEngine } from '@embedpdf/engine';
  import { plugins } from './pdf';
</script>

<Viewer engine={() => localEngine()} {plugins} />

An engine you pass in yourself is never destroyed by the viewer, so one engine can serve every viewer in your app.

Register the plugins#

Plugins are a list you create once, next to the engine:

plugins.ts
import { stagePlugin } from '@embedpdf/svelte/stage';
import { renderPlugin } from '@embedpdf/svelte/render';
import { interactionPlugin } from '@embedpdf/svelte/interaction';
import { selectionPlugin } from '@embedpdf/svelte/selection';

export const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), selectionPlugin()];

The viewer reads engine and plugins once, when it mounts, and ignores later changes to them.

Order doesn’t matter, and a plugin that needs another says so when the app starts: the annotation plugin, for example, needs the interaction plugin.

Server-side rendering#

The headless packages render nothing on the server that needs the engine: the engine only starts in the browser. So a module-level engine is safe everywhere.

In SvelteKit, render the viewer in the browser only, for example behind {#if browser} with browser from $app/environment:

ssr.svelte
<!-- src/routes/contract/+page.svelte -->
<script lang="ts">
  import { browser } from '$app/environment';
  import DocumentViewer from '$lib/DocumentViewer.svelte';
</script>

{#if browser}
  <DocumentViewer />
{:else}
  <div style="height: 600px">Loading the viewer…</div>
{/if}

To leave a whole page out of server rendering, add export const ssr = false to its +page.ts.

Bundlers and self-hosting#

The engine’s worker and WebAssembly ship inside the package, and your build picks them up.

Vite, and so SvelteKit, needs no configuration, and neither do webpack 5 and Rspack.

There’s nothing to copy, and nothing is fetched from a CDN. The engine loads from your own site, so it works offline and behind a firewall.

If your setup can’t bundle a worker, for example with a strict Content Security Policy, host the files yourself: Self-hosting lists every option, with notes for Parcel and plain esbuild.

Fonts#

Text the engine draws, such as a text box or a filled form field, needs a font with its characters. For languages the built-in fonts don’t cover, give the engine a fallback font:

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

Fonts covers registering fonts and using them by name. Every other engine option is in the engine’s Setup.

Good to know#

One viewer, many documents#

A viewer can hold several open documents at once, like tabs. See Several documents.

Changing engine or plugins#

Both are read once. To use another engine or another set of plugins, mount a new <Viewer>, for example inside a {#key} block.

The ready-made Viewer uses these same packages. If you start there, everything on these pages still applies when you move to headless.

Was this page helpful?

Your feedback goes directly to the documentation team.