EmbedPDF

Moving from 2.x

3.0’s viewer replaces the one in EmbedPDF 2.x: @embedpdf/snippet, and the @embedpdf/react-pdf-viewer, @embedpdf/vue-pdf-viewer and @embedpdf/svelte-pdf-viewer packages. It’s still one tag and one config, but most names changed. Every config key is a prop, the toolbar is a layout you change in the config, and you reach each plugin by its name. This page maps each 2.x name to its 3.0 one.

The packages#

2.x3.0
@embedpdf/snippet@embedpdf/viewer
https://cdn.jsdelivr.net/npm/@embedpdf/snippet@2/dist/embedpdf.jshttps://cdn.jsdelivr.net/npm/@embedpdf/viewer@3/dist/embedpdf.js
@embedpdf/react-pdf-viewer@embedpdf/viewer-react
@embedpdf/vue-pdf-viewer@embedpdf/viewer-vue
@embedpdf/svelte-pdf-viewer@embedpdf/viewer-svelte
@embedpdf/snippet, called from an Angular component@embedpdf/viewer-angular

Remove the 2.x package, and install the new one:

install.sh
npm install @embedpdf/viewer-svelte@next

The new package brings the PDF engine and its WebAssembly. If you installed @embedpdf/engines, @embedpdf/pdfium or @embedpdf/models only for the 2.x viewer, remove them too.

The viewer#

The 2.x code is the comment at the top. The 3.0 code below it does the same:

report.svelte
<!-- 2.x:

<script lang="ts">
  import { PDFViewer, type PluginRegistry } from '@embedpdf/svelte-pdf-viewer';

  const onready = (registry: PluginRegistry) => console.log('ready', registry);
</script>

<PDFViewer
  config={{
    src: '/report.pdf',
    theme: { preference: 'dark' },
    disabledCategories: ['redaction', 'document-print'],
  }}
  style="height: 100vh"
  {onready}
/>
-->

<script lang="ts">
  import { PDFViewer, type Viewer } from '@embedpdf/viewer-svelte';

  const onready = (viewer: Viewer) => console.log('ready', viewer);
</script>

<PDFViewer
  src="/report.pdf"
  theme="dark"
  disable={['redaction', 'documents.print']}
  style="height: 100vh"
  {onready}
/>
  • No config prop. Each key is a prop of its own. A config you keep in an object still works: spread it into the viewer, as in the Quick start. In plain HTML, init() still takes the whole config, without type: 'container', and target can be a selector.
  • onReady gives you the viewer, not the plugin registry. See From code.
  • The viewer is one element, <embedpdf-viewer>. 2.x’s framework packages put an <embedpdf-container> inside a <div> of their own. Now there’s no <div>: your class and style go on the viewer itself.
  • Five keys follow their prop: src, theme, locale, disable and readOnly. 2.x’s framework packages read the config once. Every other key is still read once: see Change the config later.
  • Server-side rendering needs no wrapper. Every framework package is safe to import on the server, so a <ClientOnly> or a dynamic import around the viewer can go. See Setup.

The config, key by key#

2.x3.0
srcsrc: a URL, a File or bytes
documentManager.initialDocumentsdocuments, each with src, name, password and active, in place of url, name, password and autoActivate
tabBarlayout.tabs() in the layout: 'always', 'multiple' (the default) or 'never'
themetheme, with new keys: see Theme
iconsicons, as path data: see Buttons and the toolbar
commands.commandscommands, a list
ui.schemalayout
disabledCategoriesdisable, with new names: see Turn features off
permissionsscope: see Permissions
i18n.defaultLocalelocale
i18n.localesstrings: words by language code, with a button’s label under its command’s id
annotations.annotationAuthoridentity: { displayName }
wasmUrlengine.wasmUrl. The file is embedpdf.wasm now, from @embedpdf/engine-runtime-wasm32, in place of pdfium.wasm.
fontFallbackengine.fallbackFonts: font files, each with a key and a url
fonts.uitheme.tokens.font
fonts.signatureplugins.signature.fonts: font files, each with a key, a label and a url
stamp.manifestsplugins.stamp.standard: true, a URL, or false for none
Every other plugin’s key: zoom, scroll, annotations, …plugins, under the 3.0 plugin’s name: see Plugin settings

Plugin settings#

The plugins’ settings are new too. plugins takes them under the 3.0 plugin’s name, as its page in the headless docs lists them, with plain strings where 2.x had enums. The ones 2.x apps set most:

2.x3.0
zoom: { defaultZoomLevel: ZoomMode.FitWidth }stage: { zoom: { mode: 'fit-width' } }
scroll: { defaultStrategy: ScrollStrategy.Horizontal }stage: { layout: 'horizontal' }
scroll: { defaultPageGap: 20 }stage: { gap: 20 }
spread: { defaultSpreadMode: SpreadMode.Odd }stage: { spread: 'odd' }
rotation: { defaultRotation: Rotation.Degree90 }stage: { viewRotation: 90 }
pan: { defaultMode: 'always' }interaction: { defaultTool: 'pan' }
thumbnails: { width: 120 }thumbnails: { zoom: { pageWidth: 120 } }
annotations: { selectAfterCreate: false }annotation: { afterCreate: { select: false } }
annotations: { deactivateToolAfterCreate: true }annotation: { afterCreate: { tool: 'default' } }
stamp: { manifests: [] }stamp: { standard: false }

All of them in one 3.0 config:

{
  plugins: {
    stage: {
      zoom: { mode: 'fit-width' },
      layout: 'horizontal',
      gap: 20,
      spread: 'odd',
      viewRotation: 90,
    },
    interaction: { defaultTool: 'pan' },
    thumbnails: { zoom: { pageWidth: 120 } },
    annotation: { afterCreate: { select: false, tool: 'default' } },
    stamp: { standard: false },
  },
}

Plugins links each plugin to its settings.

From code#

print-button.svelte
<!-- 2.x:

<script lang="ts">
  import { PDFViewer, type CommandsPlugin, type PluginRegistry } from '@embedpdf/svelte-pdf-viewer';

  let registry = $state.raw<PluginRegistry | null>(null);

  const print = () =>
    registry?.getPlugin<CommandsPlugin>('commands')?.provides().execute('document:print');
</script>

<button disabled={!registry} onclick={print}>Print</button>
<PDFViewer config={{ src: '/report.pdf' }} style="height: 80vh" onready={(r) => (registry = r)} />
-->

<script lang="ts">
  import { PDFViewer, type Viewer } from '@embedpdf/viewer-svelte';

  let viewer = $state.raw<Viewer | null>(null);
</script>

<button disabled={!viewer} onclick={() => viewer?.commands.execute('document:print')}>Print</button>
<PDFViewer src="/report.pdf" onready={(v) => (viewer = v)} style="height: 80vh" />

onReady gives you the viewer once it’s live. viewer.get('search') is what registry.getPlugin('search').provides() gave you in 2.x, and viewer.documents and viewer.commands need no call at all.

2.x3.0
await container.registry, or a ref’s registryonReady. In plain HTML, also element.viewer once the element fires epdf:ready.
registry.getPlugin(id).provides()viewer.get(name)
.forDocument(documentId) on a pluginviewer.forDocument(documentId).get(name)
onInit, with the containerNone: what you did with the container is config now (theme, icons)
registry.getEngine()None: the viewer keeps its engine. Read the document through the plugins instead, such as viewer.get('metadata'), and get the file with viewer.documents.download().

The plugins have new names:

2.x plugin3.0 plugin
zoom, scroll, spread, rotate, viewportstage
render, tilingrender
thumbnailthumbnails
document-manager, exportdocuments
interaction-manager, paninteraction, whose pan tool is the hand
uishell for what’s open, and the config’s layout for what goes where
print, fullscreen, historyNo plugin: run their commands, document:print, view:fullscreen, history:undo and history:redo
annotation, search, selection, form, redaction, stamp, signature, i18n, commandsThe same name

Their methods are new as well, each on its plugin’s page in the headless docs. The calls 2.x code makes most:

2.x3.0
openDocumentUrl({ url })viewer.documents.open({ kind: 'url', url })
openDocumentBuffer({ buffer, name })viewer.documents.open({ kind: 'bytes', bytes: buffer }, { name })
setActiveDocument(id), closeDocument(id)viewer.documents.setActive(id), viewer.documents.close(id)
onDocumentOpened(callback)viewer.documents.onOpened(({ documentId }) => …)
saveAsCopy()viewer.documents.download(): the PDF’s bytes
download()viewer.commands.execute('document:download')
scrollToPage({ pageNumber: 5 })viewer.get('stage').goToPage(4): pages count from 0
setLocale('de')The locale prop

Theme#

2.x set colors per mode, in groups: theme.light.accent.primary. 3.0 has one set of tokens for both modes, and dark for what’s different in dark mode:

{
  theme: {
    preference: 'system',
    accent: '#9333ea',
    tokens: { canvas: '#f5f3ff' },
    dark: { accent: '#a855f7', canvas: '#1e1b2e' },
  },
}

2.x’s light colors go in tokens, and its dark ones in dark:

2.x color3.0 token
accent.primaryaccent
background.appcanvas
background.surfacesurface
background.elevatedsurface-raised
foreground.primarytext
foreground.secondarytext-muted
border.defaultborder
interactive.hoverhover

The other 2.x colors, such as accent.primaryHover or state.error, have no token. For what tokens don’t cover, restyle a part with ::part(): see Theming. The container’s setTheme() is the theme prop now, which follows its value.

Buttons and the toolbar#

2.x changed the toolbar after the viewer started: registerCommand(), registerIcon(), then mergeSchema() with a copy of the toolbar’s items. In 3.0 all three are config, and the layout is a function that gets the default and returns yours. Here is 2.x’s example, a smiley in place of the comments button:

{
  icons: {
    smiley: [
      'M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0',
      'M9 10l.01 0',
      'M15 10l.01 0',
      'M9.5 15a3.5 3.5 0 0 0 5 0',
    ],
  },
  commands: [{ id: 'acme:hello', label: 'Say hello', icon: 'smiley', run: () => alert('Hello!') }],
  layout: (layout) => layout.replace('panel:comments', 'acme:hello'),
}
2.x3.0
commands.registerCommand(command)commands: [command]
A command’s actionrun
disabledenabled, which says the opposite
shortcutsshortcut
registry and state, in a command’s functionsget, which gives you any plugin by its name
The container’s registerIcon(name, { viewBox, paths })icons: 24 × 24 path data, each path a string
ui.mergeSchema({ toolbars })layout: (layout) => layout.add(…), .remove(…), .replace(…)
ui.toggleMenu() in a commandmenu: 'id' on the command
A { type: 'custom', componentId } item{ custom: 'id' }, with your own component

A command’s label, labelKey, icon, active, visible and categories keep their names. The ids changed:

2.x3.0
main-toolbarmain
annotation-toolbar, shapes-toolbar, insert-toolbar, form-toolbar, redaction-toolbarannotate, shapes, insert, form, redact
document-menu, zoom-menu, page-settings-menudocument, zoom, view
The selection and annotation selection menusThe selection and annotation bars
document:export, document:fullscreendocument:download, view:fullscreen
annotation:add-highlight, panel:toggle-search, scroll:next-pagetool:highlight, panel:search, page:next

Every 3.0 id is on Layout and Buttons & commands.

Turn features off#

disabledCategories is disable. It takes 3.0’s features, with their parts after a dot, and single commands by their id:

2.x3.0
annotationannotations and shapes: 2.x’s shapes were annotations
annotation-highlightannotations.highlight
annotation-inkannotations.drawing
annotation-shapeshapes
insert-rubber-stamp, insert-imagestamps
insert-signaturesignatures.signing
formforms
redactionredaction
panel-searchsearch
panel-commentcomments
selection-copytext.copy
document-open, document-export, document-printdocuments.open, documents.download, documents.print
zoom, rotate, spread, scroll, panNo feature: remove their buttons, or disable each command, such as 'zoom:in'

New in 3.0, readOnly: true turns off everything that changes the document.

Permissions#

2.x’s permissions changed what the viewer offered. 3.0’s scope is the list of what the user may do, and the engine checks it on every call:

2.x3.0
No permissions: the PDF’s own restrictions countNo scope: the same
enforceDocumentPermissions: falsescope: ['*']: the user may do everything
overrides: { print: true }'doc.print' added to the default scope, which Permissions lists
overrides: { print: false }A scope that leaves out both 'doc.print' and 'pdf.permissions', or, for the button only, disable: ['documents.print']

2.x’s print is doc.print in a scope, printHighQuality is doc.print.high, copyContents is doc.text.copy, modifyAnnotations is doc.annotate.modify, fillForms is doc.forms.fill and assembleDocument is doc.pages.assemble. See Permissions.

Security and self-hosting#

Nothing from a third party#

2.x fetched several files from public servers. 3.0 fetches nothing from a third party unless you point it there:

2.x fetched3.0
pdfium.wasm, from jsDelivrembedpdf.wasm, a file of your build
Fallback fonts, from jsDelivrNone, unless you give engine.fallbackFonts
The UI font and the signature fonts, from Google FontsThe system font. The signature fonts are lazy chunks of your build.
The standard stamps, from jsDelivrA lazy chunk of your build

So a PDF whose text needs a font it doesn’t embed, such as Chinese or Japanese, needs a fallback font you give the engine: see Fonts. Every file and its setting is on Self-hosting.

Content Security Policy#

2.x drew its styles from a <style> element in its shadow root, so a strict policy needed style-src 'unsafe-inline', and it started the engine’s workers from blob: URLs. 3.0 needs no 'unsafe-inline' or 'unsafe-eval' and runs with Trusted Types on. Installed from npm, its workers are files of your build, under worker-src 'self'; from the CDN they still start from blob: URLs. The policy to copy is on Security & CSP.

The page around it#

  • 2.x added its font stylesheets to your page’s <head>. 3.0 adds nothing to your page: its styles stay in its own shadow DOM.
  • Menus and tooltips open in the browser’s top layer, so your page’s headers never cover them, while panels and dialogs stay inside the viewer’s box. See How it works.
  • 3.0 needs Chrome and Edge 114, Firefox 125 or Safari 17, or newer: see Setup.

Not in 3.0#

What 2.x had that 3.0 doesn’t:

  • Loading in parts. 2.x’s mode: 'range-request' fetched a big file a piece at a time. The built-in engine downloads the whole file first: see Big files.
  • Request options. For headers or credentials (requestOptions), fetch the file yourself and give src its bytes or a File.
  • A limit on open documents (maxDocuments).
  • The engine on the page’s own thread (worker: false), and its log (log). The engine always runs in a worker.
  • Zoom limits and steps: minZoom, maxZoom, zoomStep, zoomRanges and presets.
  • Marquee zoom, and the screenshot tool (the capture plugin).
  • Protect. 2.x’s Protect dialog, setDocumentEncryption() and removeEncryption(): 3.0 can’t add or remove a document’s password.
  • The outline and attachments panels, for now. The engine doesn’t read a document’s outline yet, and no plugin lists its attached files, so these two panels don’t show (2.x’s bookmark and attachment plugins).
  • Swedish and Traditional Chinese. They aren’t built in: add them with strings.
  • Theme events: themechange, activeColorScheme and themePreference.
  • A few settings: export.defaultFileName, fullscreen.targetElement, i18n.fallbackLocale (English is the fallback) and i18n.paramResolvers.

Next steps#

Was this page helpful?

Your feedback goes directly to the documentation team.