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 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#
| 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. |
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:
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 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:
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 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>
);
}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) | 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.
| 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 } |
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.