EmbedPDF

Opening documents

Give the viewer the documents to open when it starts, and <DocumentGate> shows your content once the document is ready:

initial-documents.vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import { RenderLayer } from '@embedpdf/vue/render';
import { Stage } from '@embedpdf/vue/stage';
import { engine, plugins } from './pdf';
import Spinner from './Spinner.vue';
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :initial-documents="[{ source: { kind: 'url', url: '/files/contract.pdf' } }]"
  >
    <DocumentGate>
      <template #fallback><Spinner /></template>
      <Stage>
        <template #page><RenderLayer /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>

The viewer is on screen at once; the document opens in the background. Components outside <DocumentGate>, such as your toolbar’s frame, render right away.

Open a document later#

useDocuments() opens documents while the app runs, for example one the user picked:

open-button.vue
<script setup lang="ts">
import { useDocuments } from '@embedpdf/vue/runtime';

const documents = useDocuments();

async function onPick(event: Event) {
  const file = (event.target as HTMLInputElement).files![0];
  await documents.open({ kind: 'bytes', bytes: await file.arrayBuffer() }, { name: file.name });
}
</script>

<template>
  <input type="file" accept="application/pdf" @change="onPick" />
</template>

open() resolves with { document } once it’s open, and the new document becomes the active one. Pass { activate: false } to open it in the background. Pick a PDF of your own here:

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { localEngine } from '@embedpdf/engine';
import Empty from './Empty.vue';
import Toolbar from './Toolbar.vue';

import '../open.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];

const welcome = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/welcome.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :initial-documents="[{ source: welcome, name: 'welcome.pdf' }]"
  >
    <Toolbar :welcome="welcome" />
    <DocumentGate>
      <template #fallback><Empty /></template>
      <Stage class="stage">
        <template #page><RenderLayer /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>

Where a document comes from#

SourceWhat it is
{ kind: 'url', url }A file the viewer fetches. The URL can be relative to your site.
{ kind: 'bytes', bytes }A file you already have: an upload, a file picker, a download.
async (signal) => sourceAnything else: a function that returns one of the above.

The viewer calls a function under the document’s loading state, and signal fires if the user closes the document first, so pass it on to what the function waits for.

Files that need a sign-in#

When your server wants a header or a cookie for a file, give the viewer your own fetch. Every { kind: 'url' } document downloads through it: it gets the URL, and an init with the document’s cancel signal to pass on.

fetch-headers.vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import { RenderLayer } from '@embedpdf/vue/render';
import { Stage } from '@embedpdf/vue/stage';
import { getToken } from './auth';
import { engine, plugins } from './pdf';

const withToken = (url: string, init: RequestInit) =>
  fetch(url, { ...init, headers: { Authorization: `Bearer ${getToken()}` } });
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :initial-documents="[{ source: { kind: 'url', url: '/api/files/contract.pdf' } }]"
    :fetch="withToken"
  >
    <DocumentGate>
      <Stage>
        <template #page><RenderLayer /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>

For your cookies on another site, add credentials: 'include' instead; that site’s CORS headers have to allow it. A server that answers 401 or 403 makes the document’s error permission-denied, and documents.retry(id) calls your fetch again, so a token it reads when it’s called is a fresh one. The engine’s own files never go through it.

Loading, errors and passwords#

<DocumentGate> has something to show for each state a document can be in:

gate-states.vue
<script setup lang="ts">
import { DocumentGate } from '@embedpdf/vue/runtime';
import { RenderLayer } from '@embedpdf/vue/render';
import { Stage } from '@embedpdf/vue/stage';
import PasswordForm from './PasswordForm.vue';
import Spinner from './Spinner.vue';
</script>

<template>
  <DocumentGate>
    <template #fallback><Spinner /></template>
    <template #locked="{ document }"><PasswordForm :document="document" /></template>
    <template #error="{ document }">
      <p>Couldn't open {{ document.name }}: {{ document.error.message }}</p>
    </template>
    <Stage>
      <template #page><RenderLayer /></template>
    </Stage>
  </DocumentGate>
</template>

A password-protected PDF opens locked. Ask for the password and unlock it:

password-form.vue
<script setup lang="ts">
import { ref } from 'vue';
import { useDocuments, type DocumentInfo } from '@embedpdf/vue/runtime';
import PasswordPrompt from './PasswordPrompt.vue';

const props = defineProps<{ document: DocumentInfo }>();
const documents = useDocuments();
const wrong = ref(false);

async function submit(password: string) {
  try {
    await documents.unlock(props.document.id, { password });
  } catch {
    wrong.value = true; // it stays locked; ask again
  }
}
</script>

<template>
  <PasswordPrompt :wrong="wrong" @submit="submit" />
</template>

After an error, such as a network failure, documents.retry(id) tries again. Here the second document isn’t a PDF, so it shows its error:

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { localEngine } from '@embedpdf/engine';
import OpenError from './OpenError.vue';
import PasswordForm from './PasswordForm.vue';
import Tabs from './Tabs.vue';

import '../states.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];

const welcome = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/welcome.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// A file that isn't a PDF, so its document can't be opened.
const broken: OpenInput = { kind: 'bytes', bytes: new TextEncoder().encode('Not a PDF') };
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :initial-documents="[
      { source: welcome, name: 'welcome.pdf' },
      { source: broken, name: 'broken.pdf', active: true },
    ]"
  >
    <Tabs />
    <DocumentGate>
      <template #fallback><div class="panel">Opening…</div></template>
      <template #locked="{ document }"><PasswordForm :document="document" /></template>
      <template #error="{ document }"><OpenError :document="document" /></template>
      <Stage class="stage">
        <template #page><RenderLayer /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>

To show the state yourself, for example on a tab, useDocument() gives you the document’s status: 'loading', 'locked', 'ready' or 'error'. Every field is listed under State.

Once a document is 'ready', each page’s picture can still take a moment to arrive. To show that too, see Show that a page is loading.

Who the user is and what they may do#

Tell the viewer who the user is and what they may do, once. Every document it opens uses them:

identity-scope.vue
<script setup lang="ts">
import { Viewer } from '@embedpdf/vue/runtime';
import { engine, plugins } from './pdf';

const identity = { userId: 'u_381', displayName: 'Dana Smith' };
const scope = ['doc.open', 'doc.render', 'doc.text.select', 'doc.annotate.read', 'annotations:create:self'];
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :identity="identity" :scope="scope" />
</template>
  • identity is who the user is. Their displayName becomes the author of the annotations they make.
  • scope is what they may do, as a list of permissions. Leave it out and they may do what the file allows: everything, unless the PDF restricts itself, as in Acrobat.

Loading live preview…

<script setup lang="ts">
import { ref } from 'vue';
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { localEngine } from '@embedpdf/engine';
import { roles, type Role } from './roles';
import Toolbar from './Toolbar.vue';

import '../identity.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];

const welcome = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/welcome.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const dana = { userId: 'u_381', displayName: 'Dana Smith' };
const role = ref<Role>('reader');
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :identity="dana"
    :scope="roles[role]"
    :initial-documents="[{ source: welcome, name: 'welcome.pdf' }]"
  >
    <Toolbar v-model:role="role" :welcome="welcome" />
    <DocumentGate>
      <template #fallback><p class="loading">Opening…</p></template>
      <Stage class="stage">
        <template #page><RenderLayer /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
Change them on the viewer, and the documents you open after that use the new values; documents already open keep theirs.

A document can have its own, which replace the viewer’s for that document:

const initialDocuments = [{ source, scope: ['doc.open', 'doc.render'] }]; // this one is read-only
await documents.open(source, { scope: ['doc.open', 'doc.render'] }); // and so is this one

Permissions lists every permission.

Close a document#

await documents.close(document.id);

Closing frees the document’s memory in the engine. The next document becomes the active one.

Methods#

What useDocuments() gives you for opening and closing documents and reading their pages. Tabs and the active document are on Several documents, downloading and printing on Saving & downloading.

MethodWhat it does
open(source, options?)Opens a document and makes it the active one. Resolves { document } once it’s ready, or locked with a password; rejects when it can’t be opened. Open a document later
canOpenFiles()Whether open() takes a file’s bytes, such as a file the user picked, or a URL it downloads: true on the local engine, false on CloudPDF, where every document comes from your server
openAll(documents)Opens several documents, as initialDocuments does: every tab appears at once, in order, and the one marked active (or the first) becomes the active one. A failure shows in that document’s status instead of throwing.
unlock(id, { password })Unlocks a locked document. Rejects on a wrong password, and the document stays locked so you can ask again. Loading, errors and passwords
retry(id, options?)Tries a failed document again, with the same source and options. Resolves like open(). A document from a grant takes what it waited for: { passphrase } or { identity }. Loading, errors and passwords
identify(id, answers)Answers what the document’s grant asks before the first change (its identityRequest): the change waiting goes out. Rejects while a required answer is missing or one can’t be taken, and the change keeps waiting.
declineIdentity(id)Takes back the change waiting for the visitor’s answers; the next change asks again.
close(id), closeAll()Closes one document, or every document, and frees its memory. Close a document
get(id?)A document’s fields, the ones useDocument() gives you, or null when it isn’t open. Without an id, the document in scope
listPages(id?)The document’s pages, in order. In a component, use usePageList().
getPage(page, id?)One page, by its ref or its index, or null when the document doesn’t have it
getPageIndex(ref, id?)A page’s position, from 0, or -1 when the document doesn’t have it
getRevision(id?)A number that goes up each time pages are added, removed, moved or rotated; -1 with no document

options for open() are name (what its tab shows), activate: false (open it in the background), password, identity (who your page knows the user is), and, on the local engine, scope. Without an id, the page methods use the document in scope: the one a <DocumentScope> around your component names, or else the active one. To stop an open that’s still loading, close the document: that cancels its download too.

State#

useDocument() gives you the document a component talks to: the active one, or the one its <DocumentScope> names. Its fields are refs: your template updates when one changes. Outside a component, get(id) gives the same fields. With no document open yet, it reads as one still on its way: an empty id and status: 'loading', so a component that shows it never has to check for null.

FieldWhat it is
idThe document’s id, for close(id), unlock(id, …) and <DocumentScope>
nameThe name its tab shows, from open() or rename()
status'loading', 'locked', 'ready' or 'error'
pageCountHow many pages it has; 0 until it’s ready
hasUnsavedChangesWhether it has changes that weren’t downloaded yet. Warn before changes are lost
passwordProvidedWhen it’s locked: whether a password was tried and was wrong
errorWhy it couldn’t be opened, when status is 'error': { code, message }
grantRequestWhen it’s from a grant: the passphrase or the questions it waits for, to open or to go on
identityRequestWhen it’s ready and a change waits for the visitor’s answers to the grant’s questions

Every open document, with the same fields, comes from useDocumentsState().

Events#

open-failed.vue
<script setup lang="ts">
import { ref } from 'vue';
import { useDocumentsEvent } from '@embedpdf/vue/runtime';

const message = ref('');

useDocumentsEvent(
  (documents) => documents.onOpenFailed,
  ({ error }) => (message.value = `Couldn't open the file: ${error.message}`),
);
</script>

<template>
  <p role="alert">{{ message }}</p>
</template>
EventFires whenCarries
onOpenedA document is open and readydocumentId, info
onOpenFailedA document couldn’t be openeddocumentId, error
onLockedA document needs a passworddocumentId, passwordProvided
onClosedA document was closeddocumentId
onPagesChangedPages were added, removed, moved or rotateddocumentId, pages, revision

Switching the active document is on Several documents.

Good to know#

Opening large files#

The local engine needs the whole file before it can show the first page, so a large PDF on a slow connection takes as long as its download.

initialDocuments is read once#

Like engine and plugins, the viewer reads initialDocuments when it mounts. To open another document later, use documents.open().

Several documents#

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

Was this page helpful?

Your feedback goes directly to the documentation team.