CloudPDF
DocsPricing
Start building

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

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

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:

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.

Loading live preview…

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

Go to a page’s file#

reveal(file) selects a page’s file attachment annotation and brings it into view, through the 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.

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

Methods#

Everything inject(EpdfAttachment) gives you.

MethodWhat it does
get(id)One file by its id, or null.
isPending(file)Whether a change to it waits for the engine. Attach files
canRemove(file)Whether this file may go. Remove a file
select(files)Makes these files the selection; [] clears it. 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
reveal(file)Goes to a page’s file and selects it. Go to a page’s 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.

FieldWhat it isRead once
files()Every file, the document’s then the pages’, with your changes shown before the engine answerslist()
selected()The selected files, in the order they were selectedgetSelected()
status()'idle' until something asks for them, then 'loading', 'ready', 'forbidden' or 'error'getStatus()
canAdd()Whether attaching and removing files is allowedcanAdd()
canDownload()Whether saving files is allowedcanDownload()

Events#

attachment-changed.ts
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: '' },
  );
}
EventFires whenCarries
changed$The document’s attachments were added, removed or brought back by an undo, by you or by someone elsetype, 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 connectionfiles

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#

PermissionWhat it allowsCheck
doc.openListing the filesnone: it opened
doc.downloadSaving a file: it leaves the documentattachment.canDownload()
doc.attachments.modifyAttaching files and removing the document’s ownattachment.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.

The ready-made viewer#

The viewer’s Attachments panel is built on this plugin. See Panels.