Saving & downloading
Everything people do in the viewer, such as annotations, form values and page edits, is part of the
document. download() gives you the PDF with all of it, as bytes, and saveFile() hands them to
the browser as a download:
import { saveFile, useDocuments } from '@embedpdf/react/runtime';
export function DownloadButton() {
const documents = useDocuments();
return (
<button onClick={async () => saveFile(await documents.download(), 'contract.pdf')}>
Download
</button>
);
}download() waits for changes still on their way, such as the last words someone typed in a text
box, so the file has everything the user sees. Without an id it downloads the document in scope,
usually the active one; documents.download(id) picks another.
Send it to your server#
The bytes are a normal PDF. Upload them like any file:
const bytes = await documents.download();
await fetch(`/api/files/${fileId}`, { method: 'PUT', body: bytes });A quick save or a full rewrite#
await documents.download(); // appends the changes to the original file
await documents.download(undefined, { mode: 'rewrite' }); // writes a fresh, compact fileThe default, 'incremental', keeps the original bytes and adds the changes after them: it’s fast,
and it keeps existing digital signatures valid. 'rewrite' writes a new file without old
revisions; use it after redaction, so removed content can’t be recovered
from an earlier revision. See Saving.
Loading live preview…
import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, saveFile, useDocuments } from '@embedpdf/react/runtime';
import type { OpenInput, PdfSaveMode } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { cloudEngine } from '@cloudpdf/engine';
import './download.css';
const engine = cloudEngine();
const plugins = [stagePlugin(), renderPlugin()];
const ebook: OpenInput = { kind: 'share', shareToken: 'shr_WGj1goAtlNN_fQ5OswPrbJQM' };
const megabytes = (bytes: Uint8Array) => `${(bytes.byteLength / 1_000_000).toFixed(2)} MB`;
function DownloadBar() {
const documents = useDocuments();
const [mode, setMode] = useState<PdfSaveMode>('incremental');
const [sizes, setSizes] = useState<Record<PdfSaveMode, string> | null>(null);
// How big each kind of download is: the same document, written two ways.
useEffect(() => {
const cancel = new AbortController();
const { signal } = cancel;
Promise.all([
documents.download(undefined, { signal }),
documents.download(undefined, { mode: 'rewrite', signal }),
])
.then(([incremental, rewrite]) =>
setSizes({ incremental: megabytes(incremental), rewrite: megabytes(rewrite) }),
)
.catch(() => {}); // cancelled: the example went away
return () => cancel.abort();
}, [documents]);
const download = async () => saveFile(await documents.download(undefined, { mode }), 'ebook.pdf');
return (
<div className="toolbar">
<div className="segmented" role="group" aria-label="Kind of download">
<button
type="button"
aria-pressed={mode === 'incremental'}
onClick={() => setMode('incremental')}
>
Incremental
</button>
<button type="button" aria-pressed={mode === 'rewrite'} onClick={() => setMode('rewrite')}>
Rewrite
</button>
</div>
<button
type="button"
className="download"
disabled={!documents.canDownload()}
onClick={download}
>
Download
</button>
<output className="sizes">
{sizes ? (
<>
Incremental <strong>{sizes.incremental}</strong> · Rewrite{' '}
<strong>{sizes.rewrite}</strong>
</>
) : (
'Measuring…'
)}
</output>
</div>
);
}
export default function App() {
return (
<Viewer
engine={engine}
plugins={plugins}
initialDocuments={[{ source: ebook, name: 'ebook.pdf' }]}
>
<DocumentGate fallback={<p className="loading">Opening…</p>}>
<DownloadBar />
<Stage className="stage">{() => <RenderLayer />}</Stage>
</DocumentGate>
</Viewer>
);
}Print#
print() prints the document, not your app: its pages, each on a sheet of its own size, through the
browser’s print dialog. Like download(), it waits for changes still on their way first.
await documents.print();
await documents.print(undefined, { pages: [page.ref], includeAnnotations: false });pages takes page refs and prints them in that order; without it, every page prints. A page prints
as the PDF says it prints, which isn’t always how it looks on screen: an annotation or a form field
whose print setting is off stays off the paper, and a layer meant for paper only shows up on it.
includeAnnotations: false leaves the annotations out; form fields always print.
When the actions plugin is registered, the PDF’s own print actions run around the browser’s dialog, as they would in Acrobat.
How sharp it prints#
On a desktop browser with its own PDF viewer on, a document that allows high-quality printing and keeping a copy prints as a PDF: a copy of the pages as they print, which the browser’s viewer prints with sharp text and lines. Every print dialog can save what it prints as a file, so the copy takes a right to keep it. Everywhere else the browser prints pictures of the pages:
| Permission | Prints |
|---|---|
doc.print.high and doc.download or doc.download.flattened | The PDF copy; pictures at 300 DPI on phones, tablets and Chrome set to download PDFs |
doc.print.high | Pictures at 300 DPI |
doc.print only | Pictures at 150 DPI, as a PDF asks when it allows only low-resolution printing |
getPrintQuality() says which, for a note in your dialog: { kind: 'pdf' },
{ kind: 'images', dpi: 300 }, or null when the document can’t be printed here. When the
browser turns the PDF down after all, the same pages print as pictures at 300 DPI, and the
prepared print’s quality says so.
Your own print dialog#
Drawing the pages takes a moment, and costs work for every page, on the server too with a cloud document. So a dialog of your own lets the user choose first, and prepares when they press Print, with its progress:
printButton.onclick = async () => {
const prepared = await documents.preparePrint(undefined, {
pages,
signal,
onProgress: ({ pagesRendered, pageCount }) => setProgress(pagesRendered / pageCount),
});
if (prepared.needsTap) showReady(() => prepared.print());
else await prepared.print();
};Safari prints only from a tap or click (later, iOS refuses the print dialog and the Mac prints
blank pages), and the one on Print went to preparing. There prepared.needsTap is true: show that the printout is ready, and print from the next
tap. Other browsers print as soon as it is ready. documents.print() prepares and prints in one
call, so in Safari, prepare first.
prepared.print() prepares again first when the document changed since, and resolves when the
browser’s dialog closes. Cancel a printout on its way with its signal, or dispose() the one
that was ready; closing the document frees it too. The viewer’s Print dialog works this way.
Warn before changes are lost#
useDocument() tells you whether the document has changes that haven’t been downloaded yet:
import { useEffect } from 'react';
import { useDocument } from '@embedpdf/react/runtime';
export function UnsavedWarning() {
const { hasUnsavedChanges } = useDocument();
useEffect(() => {
if (!hasUnsavedChanges) return;
const warn = (event: BeforeUnloadEvent) => event.preventDefault();
window.addEventListener('beforeunload', warn);
return () => window.removeEventListener('beforeunload', warn);
}, [hasUnsavedChanges]);
return hasUnsavedChanges ? <span>Unsaved changes</span> : null;
}It becomes false again after a download(). The document’s other fields are listed on
Opening documents.
Loading live preview…
import { useEffect, useState } from 'react';
import {
Viewer,
DocumentGate,
saveFile,
useDocument,
useDocuments,
useDocumentsEvent,
} 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 { metadataPlugin, useMetadata, useMetadataState } from '@embedpdf/react/metadata';
import { cloudEngine } from '@cloudpdf/engine';
import './unsaved.css';
const engine = cloudEngine();
const plugins = [stagePlugin(), renderPlugin(), metadataPlugin()];
const ebook: OpenInput = { kind: 'share', shareToken: 'shr_WGj1goAtlNN_fQ5OswPrbJQM' };
function TitleBar() {
const documents = useDocuments();
const metadata = useMetadata();
const title = useMetadataState((state) => state.metadata?.title ?? '');
const { hasUnsavedChanges } = useDocument();
// A change on load: the document gets a new title, so it has something to lose.
useEffect(() => {
void metadata.update({ title: 'Quarterly report (draft)' });
}, [metadata]);
// While there's something to lose, the browser asks before the page closes.
useEffect(() => {
if (!hasUnsavedChanges) return;
const warn = (event: BeforeUnloadEvent) => event.preventDefault();
window.addEventListener('beforeunload', warn);
return () => window.removeEventListener('beforeunload', warn);
}, [hasUnsavedChanges]);
return (
<div className="toolbar">
<input
className="field"
aria-label="Title"
defaultValue={title}
key={title}
onBlur={(event) => metadata.update({ title: event.target.value })}
/>
<span className="badge" data-unsaved={hasUnsavedChanges}>
{hasUnsavedChanges ? 'Unsaved changes' : 'Downloaded'}
</span>
<button
type="button"
className="button"
onClick={async () => saveFile(await documents.download(), 'report.pdf')}
>
Download
</button>
</div>
);
}
function ChangeLog() {
const [entries, setEntries] = useState<string[]>([]);
useDocumentsEvent(
(documents) => documents.onUnsavedChangesChanged,
({ hasUnsavedChanges }) =>
setEntries((previous) => [
hasUnsavedChanges
? 'It has changes that weren’t downloaded'
: 'Downloaded: nothing to lose',
...previous,
]),
);
return (
<ul className="log" aria-live="polite">
{entries.slice(0, 3).map((entry, index) => (
<li key={entries.length - index}>{entry}</li>
))}
</ul>
);
}
export default function App() {
return (
<Viewer
engine={engine}
plugins={plugins}
initialDocuments={[{ source: ebook, name: 'report.pdf' }]}
>
<DocumentGate fallback={<p className="loading">Opening…</p>}>
<TitleBar />
<ChangeLog />
<Stage className="stage">{() => <RenderLayer />}</Stage>
</DocumentGate>
</Viewer>
);
}On CloudPDF, changes are stored on your server as people make them, so hasUnsavedChanges stays
false: there’s nothing to lose. Download when you want the file itself.
Methods#
What useDocuments() gives you for downloading and printing. Opening and closing are on
Opening documents, tabs and the active document on
Several documents.
| Method | What it does |
|---|---|
download(id?, { mode }) | The PDF with every change, as bytes. Waits for changes still on their way first. mode is 'incremental' (the default) or 'rewrite'. A quick save or a full rewrite |
downloadLayer(id?) | Only the changes, as a layer you store and open again on top of the original. Local engine only; rejects when the document wasn’t opened with a layer. Keep only the changes |
print(id?, { pages, includeAnnotations, onProgress }) | Prints the document’s pages through the browser’s print dialog. Print |
preparePrint(id?, { pages, includeAnnotations, onProgress }) | Draws the pages for printing, ready for the browser’s dialog; resolves { quality, pageCount, needsTap, print(), dispose() }. Your own print dialog |
getPrintQuality(id?) | How it prints here: { kind: 'images', dpi }, or null when it can’t. How sharp it prints |
canDownload(id?), canPrint(id?) | Whether downloading and printing are allowed. Permissions |
Without an id, each uses the document in scope: the one a
<DocumentScope> around your component names, or else the
active one. Every method that returns a promise also takes a signal in its options, to cancel it.
Events#
import { useDocumentsEvent } from '@embedpdf/react/runtime';
export function TitleDot() {
useDocumentsEvent(
(documents) => documents.onUnsavedChangesChanged,
({ hasUnsavedChanges }) => {
document.title = hasUnsavedChanges ? '● Contract' : 'Contract';
},
);
return null;
}| Event | Fires when | Carries |
|---|---|---|
onUnsavedChangesChanged | The document gets its first unsaved change, or is downloaded | documentId, hasUnsavedChanges |
Permissions#
| Permission | What it allows | Check |
|---|---|---|
doc.download | download() and downloadLayer() | documents.canDownload() |
doc.print | print() and preparePrint() | documents.canPrint() |
doc.print.high | Printing at 300 DPI instead of 150 | documents.getPrintQuality() |
Hide the buttons when the check says no. The engine refuses the call anyway.
Good to know#
Signed documents#
An incremental download keeps existing signatures valid, as long as the changes are ones the signature allows. See Signatures.