CloudPDF
DocsPricing
Start building

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:

download-button.tsx
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 file

The 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:

PermissionPrints
doc.print.high and doc.download or doc.download.flattenedThe PDF copy; pictures at 300 DPI on phones, tablets and Chrome set to download PDFs
doc.print.highPictures at 300 DPI
doc.print onlyPictures 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:

unsaved-warning.tsx
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.

MethodWhat 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#

title-dot.tsx
import { useDocumentsEvent } from '@embedpdf/react/runtime';

export function TitleDot() {
  useDocumentsEvent(
    (documents) => documents.onUnsavedChangesChanged,
    ({ hasUnsavedChanges }) => {
      document.title = hasUnsavedChanges ? '● Contract' : 'Contract';
    },
  );

  return null;
}
EventFires whenCarries
onUnsavedChangesChangedThe document gets its first unsaved change, or is downloadeddocumentId, hasUnsavedChanges

Permissions#

PermissionWhat it allowsCheck
doc.downloaddownload() and downloadLayer()documents.canDownload()
doc.printprint() and preparePrint()documents.canPrint()
doc.print.highPrinting at 300 DPI instead of 150documents.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.