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.vue
<script setup lang="ts">
import { saveFile, useDocuments } from '@embedpdf/vue/runtime';

const documents = useDocuments();

async function download() {
  saveFile(await documents.download(), 'contract.pdf');
}
</script>

<template>
  <button @click="download">Download</button>
</template>

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…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { cloudEngine } from '@cloudpdf/engine';
import DownloadBar from './DownloadBar.vue';

import '../download.css';

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

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :initial-documents="[{ source: welcome, name: 'welcome.pdf' }]"
  >
    <DocumentGate>
      <template #fallback><p class="loading">Opening…</p></template>
      <DownloadBar />
      <Stage class="stage">
        <template #page><RenderLayer /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>

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.vue
<script setup lang="ts">
import { watchEffect } from 'vue';
import { useDocument } from '@embedpdf/vue/runtime';

const { hasUnsavedChanges } = useDocument();

watchEffect((onCleanup) => {
  if (!hasUnsavedChanges.value) return;
  const warn = (event: BeforeUnloadEvent) => event.preventDefault();
  window.addEventListener('beforeunload', warn);
  onCleanup(() => window.removeEventListener('beforeunload', warn));
});
</script>

<template>
  <span v-if="hasUnsavedChanges">Unsaved changes</span>
</template>

It becomes false again after a download(). The document’s other fields are listed on Opening documents.

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { metadataPlugin } from '@embedpdf/vue/metadata';
import { cloudEngine } from '@cloudpdf/engine';
import ChangeLog from './ChangeLog.vue';
import TitleBar from './TitleBar.vue';

import '../unsaved.css';

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

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :initial-documents="[{ source: welcome, name: 'welcome.pdf' }]"
  >
    <DocumentGate>
      <template #fallback><p class="loading">Opening…</p></template>
      <TitleBar />
      <ChangeLog />
      <Stage class="stage">
        <template #page><RenderLayer /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>

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.vue
<script setup lang="ts">
import { useDocumentsEvent } from '@embedpdf/vue/runtime';

useDocumentsEvent(
  (documents) => documents.onUnsavedChangesChanged,
  ({ hasUnsavedChanges }) => {
    document.title = hasUnsavedChanges ? '● Contract' : 'Contract';
  },
);
</script>
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.

Printing doesn’t need doc.render: the pages print from print pictures, which need only the print permissions. So a user who may not see the pages rendered can still be allowed to print.

Good to know#

Signed documents#

An incremental download keeps existing signatures valid, as long as the changes are ones the signature allows. See Signatures.