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.tsx
import { DocumentGate, Viewer } from '@embedpdf/react/runtime';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage } from '@embedpdf/react/stage';
import { engine, plugins } from './pdf';
import { Spinner } from './Spinner';

export function ContractViewer() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[{ source: { kind: 'url', url: '/files/contract.pdf' } }]}
    >
      <DocumentGate fallback={<Spinner />}>
        <Stage>{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}

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.tsx
import { useDocuments } from '@embedpdf/react/runtime';

export function OpenButton() {
  const documents = useDocuments();

  const onPick = async (file: File) => {
    await documents.open({ kind: 'bytes', bytes: await file.arrayBuffer() }, { name: file.name });
  };

  return <input type="file" accept="application/pdf" onChange={(e) => onPick(e.target.files![0])} />;
}

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…

import { Viewer, DocumentGate, useDocument, useDocuments } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

import './open.css';

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

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};

function Toolbar() {
  const documents = useDocuments();
  const { id, name, status, pageCount } = useDocument();

  // A file the user picks opens next to the ebook, and becomes the active document.
  const openFile = async (file: File) => {
    await documents.open({ kind: 'bytes', bytes: await file.arrayBuffer() }, { name: file.name });
  };

  return (
    <div className="toolbar">
      <span className="document">
        {id ? (
          <>
            <strong>{name ?? 'Untitled'}</strong>
            {status === 'ready' ? ` · ${pageCount} pages` : ` · ${status}`}
          </>
        ) : (
          'No document open'
        )}
      </span>
      <label className="button picker">
        Open a PDF…
        <input
          type="file"
          accept="application/pdf"
          onChange={(event) => {
            const file = event.target.files?.[0];
            if (file) void openFile(file);
            event.target.value = '';
          }}
        />
      </label>
      {id ? (
        <button type="button" className="button" onClick={() => documents.close(id)}>
          Close
        </button>
      ) : (
        <button
          type="button"
          className="button"
          onClick={() => documents.open(ebook, { name: 'ebook.pdf' })}
        >
          Open the ebook again
        </button>
      )}
    </div>
  );
}

// The gate's fallback: a document on its way, or none at all.
function Empty() {
  const { id } = useDocument();
  return (
    <p className="loading">{id ? 'Opening…' : 'No document is open. Open a PDF to see it here.'}</p>
  );
}

export default function App() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[{ source: ebook, name: 'ebook.pdf' }]}
    >
      <Toolbar />
      <DocumentGate fallback={<Empty />}>
        <Stage className="stage">{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}

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.

A function is how you add your own fetch, such as one with a login header. The viewer calls it under the document’s loading state, and signal cancels the download if the user closes the document first:

const source = async (signal: AbortSignal) => {
  const response = await fetch(`/api/files/${id}`, { headers: { authorization }, signal });
  return { kind: 'bytes', bytes: await response.arrayBuffer() };
};

Loading, errors and passwords#

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

gate-states.tsx
import { DocumentGate } from '@embedpdf/react/runtime';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage } from '@embedpdf/react/stage';
import { PasswordForm } from './PasswordForm';
import { Spinner } from './Spinner';

export function DocumentView() {
  return (
    <DocumentGate
      fallback={<Spinner />}
      locked={(document) => <PasswordForm document={document} />}
      error={(document) => <p>Couldn’t open {document.name}: {document.error.message}</p>}
    >
      <Stage>{() => <RenderLayer />}</Stage>
    </DocumentGate>
  );
}

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

password-form.tsx
import { useState } from 'react';
import { useDocuments, type DocumentInfo } from '@embedpdf/react/runtime';
import { PasswordPrompt } from './PasswordPrompt';

export function PasswordForm({ document }: { document: DocumentInfo }) {
  const documents = useDocuments();
  const [wrong, setWrong] = useState(false);

  const submit = async (password: string) => {
    try {
      await documents.unlock(document.id, { password });
    } catch {
      setWrong(true); // it stays locked; ask again
    }
  };

  return <PasswordPrompt onSubmit={submit} wrong={wrong} />;
}

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…

import { useState } from 'react';
import {
  Viewer,
  DocumentGate,
  useDocuments,
  useDocumentsState,
  type DocumentInfo,
  type FailedDocumentInfo,
} from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

import './states.css';

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

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.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') };

function Tabs() {
  const documents = useDocuments();
  const { documents: open, activeId } = useDocumentsState();

  return (
    <div className="tabs" role="tablist">
      {open.map((document) => (
        <button
          key={document.id}
          type="button"
          role="tab"
          className="tab"
          aria-selected={document.id === activeId}
          onClick={() => documents.setActive(document.id)}
        >
          <span className="status" data-status={document.status} />
          {document.name}
        </button>
      ))}
    </div>
  );
}

function PasswordForm({ document }: { document: DocumentInfo }) {
  const documents = useDocuments();
  const [password, setPassword] = useState('');
  const [wrong, setWrong] = useState(document.passwordProvided ?? false);

  const unlock = async () => {
    try {
      await documents.unlock(document.id, { password });
    } catch {
      setWrong(true); // it stays locked; ask again
    }
  };

  return (
    <div className="panel">
      <h3>{document.name} needs a password</h3>
      {wrong && <p>That password isn’t right. Try again.</p>}
      <div className="actions">
        <input
          className="field"
          type="password"
          aria-label="Password"
          value={password}
          onChange={(event) => setPassword(event.target.value)}
        />
        <button type="button" className="button" onClick={unlock}>
          Unlock
        </button>
      </div>
    </div>
  );
}

function OpenError({ document }: { document: FailedDocumentInfo }) {
  const documents = useDocuments();
  return (
    <div className="panel" data-tone="error">
      <h3>Couldn’t open {document.name}</h3>
      <p>{document.error.message}</p>
      <div className="actions">
        <button type="button" className="button" onClick={() => documents.retry(document.id)}>
          Try again
        </button>
        <button type="button" className="button" onClick={() => documents.close(document.id)}>
          Close
        </button>
      </div>
    </div>
  );
}

export default function App() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[
        { source: ebook, name: 'ebook.pdf' },
        { source: broken, name: 'broken.pdf', active: true },
      ]}
    >
      <Tabs />
      <DocumentGate
        fallback={<div className="panel">Opening…</div>}
        locked={(document) => <PasswordForm document={document} />}
        error={(document) => <OpenError document={document} />}
      >
        <Stage className="stage">{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}

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.

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.tsx
import { Viewer } from '@embedpdf/react/runtime';
import { engine, plugins } from './pdf';

export function ReviewViewer() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      identity={{ userId: 'u_381', displayName: 'Dana Smith' }}
      scope={['doc.open', 'doc.render', 'doc.text.select', 'doc.annotate.read', 'annotations:create:self']}
    />
  );
}
  • 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…

import { useState } from 'react';
import { Viewer, DocumentGate, useDocument, useDocuments } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

import './identity.css';

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

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const dana = { userId: 'u_381', displayName: 'Dana Smith' };

// What each role may do, as permissions.
const roles = {
  reader: ['doc.open', 'doc.render', 'doc.text.select'],
  editor: [
    'doc.open',
    'doc.render',
    'doc.text.select',
    'doc.text.copy',
    'doc.download',
    'doc.print',
  ],
};
type Role = keyof typeof roles;

function Toolbar({ role, onRoleChange }: { role: Role; onRoleChange: (role: Role) => void }) {
  const documents = useDocuments();
  // Read the document too, so the checks are read again when it opens or closes.
  const { id } = useDocument();
  // The role the open document was opened with: it keeps that one.
  const [openedAs, setOpenedAs] = useState(role);

  const openAgain = async () => {
    await documents.close(id);
    await documents.open(ebook, { name: 'ebook.pdf' });
    setOpenedAs(role);
  };

  return (
    <>
      <div className="toolbar">
        <label className="label">
          Dana Smith, as
          <select
            className="select"
            value={role}
            onChange={(event) => onRoleChange(event.target.value as Role)}
          >
            <option value="reader">reader</option>
            <option value="editor">editor</option>
          </select>
        </label>
        <span className="check" data-allowed={documents.canDownload()}>
          Download
        </span>
        <span className="check" data-allowed={documents.canPrint()}>
          Print
        </span>
      </div>
      <p className="note">
        This document opened for a {openedAs}.
        {openedAs !== role && (
          <button type="button" className="button" onClick={openAgain}>
            Open it again as {role}
          </button>
        )}
      </p>
    </>
  );
}

export default function App() {
  const [role, setRole] = useState<Role>('reader');
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      identity={dana}
      scope={roles[role]}
      initialDocuments={[{ source: ebook, name: 'ebook.pdf' }]}
    >
      <Toolbar role={role} onRoleChange={setRole} />
      <DocumentGate fallback={<p className="loading">Opening…</p>}>
        <Stage className="stage">{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}
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)Tries a failed document again, with the same source and options. Resolves like open().
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, and, on the local engine, scope and identity. 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. Your component re-renders when a value 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 }

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

Events#

open-failed.tsx
import { useState } from 'react';
import { useDocumentsEvent } from '@embedpdf/react/runtime';

export function OpenError() {
  const [message, setMessage] = useState('');

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

  return <p role="alert">{message}</p>;
}
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.