---
title: "Attachments — Angular"
description: "List the files a PDF carries, save them, let people attach and remove files, and go to a file pinned to a page."
framework: "Angular"
source: "https://www.cloudpdf.com/docs/headless/angular/documents/attachments"
---

# Attachments

A PDF can carry other files: a spreadsheet with the numbers behind a report, the original of a
scanned contract. They sit in two places: the document's own attachments, and file attachment
annotations, a file pinned to a spot on a page. Register `withAttachment()` to list both as one
list:

**`attachment-list.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { displayNameOf, EpdfAttachment } from '@embedpdf/angular/attachment';
import type { AttachedFile } from '@embedpdf/angular/attachment';
import { saveFile } from '@embedpdf/angular/runtime';

@Component({
  selector: 'app-attachment-list',
  template: `
    <ul>
      @for (file of attachment.files(); track file.id) {
        <li>
          <bdi>{{ nameOf(file) }}</bdi>
          <button [disabled]="!attachment.canDownload()" (click)="save(file)">Save</button>
        </li>
      }
    </ul>
  `,
})
export class AttachmentList {
  protected readonly attachment = inject(EpdfAttachment);
  protected readonly nameOf = displayNameOf;

  protected async save(file: AttachedFile) {
    const { bytes, name, mimeType } = await this.attachment.download(file);
    saveFile(bytes, name, mimeType ?? undefined);
  }
}
```

What `inject(EpdfAttachment)` gives you updates by itself: after your own changes, and on a shared
document, after someone else's. Nothing is read when a document opens: the attachments load the
first time something asks for them. The files on the pages come from the
[annotation plugin](https://www.cloudpdf.com/docs/headless/angular/annotations), when you've registered it.

## Show the files

`files` lists the document's attachments first, in the order the document keeps them (by name),
then the files on its pages, in page order. Each file has its `name`, `mimeType`, `description`,
`size`, `checksum`, `createdAt` and `modifiedAt` (`null` when the PDF doesn't say), an `id` for
your list's keys, and its `home`:

- `{ kind: 'document', ref }`: one of the document's attachments, `ref` its name in the document.
- `{ kind: 'page', ref }`: a file attachment annotation; `ref.page` is its page.

Show a name with `displayNameOf(file)`, and put it in a `<bdi>`. A name can hide characters that
make it look like another (`invoice.pdf` that is really `invoice.exe`); `displayNameOf` takes them
out, and the `<bdi>` keeps a name in another script from turning your row around.

`status` says whether the attachments haven't been asked for yet (`'idle'`), are loading, are
there (`'ready'`), or couldn't be read.

## Save a file

`download(file)` returns the file's bytes, its name ready to save under (`displayNameOf`'s) and
its type. Hand them to `saveFile()` to save them as a browser download:

```ts
const { bytes, name, mimeType } = await attachment.download(file);
saveFile(bytes, name, mimeType ?? undefined);
```

Save a file; never open it in a new tab or a frame of your page. A file opened there runs with your
page's origin, so an HTML attachment could act as your page.

Saving takes a file out of the document, so it needs `doc.download`: check `canDownload`. A file
too large to unpack in the browser is refused (see [Large files](#large-files)).

## Attach files

`add(files)` attaches files as one change: one step to undo. A `File` the user picked brings its
name and type; bytes need a `name`:

```ts
await attachment.add([{ data: picked }, { data: bytes, name: 'figures.csv' }]);
```

Names are unique in a document: a name it has already gets a number, `notes (2).txt`, so nothing
is replaced. The files show at once, `isPending(file)` while the engine saves them, and `add()`
resolves with them. Check `canAdd` first.

## Remove a file

`remove(file)` takes a file away, as one step to undo: an undo brings back the same file, with its
bytes and dates. A file on a page goes with its annotation, through the annotation plugin.
`canRemove(file)` says whether this one may go.

**`files.ts`**

```typescript
import { ChangeDetectionStrategy, Component, inject, ViewEncapsulation } from '@angular/core';
import { EpdfDocumentGate, 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 { displayNameOf, EpdfAttachment, withAttachment } from '@embedpdf/angular/attachment';
import type { AttachedFile } from '@embedpdf/angular/attachment';
import { cloudEngine } from '@cloudpdf/engine';

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

// The files the document carries: attach some, save one, remove one.
@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
  providers: [
    provideEmbedPdf(
      { engine: () => cloudEngine(), initialDocuments: [{ source: welcome }] },
      withStage(),
      withRender(),
      withAttachment(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './files.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="layout">
        <aside class="files">
          <header class="files-header">
            <h3 class="files-title">Files</h3>
            <label class="button primary" [attr.data-disabled]="!attachment.canAdd() || null">
              Attach
              <input
                type="file"
                multiple
                hidden
                [disabled]="!attachment.canAdd()"
                (change)="attach($event)"
              />
            </label>
          </header>
          @if (attachment.files().length === 0) {
            <p class="empty">Attach a file to carry it inside the document.</p>
          } @else {
            <ul class="list">
              @for (file of attachment.files(); track file.id) {
                <li class="row" [attr.data-pending]="attachment.isPending(file) || null">
                  <button
                    type="button"
                    class="row-name"
                    [disabled]="!attachment.canDownload()"
                    (click)="save(file)"
                  >
                    <bdi>{{ nameOf(file) }}</bdi>
                    <span class="row-size">{{ sizeOf(file.size) }}</span>
                  </button>
                  <button
                    type="button"
                    class="row-remove"
                    [attr.aria-label]="'Remove ' + nameOf(file)"
                    [disabled]="!attachment.canRemove(file)"
                    (click)="attachment.remove(file)"
                  >
                    ×
                  </button>
                </li>
              }
            </ul>
          }
        </aside>

        <epdf-stage class="stage">
          <ng-template epdfPage>
            <epdf-render-layer />
          </ng-template>
        </epdf-stage>
      </div>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  protected readonly attachment = inject(EpdfAttachment);
  protected readonly nameOf = displayNameOf;

  /** A file's size, as people read it. */
  protected sizeOf(bytes: number | null): string {
    return bytes === null ? '' : bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
  }

  protected async save(file: AttachedFile) {
    const { bytes, name, mimeType } = await this.attachment.download(file);
    saveFile(bytes, name, mimeType ?? undefined);
  }

  protected attach(event: Event) {
    const input = event.target as HTMLInputElement;
    const picked = Array.from(input.files ?? []);
    input.value = '';
    if (picked.length > 0) void this.attachment.add(picked.map((data) => ({ data })));
  }
}
```

**`files.css`**

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

/* The files beside the pages */
.layout {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 14px;
}

.files {
  display: flex;
  flex-direction: column;
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 1px 2px var(--shadow);
  overflow: hidden;
}

.files-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 10px 10px 14px;
  border-bottom: 1px solid var(--line);
}

.files-title {
  margin: 0;
  font-size: 13px;
  font-weight: 650;
  color: var(--ink);
}

.empty {
  margin: 0;
  padding: 18px 14px;
  font-size: 12.5px;
  color: var(--muted);
}

/* One row per file: its name and size save it; a file still being added is faded */
.list {
  margin: 0;
  padding: 6px;
  list-style: none;
  overflow-y: auto;
}

.row {
  display: flex;
  align-items: center;
  border-radius: 8px;
}

.row:hover {
  background: var(--tint);
}

.row[data-pending] {
  opacity: 0.55;
}

.row-name {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
  font: inherit;
  font-size: 13px;
  text-align: start;
  color: var(--ink);
  background: none;
  border: 0;
  padding: 6px 10px;
  cursor: pointer;
}

.row-name bdi {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row-size {
  font-size: 11.5px;
  color: var(--muted);
}

.row-remove {
  font: inherit;
  font-size: 16px;
  line-height: 1;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: 6px;
  padding: 4px 8px;
  cursor: pointer;
  opacity: 0;
}

.row:hover .row-remove,
.row-remove:focus-visible {
  opacity: 1;
}

.row-remove:hover {
  color: var(--ink);
  background: var(--line);
}

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

.button.primary {
  color: #ffffff;
  background: var(--accent);
  border-color: var(--accent);
}

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

.button[data-disabled],
.row-name:disabled,
.row-remove:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* The pages */
.stage {
  height: 420px;
  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);
}
```

## Go to a page's file

`reveal(file)` selects a page's file attachment annotation and brings it into view, through the
[stage](https://www.cloudpdf.com/docs/headless/angular/viewing/stage) when you've registered it. It does nothing for one of the
document's attachments, which isn't on a page.

## Select files

`select(files)` makes files the selection, and `selected` lists them. The selection is the
reader's: show it, and act on it with your own buttons and menus.

```ts
attachment.select([file]);
await attachment.remove(selected[0]);
```

## Methods

Everything `inject(EpdfAttachment)` gives you.

| Method            | What it does                                                                          |
| :---------------- | :------------------------------------------------------------------------------------ |
| `get(id)`         | One file by its `id`, or `null`.                                                      |
| `isPending(file)` | Whether a change to it waits for the engine. [Attach files](#attach-files)            |
| `canRemove(file)` | Whether this file may go. [Remove a file](#remove-a-file)                             |
| `select(files)`   | Makes these files the selection; `[]` clears it. [Select files](#select-files)        |
| `add(files)`      | Attaches files as one change. Resolves with the new files.                            |
| `remove(file)`    | Takes a file away, as one change.                                                     |
| `download(file)`  | Resolves `{ bytes, name, mimeType }`, ready to save. [Save a file](#save-a-file)      |
| `reveal(file)`    | Goes to a page's file and selects it. [Go to a page's file](#go-to-a-pages-file)      |
| `ensureLoaded()`  | Reads the attachments, unless they're loaded or loading. Resolves once they're there. |
| `refresh()`       | Reads the attachments again. Rarely needed: they stay up to date by themselves.       |

Every method that returns a promise also takes a `signal` in its options, to cancel it.

## State

Every value is a signal on `inject(EpdfAttachment)`: read it in a template or a `computed()`, and your view updates when it changes. The method in the last column reads the same value once.

| Field           | What it is                                                                                      | Read once       |
| :-------------- | :---------------------------------------------------------------------------------------------- | :-------------- |
| `files()`       | Every file, the document's then the pages', with your changes shown before the engine answers   | `list()`        |
| `selected()`    | The selected files, in the order they were selected                                             | `getSelected()` |
| `status()`      | `'idle'` until something asks for them, then `'loading'`, `'ready'`, `'forbidden'` or `'error'` | `getStatus()`   |
| `canAdd()`      | Whether attaching and removing files is allowed                                                 | `canAdd()`      |
| `canDownload()` | Whether saving files is allowed                                                                 | `canDownload()` |

## Events

**`attachment-changed.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { filter, map } from 'rxjs';
import { EpdfAttachment } from '@embedpdf/angular/attachment';

@Component({
  selector: 'app-attachment-notice',
  template: `<p role="status">{{ notice() }}</p>`,
})
export class AttachmentNotice {
  private readonly attachment = inject(EpdfAttachment);

  protected readonly notice = toSignal(
    this.attachment.changed$.pipe(
      filter(({ origin }) => origin.kind === 'remote'),
      map(({ type, files }) => `Someone ${type} ${files.length} file(s)`),
    ),
    { initialValue: '' },
  );
}
```

| Event       | Fires when                                                                                                                           | Carries                   |
| :---------- | :----------------------------------------------------------------------------------------------------------------------------------- | :------------------------ |
| `changed$`  | The document's attachments were added, removed or brought back by an undo, by you or by someone else                                 | `type`, `files`, `origin` |
| `resynced$` | The attachments were read from the file: the first time they're asked for, after `refresh()`, or to catch up after a lost connection | `files`                   |

`type` is `'added'`, `'removed'` or `'restored'`; for a removal, `files` are the files as they
were. A file on a page is an annotation: its changes are the annotation plugin's events.

## Permissions

| Permission               | What it allows                                  | Check                      |
| :----------------------- | :---------------------------------------------- | :------------------------- |
| `doc.open`               | Listing the files                               | none: it opened            |
| `doc.download`           | Saving a file: it leaves the document           | `attachment.canDownload()` |
| `doc.attachments.modify` | Attaching files and removing the document's own | `attachment.canAdd()`      |

Removing a file on a page is deleting its annotation, so the annotation's own permission decides:
`canRemove(file)` asks it.

## Good to know

### Large files

The server unpacks the file and applies its own limit; `download()` rejects a larger one with
`invalid-input`.

### Removed files and saving

Removing a file takes it out of the document as it is now; an incremental save still contains the
version you opened. When a file must really be gone, save with `mode: 'rewrite'`. See
[Saving](https://www.cloudpdf.com/docs/headless/angular/documents/saving).

### The ready-made viewer

The viewer's Attachments panel is built on this plugin. See
[Panels](https://www.cloudpdf.com/docs/viewer/angular/customize/panels).
