---
title: "Saving & downloading — Angular"
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: "Angular"
source: "https://www.cloudpdf.com/docs/headless/angular/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.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { EpdfDocuments, saveFile } from '@embedpdf/angular/runtime';

@Component({
  selector: 'app-download-button',
  template: `<button (click)="download()">Download</button>`,
})
export class DownloadButton {
  private readonly documents = inject(EpdfDocuments);

  protected async download() {
    saveFile(await this.documents.download(), 'contract.pdf');
  }
}
```

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

**`download.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  DestroyRef,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import {
  EpdfDocumentGate,
  EpdfDocuments,
  provideEmbedPdf,
  saveFile,
} from '@embedpdf/angular/runtime';
import type { OpenInput, PdfSaveMode } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { cloudEngine } from '@cloudpdf/engine';

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };

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

@Component({
  selector: 'demo-download-bar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <div class="segmented" role="group" aria-label="Kind of download">
        <button
          type="button"
          [attr.aria-pressed]="mode() === 'incremental'"
          (click)="mode.set('incremental')"
        >
          Incremental
        </button>
        <button
          type="button"
          [attr.aria-pressed]="mode() === 'rewrite'"
          (click)="mode.set('rewrite')"
        >
          Rewrite
        </button>
      </div>
      <button
        type="button"
        class="download"
        [disabled]="!documents.canDownload()"
        (click)="download()"
      >
        Download
      </button>
      <output class="sizes">
        @if (sizes(); as sizes) {
          Incremental <strong>{{ sizes.incremental }}</strong> · Rewrite
          <strong>{{ sizes.rewrite }}</strong>
        } @else {
          Measuring…
        }
      </output>
    </div>
  `,
})
export class DownloadBar {
  protected readonly documents = inject(EpdfDocuments);
  protected readonly mode = signal<PdfSaveMode>('incremental');
  protected readonly sizes = signal<Record<PdfSaveMode, string> | null>(null);

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

  protected async download() {
    saveFile(await this.documents.download(undefined, { mode: this.mode() }), 'welcome.pdf');
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, DownloadBar],
  providers: [
    provideEmbedPdf(
      { engine: () => cloudEngine(), initialDocuments: [{ source: welcome, name: 'welcome.pdf' }] },
      withStage(),
      withRender(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './download.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: opening">
      <demo-download-bar />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #opening><p class="loading">Opening…</p></ng-template>
  `,
})
export class App {}
```

**`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);
}
```

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

`inject(EpdfDocument)` tells you whether the document has changes that haven't been downloaded yet:

**`unsaved-warning.ts`**

```typescript
import { Component, effect, inject } from '@angular/core';
import { EpdfDocument } from '@embedpdf/angular/runtime';

@Component({
  selector: 'app-unsaved-warning',
  template: `
    @if (document.hasUnsavedChanges()) {
      <span>Unsaved changes</span>
    }
  `,
})
export class UnsavedWarning {
  protected readonly document = inject(EpdfDocument);

  constructor() {
    effect((onCleanup) => {
      if (!this.document.hasUnsavedChanges()) return;
      const warn = (event: BeforeUnloadEvent) => event.preventDefault();
      window.addEventListener('beforeunload', warn);
      onCleanup(() => window.removeEventListener('beforeunload', warn));
    });
  }
}
```

It becomes `false` again after a `download()`. The document's other fields are listed on
[Opening documents](opening#state).

**`unsaved.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
  EpdfDocument,
  EpdfDocumentGate,
  EpdfDocuments,
  provideEmbedPdf,
  saveFile,
} from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfMetadata, withMetadata } from '@embedpdf/angular/metadata';
import { cloudEngine } from '@cloudpdf/engine';

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };

@Component({
  selector: 'demo-title-bar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <input
        #field
        class="field"
        aria-label="Title"
        [value]="title()"
        (blur)="metadata.update({ title: field.value })"
      />
      <span class="badge" [attr.data-unsaved]="document.hasUnsavedChanges()">
        {{ document.hasUnsavedChanges() ? 'Unsaved changes' : 'Downloaded' }}
      </span>
      <button type="button" class="button" (click)="download()">Download</button>
    </div>
  `,
})
export class TitleBar {
  private readonly documents = inject(EpdfDocuments);
  protected readonly document = inject(EpdfDocument);
  protected readonly metadata = inject(EpdfMetadata);
  protected readonly title = computed(() => this.metadata.fields()?.title ?? '');

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

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

  protected async download() {
    saveFile(await this.documents.download(), 'welcome.pdf');
  }
}

@Component({
  selector: 'demo-change-log',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <ul class="log" aria-live="polite">
      @for (entry of entries().slice(0, 3); track entries().length - $index) {
        <li>{{ entry }}</li>
      }
    </ul>
  `,
})
export class ChangeLog {
  protected readonly entries = signal<string[]>([]);

  constructor() {
    inject(EpdfDocuments)
      .unsavedChangesChanged$.pipe(takeUntilDestroyed())
      .subscribe(({ hasUnsavedChanges }) =>
        this.entries.update((previous) => [
          hasUnsavedChanges
            ? 'It has changes that weren’t downloaded'
            : 'Downloaded: nothing to lose',
          ...previous,
        ]),
      );
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, TitleBar, ChangeLog],
  providers: [
    provideEmbedPdf(
      { engine: () => cloudEngine(), initialDocuments: [{ source: welcome, name: 'welcome.pdf' }] },
      withStage(),
      withRender(),
      withMetadata(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './unsaved.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: opening">
      <demo-title-bar />
      <demo-change-log />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #opening><p class="loading">Opening…</p></ng-template>
  `,
})
export class App {}
```

**`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);
}
```

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 `inject(EpdfDocuments)` 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
[`[epdfDocumentScope]`](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.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Title } from '@angular/platform-browser';
import { EpdfDocuments } from '@embedpdf/angular/runtime';

@Component({ selector: 'app-title-dot', template: '' })
export class TitleDot {
  private readonly title = inject(Title);

  constructor() {
    inject(EpdfDocuments)
      .unsavedChangesChanged$.pipe(takeUntilDestroyed())
      .subscribe(({ hasUnsavedChanges }) => this.title.setTitle(hasUnsavedChanges ? '● Contract' : 'Contract'));
  }
}
```

| Event                    | Fires when                                                   | Carries                           |
| :----------------------- | :----------------------------------------------------------- | :-------------------------------- |
| `unsavedChangesChanged$` | 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).
