Opening documents
Give the viewer the documents to open when it starts, and <DocumentGate> shows your content once
the document is ready:
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:
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 welcome = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/welcome.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 welcome, 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(welcome, { name: 'welcome.pdf' })}
>
Open the welcome 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: welcome, name: 'welcome.pdf' }]}
>
<Toolbar />
<DocumentGate fallback={<Empty />}>
<Stage className="stage">{() => <RenderLayer />}</Stage>
</DocumentGate>
</Viewer>
);
}Where a document comes from#
| Source | What 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) => source | Anything 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.
import { DocumentGate, Viewer } from '@embedpdf/react/runtime';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage } from '@embedpdf/react/stage';
import { getToken } from './auth';
import { engine, plugins } from './pdf';
export function ContractViewer() {
return (
<Viewer
engine={engine}
plugins={plugins}
initialDocuments={[{ source: { kind: 'url', url: '/api/files/contract.pdf' } }]}
fetch={(url, init) =>
fetch(url, { ...init, headers: { Authorization: `Bearer ${getToken()}` } })
}
>
<DocumentGate>
<Stage>{() => <RenderLayer />}</Stage>
</DocumentGate>
</Viewer>
);
}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:
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:
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 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') };
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: welcome, name: 'welcome.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.
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:
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']}
/>
);
}identityis who the user is. TheirdisplayNamebecomes the author of the annotations they make.scopeis 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 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' };
// 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(welcome, { name: 'welcome.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: welcome, name: 'welcome.pdf' }]}
>
<Toolbar role={role} onRoleChange={setRole} />
<DocumentGate fallback={<p className="loading">Opening…</p>}>
<Stage className="stage">{() => <RenderLayer />}</Stage>
</DocumentGate>
</Viewer>
);
}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 onePermissions 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.
| Method | What 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. 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.
| Field | What it is |
|---|---|
id | The document’s id, for close(id), unlock(id, …) and <DocumentScope> |
name | The name its tab shows, from open() or rename() |
status | 'loading', 'locked', 'ready' or 'error' |
pageCount | How many pages it has; 0 until it’s ready |
hasUnsavedChanges | Whether it has changes that weren’t downloaded yet. Warn before changes are lost |
passwordProvided | When it’s locked: whether a password was tried and was wrong |
error | Why it couldn’t be opened, when status is 'error': { code, message } |
grantRequest | When it’s from a grant: the passphrase or the questions it waits for, to open or to go on |
identityRequest | When 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#
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>;
}| Event | Fires when | Carries |
|---|---|---|
onOpened | A document is open and ready | documentId, info |
onOpenFailed | A document couldn’t be opened | documentId, error |
onLocked | A document needs a password | documentId, passwordProvided |
onClosed | A document was closed | documentId |
onPagesChanged | Pages were added, removed, moved or rotated | documentId, 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.
Your feedback goes directly to the documentation team.