---
title: "Saving & downloading — Vue"
description: "Get the PDF with every change, let the user download or print it, keep only the changes, and warn before unsaved changes are lost."
framework: "Vue"
source: "https://www.cloudpdf.com/docs/headless/vue/documents/saving"
---

# 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`**

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

```ts
const bytes = await documents.download();
await fetch(`/api/files/${fileId}`, { method: 'PUT', body: bytes });
```

## A quick save or a full rewrite

```ts
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](../annotations/redaction), so removed content can't be recovered
from an earlier revision. See [Saving](https://www.cloudpdf.com/docs/engine/documents/saving).

**`App.vue`**

```vue
<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>
```

**`download.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --accent: #0876fd;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The download, and how big each kind of file is */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.segmented {
  display: inline-flex;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
}

.segmented button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: 7px;
  padding: 6px 10px;
  cursor: pointer;
}

.segmented button[aria-pressed='true'] {
  color: var(--ink);
  background: var(--tint);
}

.download {
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: #ffffff;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 9px;
  padding: 8px 14px;
  cursor: pointer;
}

.download:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.segmented button:focus-visible,
.download:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.sizes {
  margin-left: auto;
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.sizes strong {
  color: var(--ink);
}

/* The pages */
.stage {
  height: 400px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`DownloadBar.vue`**

```vue
<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue';
import { saveFile, useDocuments } from '@embedpdf/vue/runtime';
import type { PdfSaveMode } from '@embedpdf/vue/runtime';

const documents = useDocuments();
const mode = ref<PdfSaveMode>('incremental');
const sizes = ref<Record<PdfSaveMode, string> | null>(null);

const megabytes = (bytes: Uint8Array) => `${(bytes.byteLength / 1_000_000).toFixed(2)} MB`;

// How big each kind of download is: the same document, written two ways.
const cancel = new AbortController();
onBeforeUnmount(() => cancel.abort());
const { signal } = cancel;
Promise.all([
  documents.download(undefined, { signal }),
  documents.download(undefined, { mode: 'rewrite', signal }),
])
  .then(([incremental, rewrite]) => {
    sizes.value = { incremental: megabytes(incremental), rewrite: megabytes(rewrite) };
  })
  .catch(() => {}); // cancelled: the example went away

async function download() {
  saveFile(await documents.download(undefined, { mode: mode.value }), 'welcome.pdf');
}
</script>

<template>
  <div class="toolbar">
    <div class="segmented" role="group" aria-label="Kind of download">
      <button
        type="button"
        :aria-pressed="mode === 'incremental'"
        @click="mode = 'incremental'"
      >
        Incremental
      </button>
      <button type="button" :aria-pressed="mode === 'rewrite'" @click="mode = 'rewrite'">
        Rewrite
      </button>
    </div>
    <button
      type="button"
      class="download"
      :disabled="!documents.canDownload()"
      @click="download"
    >
      Download
    </button>
    <output class="sizes">
      <template v-if="sizes">
        Incremental <strong>{{ sizes.incremental }}</strong> · Rewrite
        <strong>{{ sizes.rewrite }}</strong>
      </template>
      <template v-else>Measuring…</template>
    </output>
  </div>
</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.

```ts
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](actions) 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:

```ts
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`**

```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](opening#state).

**`App.vue`**

```vue
<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>
```

**`unsaved.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --unsaved: #b54708;
  --unsaved-tint: #fffaeb;
  --saved: #067647;
  --saved-tint: #ecfdf3;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The title you change, whether it's downloaded, and the download */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.field {
  appearance: none;
  flex: 1 1 180px;
  min-width: 0;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
}

.badge {
  font-size: 12px;
  font-weight: 600;
  border-radius: 999px;
  padding: 4px 10px;
  color: var(--saved);
  background: var(--saved-tint);
}

.badge[data-unsaved='true'] {
  color: var(--unsaved);
  background: var(--unsaved-tint);
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.field:focus-visible,
.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* What `onUnsavedChangesChanged` said, newest first */
.log {
  margin: 0 0 14px;
  padding: 0;
  list-style: none;
  font-size: 12px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 380px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`ChangeLog.vue`**

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { useDocumentsEvent } from '@embedpdf/vue/runtime';

const entries = ref<string[]>([]);

useDocumentsEvent(
  (documents) => documents.onUnsavedChangesChanged,
  ({ hasUnsavedChanges }) => {
    entries.value = [
      hasUnsavedChanges ? 'It has changes that weren’t downloaded' : 'Downloaded: nothing to lose',
      ...entries.value,
    ];
  },
);
</script>

<template>
  <ul class="log" aria-live="polite">
    <li v-for="(entry, index) in entries.slice(0, 3)" :key="entries.length - index">
      {{ entry }}
    </li>
  </ul>
</template>
```

**`TitleBar.vue`**

```vue
<script setup lang="ts">
import { onMounted, ref, watch, watchEffect } from 'vue';
import { saveFile, useDocument, useDocuments } from '@embedpdf/vue/runtime';
import { useMetadata, useMetadataState } from '@embedpdf/vue/metadata';

const documents = useDocuments();
const metadata = useMetadata();
const title = useMetadataState((state) => state.metadata?.title ?? '');
const { hasUnsavedChanges } = useDocument();

// What the field shows: the title, until the user types.
const draft = ref(title.value);
watch(title, (value) => (draft.value = value));

// A change on load: the document gets a new title, so it has something to lose.
onMounted(() => {
  void metadata.update({ title: 'Documents belong on the web (draft)' });
});

// While there's something to lose, the browser asks before the page closes.
watchEffect((onCleanup) => {
  if (!hasUnsavedChanges.value) return;
  const warn = (event: BeforeUnloadEvent) => event.preventDefault();
  window.addEventListener('beforeunload', warn);
  onCleanup(() => window.removeEventListener('beforeunload', warn));
});

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

<template>
  <div class="toolbar">
    <input
      v-model="draft"
      class="field"
      aria-label="Title"
      @blur="metadata.update({ title: draft })"
    />
    <span class="badge" :data-unsaved="hasUnsavedChanges">
      {{ hasUnsavedChanges ? 'Unsaved changes' : 'Downloaded' }}
    </span>
    <button type="button" class="button" @click="download">Download</button>
  </div>
</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](opening#methods), tabs and the active document on
[Several documents](tabs#methods).

| 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](#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](#keep-only-the-changes)     |
| `print(id?, { pages, includeAnnotations, onProgress })`        | Prints the document's pages through the browser's print dialog. [Print](#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](#your-own-print-dialog)                             |
| `getPrintQuality(id?)`                                         | How it prints here: `{ kind: 'images', dpi }`, or `null` when it can't. [How sharp it prints](#how-sharp-it-prints)                                                                                        |
| `canDownload(id?)`, `canPrint(id?)`                            | Whether downloading and printing are allowed. [Permissions](#permissions)                                                                                                                                  |

Without an `id`, each uses the document in scope: the one a
[`<DocumentScope>`](tabs#talk-to-a-specific-document) 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`**

```vue
<script setup lang="ts">
import { useDocumentsEvent } from '@embedpdf/vue/runtime';

useDocumentsEvent(
  (documents) => documents.onUnsavedChangesChanged,
  ({ hasUnsavedChanges }) => {
    document.title = hasUnsavedChanges ? '● Contract' : 'Contract';
  },
);
</script>
```

| 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.

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](../forms/signatures).
