---
title: "Annotations in code — Angular"
description: "Read, create, change, reorder and delete annotations from your own code, with changes shown at once."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/annotations/with-code"
---

# Annotations in code

Everything people do with the pointer, your code can do too. `inject(EpdfAnnotation)` gives you the
annotations on screen, and every change you make shows at once, before the engine has saved it:

```ts
const { annotation: square } = await annotation.create(page.ref, {
  subtype: 'square',
  box: { x: 72, y: 92, width: 200, height: 100 },
  color: '#0078ff',
});

await annotation.update(square.ref, { contents: 'Check this table' });
await annotation.delete(square.ref);
```

**`create-update.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } 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 { withInteraction } from '@embedpdf/angular/interaction';
import {
  EpdfAnnotation,
  EpdfAnnotationLayer,
  withAnnotation,
  type AnnotationRef,
} from '@embedpdf/angular/annotation';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// Create a rectangle, then change, move and delete it, all from code.
@Component({
  selector: 'demo-square-controls',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <button type="button" class="button" [disabled]="square() !== null" (click)="add()">
        Create
      </button>
      <button type="button" class="button" [disabled]="!canUpdate()" (click)="makeRed()">
        Make it red
      </button>
      <button type="button" class="button" [disabled]="!canUpdate()" (click)="moveRight()">
        Move right
      </button>
      <button type="button" class="button" [disabled]="!canUpdate()" (click)="turn()">
        Turn to 90°
      </button>
      <button type="button" class="button" [disabled]="!canDelete()" (click)="remove()">
        Delete
      </button>
      <span class="spacer"></span>
      <output class="readout">{{ status() }}</output>
    </div>
  `,
})
export class SquareControls {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly document = inject(EpdfDocument);
  private readonly squareRef = signal<AnnotationRef | null>(null);
  private readonly annotations = this.annotation.watch(); // follows the annotations, for the reads below

  protected readonly square = computed(() => {
    this.annotations(); // read again whenever the annotations change
    const ref = this.squareRef();
    return ref ? this.annotation.get(ref) : null;
  });
  // Checks are signals too: they follow the annotation, the document and its permissions.
  protected readonly canUpdate = computed(() => {
    const square = this.square();
    return square !== null && this.annotation.canUpdate(square.ref);
  });
  protected readonly canDelete = computed(() => {
    const square = this.square();
    return square !== null && this.annotation.canDelete(square.ref);
  });
  protected readonly status = computed(() => {
    const square = this.square();
    if (!square) return 'No rectangle';
    return this.annotation.isPending(square.ref) ? 'Saving…' : 'Saved';
  });
  private started = false;

  constructor() {
    // On load: one rectangle, to change.
    effect(() => {
      if (this.annotation.status() !== 'ready' || this.started) return;
      this.started = true;
      void this.add();
    });
  }

  protected async add() {
    const cover = this.document.pages()[0]?.ref;
    if (!cover) return;
    const { annotation: square } = await this.annotation.create(cover, {
      subtype: 'square',
      box: { x: 72, y: 592, width: 200, height: 100 },
      color: '#0078ff',
      strokeWidth: 3,
    });
    this.squareRef.set(square.ref);
  }

  protected makeRed() {
    const square = this.square();
    if (square) void this.annotation.update(square.ref, { color: '#dc143c' });
  }

  protected moveRight() {
    const square = this.square();
    if (!square) return;
    const { rect } = square;
    void this.annotation.update(square.ref, { rect: { ...rect, x: rect.x + 40 } }); // 40 points right
  }

  protected turn() {
    const square = this.square();
    if (square) void this.annotation.update(square.ref, { rotation: 90 });
  }

  protected remove() {
    const square = this.square();
    if (!square) return;
    void this.annotation.delete(square.ref);
    this.squareRef.set(null);
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    SquareControls,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './create-update.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-square-controls />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

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

**`create-update.css`**

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

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

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

.button:active {
  transform: translateY(1px);
}

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

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

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

> **Try it:** each button is one call. **Saving…** shows while the engine writes a change.

An annotation here is the engine's record: the same kinds, fields and names as the
[engine's annotation pages](https://www.embedpdf.com/docs/engine/annotations). Each kind's page there lists its fields.
These pages cover what the plugin adds on top.

## Read annotations

```ts
annotation.list(); // every annotation, in drawing order
annotation.list({ pages: [page.ref] }); // one page
annotation.list({ subtype: 'highlight' }); // one kind
annotation.get(ref); // one, or null
annotation.getAt(page.ref, { x: 120, y: 80 }); // the topmost one at a point, or null
```

To keep a component up to date, use `annotation.watch()`. Your component updates
when the list changes:

**`list.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { EpdfAnnotation } from '@embedpdf/angular/annotation';

@Component({
  selector: 'app-highlight-count',
  template: `<p>{{ highlights().length }} highlights</p>`,
})
export class HighlightCount {
  protected readonly highlights = inject(EpdfAnnotation).watch({ subtype: 'highlight' });
}
```

**`list.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  input,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } 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 { withInteraction } from '@embedpdf/angular/interaction';
import {
  annotationKey,
  EpdfAnnotation,
  EpdfAnnotationLayer,
  withAnnotation,
  type Annotation,
  type AnnotationSubtype,
} from '@embedpdf/angular/annotation';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// A line of text, as the four corners a highlight takes.
const quad = (x: number, y: number, width: number, height: number) => ({
  upperLeft: { x, y },
  upperRight: { x: x + width, y },
  lowerLeft: { x, y: y + height },
  lowerRight: { x: x + width, y: y + height },
});

interface Kind {
  label: string;
  subtype?: AnnotationSubtype;
}

const KINDS: Kind[] = [
  { label: 'All' },
  { label: 'Highlights', subtype: 'highlight' },
  { label: 'Notes', subtype: 'text' },
  { label: 'Rectangles', subtype: 'square' },
];

/**
 * The annotations of one kind, in drawing order. A click shows one on its page. Next to
 * `<epdf-stage #stage="epdfStage">`: `<div demoAnnotationList [stage]="stage"></div>`.
 */
@Component({
  selector: 'div[demoAnnotationList]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'panel' },
  template: `
    <div class="segmented" role="group" aria-label="Kind">
      @for (each of kinds; track each.label) {
        <button type="button" [attr.aria-pressed]="each === kind()" (click)="kind.set(each)">
          {{ each.label }}
        </button>
      }
    </div>
    <p class="count">{{ annotations().length }} found</p>
    <ul class="items">
      @for (each of annotations(); track key(each.ref)) {
        <li>
          <button type="button" class="item" (click)="show(each)">
            <span class="kind"
              >{{ each.subtype }} · page {{ pageNumberOf(each.page.objectNumber) }}</span
            >
            <span>{{ each.contents ?? '—' }}</span>
          </button>
        </li>
      }
    </ul>
  `,
})
export class AnnotationList {
  readonly stage = input.required<EpdfStage>();

  private readonly annotation = inject(EpdfAnnotation);
  private readonly pages = inject(EpdfDocument).pages;
  protected readonly kinds = KINDS;
  protected readonly kind = signal(KINDS[0]!);
  protected readonly annotations = this.annotation.watch(() => {
    const subtype = this.kind().subtype;
    return subtype ? { subtype } : undefined;
  });
  protected readonly key = annotationKey;

  protected pageNumberOf(objectNumber: number) {
    return this.pages().findIndex((page) => page.ref.objectNumber === objectNumber) + 1;
  }

  protected show(each: Annotation) {
    this.stage().reveal(each.page, { rect: each.rect });
    this.annotation.selection.set([each.ref]);
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    AnnotationList,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './list.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="viewer">
        <epdf-stage #stage="epdfStage" class="stage">
          <ng-template epdfPage>
            <epdf-render-layer />
            <epdf-annotation-layer />
          </ng-template>
        </epdf-stage>
        <div demoAnnotationList [stage]="stage"></div>
      </div>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly pages = inject(EpdfDocument).pages;
  private added = false;

  constructor() {
    // On load: highlights, notes and a rectangle on the first two pages.
    effect(() => {
      const [cover, second] = this.pages();
      if (this.annotation.status() !== 'ready' || !cover || !second || this.added) return;
      this.added = true;
      void this.annotation.create(cover.ref, {
        subtype: 'highlight',
        quadPoints: [quad(106, 218, 352, 49)],
        color: '#ffcd45',
        contents: 'The title',
      });
      void this.annotation.create(cover.ref, {
        subtype: 'text',
        rect: { x: 470, y: 232, width: 20, height: 20 },
        contents: 'Can we shorten it?',
        color: '#facc15',
      });
      void this.annotation.create(second.ref, {
        subtype: 'highlight',
        quadPoints: [quad(57, 57, 242, 36), quad(57, 93, 265, 36)],
        color: '#ffcd45',
        contents: 'The opening line',
      });
      void this.annotation.create(second.ref, {
        subtype: 'square',
        box: { x: 52, y: 580, width: 470, height: 64 },
        color: '#e5484d',
        strokeWidth: 2,
        contents: 'Rewrite this paragraph',
      });
    });
  }
}
```

**`list.css`**

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

/* One choice of a few: the pressed button is the current one */
.segmented {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 3px;
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 11px;
}

.segmented button {
  font: inherit;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 6px 12px;
  cursor: pointer;
  white-space: nowrap;
}

.segmented button:hover {
  color: var(--ink);
}

.segmented button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--surface);
  box-shadow: 0 1px 2px rgb(7 32 76 / 0.1);
}

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

/* The list: a kind filter, then one row per annotation */
.panel .segmented {
  margin-bottom: 8px;
}

.count {
  margin: 0 0 6px;
  font-size: 12px;
  color: var(--muted);
}

.items {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  font: inherit;
  font-size: 13px;
  text-align: left;
  color: var(--text);
  background: transparent;
  border: 0;
  border-radius: 9px;
  padding: 8px 10px;
  cursor: pointer;
}

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

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

.kind {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The pages and the panel: stacked, and side by side where there's room */
.viewer {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.stage,
.panel {
  flex: 1 1 100%;
  box-sizing: border-box;
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.stage {
  height: 340px;
  background: var(--canvas);
}

.panel {
  max-height: 260px;
  overflow-y: auto;
  margin: 0;
  padding: 12px;
  background: var(--surface);
}

@container (min-width: 620px) {
  .stage {
    flex: 1 1 0;
    min-width: 0;
    height: 420px;
  }

  .panel {
    flex: 0 0 260px;
    max-height: 420px;
  }
}

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

> **Try it:** pick a kind, then click a row to go to it.

Each annotation has a `subtype` (its kind), a `rect` (where it is on its page), a `page` and a
`ref`. Keep the `ref`: it's how you change or delete the annotation later. Positions are in
points from the top-left of the page; see [Coordinates](../concepts/coordinates).

Annotations are loaded in the background when a document opens. `inject(EpdfAnnotation)` has a
`status`: `'loading'` until they're all in, then `'ready'`.

## Create an annotation

`create()` takes the page, then the annotation's kind and fields:

```ts
const { annotation: note } = await annotation.create(page.ref, {
  subtype: 'text',
  rect: { x: 500, y: 72, width: 20, height: 20 },
  contents: 'Is this total right?',
});
```

Fields you leave out get the kind's default. For example, a new annotation prints with the page
unless you pass `print: false`. Every kind the engine can create, the plugin can too; the
[engine's pages](https://www.embedpdf.com/docs/engine/annotations) show each one.

Some options change how the plugin treats the new annotation:

```ts
await annotation.create(page.ref, { subtype: 'square', box }, undefined, {
  tool: 'square',
  select: true,
});
```

| Option   | What it does                                                                                   |
| :------- | :--------------------------------------------------------------------------------------------- |
| `tool`   | Starts from that tool's defaults, the user's current drawing style, before your own fields.    |
| `select` | Selects the new annotation, as drawing it would. Without it, code never changes the selection. |

Stamps and file attachments carry bytes. They go in the third argument, `resources`, named for what
they are: a stamp's `appearance`, an attachment's `file`.

```ts
const image = await (await fetch('/approved.png')).blob();
await annotation.create(page.ref, { subtype: 'stamp', box }, { appearance: image });

// A file pinned to the page, drawn as an icon, 20 × 20 like a note
await annotation.create(
  page.ref,
  { subtype: 'file-attachment', rect: { x: 40, y: 60, width: 20, height: 20 } },
  { file: pickedFile }, // a File brings its name and type
);
```

For stamps from a library, see [Stamps](stamps).

### From the selected text

`createFromSelection()` turns the text a person selected into a highlight, an underline, a
strikeout or a squiggly line, one per page:

```ts
const { annotations } = await annotation.createFromSelection('highlight');
```

It takes a tool id, so your own markup tools work too. The text selection is cleared afterwards;
pass `{ clear: false }` to keep it.

**`from-selection.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } 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 { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfSelection, EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { EpdfAnnotation, EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// On the cover: the characters of the title's first line.
const FIRST_LINE = { start: 10, count: 17 };

const MARKUP = [
  { tool: 'highlight', label: 'Highlight' },
  { tool: 'underline', label: 'Underline' },
  { tool: 'strikeout', label: 'Strike out' },
  { tool: 'squiggly', label: 'Squiggly' },
];

// The selected text becomes a mark, one per page; the selection is cleared afterwards.
@Component({
  selector: 'demo-markup-toolbar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      @for (markup of markups; track markup.tool) {
        <button
          type="button"
          class="button"
          [disabled]="!selection.hasSelection()"
          (click)="mark(markup.tool, markup.label)"
        >
          {{ markup.label }}
        </button>
      }
      <span class="spacer"></span>
      <output class="readout">{{ readout() }}</output>
    </div>
  `,
})
export class MarkupToolbar {
  private readonly annotation = inject(EpdfAnnotation);
  protected readonly selection = inject(EpdfSelection);
  private readonly document = inject(EpdfDocument);
  protected readonly markups = MARKUP;
  private readonly status = signal('');
  protected readonly readout = computed(() =>
    this.selection.hasSelection() ? 'Text selected' : this.status() || 'Select text',
  );

  constructor() {
    // On load: the title's first line is selected, ready to mark.
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (cover) this.selection.select({ page: cover, ...FIRST_LINE });
    });
  }

  protected async mark(tool: string, label: string) {
    const { annotations } = await this.annotation.createFromSelection(tool);
    this.status.set(`${label}: ${annotations.length} made`);
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    EpdfAnnotationLayer,
    MarkupToolbar,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './from-selection.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-markup-toolbar />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-selection-layer />
          <epdf-annotation-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

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

**`from-selection.css`**

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

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

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

.button:active {
  transform: translateY(1px);
}

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

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

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

> **Try it:** the title's first line is selected: press **Highlight**. Then select other words
> with the pointer and mark them.

## Change an annotation

`update()` takes the `ref` and only the fields you want to change:

```ts
await annotation.update(ref, { color: '#dc143c', contents: 'Reviewed' });
```

A field you leave out keeps its value, and `null` removes it (`{ interiorColor: null }` for no
fill). You can also pass an annotation you read straight back, which makes "change a copy, then
save it" simple:

```ts
const square = annotation.get(ref);
if (square?.subtype === 'square') await annotation.update(ref, { ...square, opacity: 0.5 });
```

To move or resize any annotation, send a new `rect`:

```ts
const { rect } = annotation.get(ref)!;
await annotation.update(ref, { rect: { ...rect, x: rect.x + 40 } }); // 40 points right
```

For a shape drawn from points, like a line or ink, the engine moves every point for you. The rules
for resizing are on the engine's [Annotations](https://www.embedpdf.com/docs/engine/annotations#move-or-resize-by-rect)
page.

Rotation is a field too: `update(ref, { rotation: 90 })` turns a shape or a stamp to 90° clockwise.

## Delete an annotation

```ts
await annotation.delete(ref);
```

Deleting an annotation also deletes its popup and its replies, as other PDF apps do.

## Change the drawing order

Annotations on a page are drawn in order, and the last one is on top. `reorder()` moves
annotations together, in the order you pass them, next to another annotation of the page or to the
back or the front:

```ts
await annotation.reorder([ref], 'start'); // to the back
await annotation.reorder([ref], 'end'); // to the front
await annotation.reorder([ref], { after: other }); // just above another annotation
```

Form fields are drawn above every annotation and keep their own order, so `reorder()` never
places an annotation above a field.

**`order.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } 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 { withInteraction } from '@embedpdf/angular/interaction';
import {
  annotationKey,
  EpdfAnnotation,
  EpdfAnnotationLayer,
  withAnnotation,
} from '@embedpdf/angular/annotation';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const COLORS = [
  { color: '#e5484d', fill: '#ffd1d3' },
  { color: '#30a46c', fill: '#c9f0da' },
  { color: '#1e90ff', fill: '#cfe6ff' },
];

// The last annotation on a page is drawn on top.
@Component({
  selector: 'demo-order-controls',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <button type="button" class="button" [disabled]="!single()" (click)="sendToBack()">
        Send to back
      </button>
      <button type="button" class="button" [disabled]="!single()" (click)="bringToFront()">
        Bring to front
      </button>
      <span class="spacer"></span>
      <output class="readout">{{ readout() }}</output>
    </div>
  `,
})
export class OrderControls {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly first = computed(() => this.annotation.selected()[0]);
  protected readonly single = computed(() => this.annotation.selected().length === 1);
  private readonly onPage = this.annotation.watch(() => {
    const first = this.first();
    return first ? { pages: [first.page] } : undefined;
  });
  private readonly position = computed(() => {
    const first = this.first();
    if (!first) return -1;
    return this.onPage().findIndex((a) => annotationKey(a.ref) === annotationKey(first.ref));
  });
  protected readonly readout = computed(() =>
    this.position() >= 0
      ? `${this.position() + 1} of ${this.onPage().length}, from the back`
      : 'Select a rectangle',
  );

  protected sendToBack() {
    const first = this.first();
    if (first) void this.annotation.reorder([first.ref], 'start');
  }

  protected bringToFront() {
    const first = this.first();
    if (first) void this.annotation.reorder([first.ref], 'end');
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    OrderControls,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './order.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-order-controls />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly pages = inject(EpdfDocument).pages;
  private added = false;

  constructor() {
    // On load: three filled rectangles stacked on the cover, the middle one selected.
    effect(() => {
      const cover = this.pages()[0]?.ref;
      if (this.annotation.status() !== 'ready' || !cover || this.added) return;
      this.added = true;
      COLORS.forEach(({ color, fill }, index) => {
        void this.annotation.create(
          cover,
          {
            subtype: 'square',
            box: { x: 300 + index * 50, y: 520 + index * 30, width: 160, height: 90 },
            color,
            interiorColor: fill,
            strokeWidth: 3,
          },
          undefined,
          { select: index === 1 },
        );
      });
    });
  }
}
```

**`order.css`**

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

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

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

.button:active {
  transform: translateY(1px);
}

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

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

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

> **Try it:** send the green rectangle to the back, then select another and bring it to the
> front.

## Changes show at once

A change shows the moment you make it. The engine saves it in the background, and your `await`
resolves once it has. Until then the annotation is **pending**:

```ts
annotation.isPending(ref); // true while the engine is still saving a change to it
```

You rarely need this: `get()` and `list()` already show the change. It's useful for a small
"saving…" hint.

If the engine refuses a change (a lock set by someone else, or permission taken away), the change
disappears again, and an event says why:

**`write-failed.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfAnnotation } from '@embedpdf/angular/annotation';

import { toast } from './toast';

@Component({
  selector: 'app-save-errors',
  template: '',
})
export class SaveErrors {
  constructor() {
    inject(EpdfAnnotation)
      .writeFailed$.pipe(takeUntilDestroyed())
      .subscribe(({ error }) => toast(`Couldn't save: ${error.message}`));
  }
}
```

The `await` of the call that made the change rejects with the same error.

## Copy an annotation

A read passed to `create()` makes the same annotation again, on another page or in another
document:

```ts
const copy = annotation.get(ref)!;

await annotation.create(otherPage.ref, copy);
```

**`copy.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
  signal,
  untracked,
  viewChild,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotation, EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

/**
 * A read passed to create() makes the same annotation again, here on the next page. Next to
 * `<epdf-stage #stage="epdfStage">`: `<demo-copy-button [stage]="stage" />`.
 */
@Component({
  selector: 'demo-copy-button',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <button
        type="button"
        class="button"
        [disabled]="annotation.selected().length !== 1 || !next()"
        (click)="copyToNextPage()"
      >
        Copy to the next page
      </button>
      <span class="spacer"></span>
      <output class="readout">
        {{ status() || (first() ? 'Ready to copy' : 'Select an annotation to copy') }}
      </output>
    </div>
  `,
})
export class CopyButton {
  readonly stage = input.required<EpdfStage>();

  protected readonly annotation = inject(EpdfAnnotation);
  private readonly pages = inject(EpdfDocument).pages;
  protected readonly status = signal('');
  protected readonly first = computed(() => this.annotation.selected()[0]);
  protected readonly next = computed(() => {
    const first = this.first();
    if (!first) return undefined;
    const pages = this.pages();
    const index = pages.findIndex((page) => page.ref.objectNumber === first.page.objectNumber);
    return pages[index + 1];
  });

  protected async copyToNextPage() {
    const first = this.first();
    const next = this.next();
    if (!first || !next) return;
    const copy = this.annotation.get(first.ref)!;
    const { annotation: made } = await this.annotation.create(next.ref, copy, undefined, {
      select: true, // so the next click copies the copy, a page further
    });
    this.stage().reveal(next.ref, { rect: made.rect });
    this.status.set(`Copied to page ${next.index + 1}`);
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    CopyButton,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './copy.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-copy-button [stage]="stage" />
      <epdf-stage #stage="epdfStage" class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly pages = inject(EpdfDocument).pages;
  private readonly stage = viewChild(EpdfStage);
  private added = false;

  constructor() {
    // On load: a rectangle and a text box on the cover, the rectangle selected and in view.
    effect(() => {
      const stage = this.stage();
      const cover = this.pages()[0]?.ref;
      if (!stage || !cover || this.annotation.status() !== 'ready' || this.added) return;
      this.added = true;
      untracked(() => this.addAnnotations(stage, cover));
    });
  }

  private addAnnotations(stage: EpdfStage, cover: PageRef) {
    void this.annotation.create(cover, {
      subtype: 'free-text',
      box: { x: 300, y: 512, width: 230, height: 40 },
      contents: 'Copy me too',
      fontSize: 16,
      fontColor: '#1a2748',
      interiorColor: '#fffbe6',
    });
    void this.annotation
      .create(
        cover,
        {
          subtype: 'square',
          box: { x: 96, y: 506, width: 178, height: 54 },
          color: '#e5484d',
          interiorColor: '#ffe4e1',
          strokeWidth: 3,
        },
        undefined,
        { select: true },
      )
      .then(({ annotation: made }) => stage.reveal(cover, { rect: made.rect }));
  }
}
```

**`copy.css`**

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

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

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

.button:active {
  transform: translateY(1px);
}

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

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

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

> **Try it:** copy the rectangle, then copy the copy: it moves a page further each time. Select the
> text box to copy it instead.

Stamps and file attachments also need their bytes. Download them first:

```ts
const drawing = await annotation.downloadResource(stamp.ref, 'appearance');
await annotation.create(otherPage.ref, stamp, { appearance: drawing });

const file = await annotation.downloadResource(attachment.ref, 'file');
await annotation.create(otherPage.ref, attachment, { file });
```

To copy on the **same page**, clear its unique name first: `{ ...copy, nm: null }`. A name is
unique per page, so a copy with a taken name is refused.

To copy many annotations at once, use [export and import](saving#export-and-import).

## Methods

What `inject(EpdfAnnotation)` gives you directly. Its parts have their own pages: `selection`, `draft` and
`text` are on [Selecting & editing](selecting#methods), `tools` and `stamps` on
[Tools & defaults](tools#methods), and `comments` on [Comments & replies](comments#methods).

| Method                                                         | What it does                                                                                                                                                                                                                                     |
| :------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get(ref)`                                                     | One annotation as it shows now, pending changes included, or `null`                                                                                                                                                                              |
| `list({ pages?, subtype? })`                                   | Every annotation in drawing order, or the ones on some pages or of one kind. In a component, use `annotation.watch(filter)`. [Read annotations](#read-annotations)                                                                               |
| `getAt(page, point)`                                           | The topmost annotation at a point on a page, or `null`. A selected annotation's handles count as the annotation.                                                                                                                                 |
| `isPending(ref)`                                               | Whether the engine is still saving a change to it. [Changes show at once](#changes-show-at-once)                                                                                                                                                 |
| `create(page, fields, resources?, { tool?, select? })`         | Creates an annotation from its kind and fields, and shows it at once. A stamp's drawing or an attachment's file goes in `resources`, as `{ appearance }` or `{ file }`. Resolves `{ annotation }`. [Create an annotation](#create-an-annotation) |
| `createFromSelection(tool, { clear? })`                        | Turns the selected text into a highlight, an underline, a strikeout or a squiggly line, one per page. Resolves `{ annotations }`. [From the selected text](#from-the-selected-text)                                                              |
| `update(ref, changes, resources?)`                             | Changes only the fields you pass, and `null` removes one. New `resources` replace a stamp's drawing or an attachment's file. Resolves `{ annotation }`. [Change an annotation](#change-an-annotation)                                            |
| `delete(ref)`                                                  | Deletes an annotation, with its popup and its replies. [Delete an annotation](#delete-an-annotation)                                                                                                                                             |
| `reorder(refs, position)`                                      | Moves annotations to a new place in their page's drawing order: next to another annotation, or to the back or front. [Change the drawing order](#change-the-drawing-order)                                                                       |
| `export({ refs?, pages? })`, `import(bundle, options?)`        | Takes annotations out as a bundle, with the bytes they carry, or puts a bundle in. `import` resolves `{ annotations, refMap, dropped }`. [Export and import](saving#export-and-import)                                                           |
| `downloadResource(ref, role)`                                  | Downloads a stamp's drawing (`'appearance'`) or an attachment's file (`'file'`), ready to pass back to `create()`. [Copy an annotation](#copy-an-annotation)                                                                                     |
| `links.get(ref)`, `links.set(ref, target)`, `links.clear(ref)` | Where an annotation links to, such as a page or a website: read it, set it, or remove the link                                                                                                                                                   |
| `refresh()`                                                    | Reads every annotation from the engine again. Changes arrive by themselves, so you rarely need it.                                                                                                                                               |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()`  | Reads `snap`, `chrome` and `afterCreate`, changes them while the app runs, or goes back to what you registered. In a component, use `inject(EpdfAnnotation).settings()`. [Settings](../annotations#settings)                                     |
| `canRead()`, `canCreate()`, `canUpdate(ref)`, `canDelete(ref)` | Whether the user may see, create, change or delete annotations. The checks that take a `ref` answer for that annotation. [Permissions](#permissions)                                                                                             |

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

## Permissions

| Permission           | What it allows     | Check                       |
| :------------------- | :----------------- | :-------------------------- |
| `doc.annotate.read`  | Seeing annotations | `annotation.canRead()`      |
| `annotations:create` | Creating them      | `annotation.canCreate()`    |
| `annotations:update` | Changing them      | `annotation.canUpdate(ref)` |
| `annotations:delete` | Deleting them      | `annotation.canDelete(ref)` |

A permission can end in `:self`, the user's own annotations, or `:group`, their group's.
`annotations:update:self` lets someone change what they made and only look at the rest, so the checks
that take a `ref` answer per annotation. They also follow the annotation's own flags: for a `locked`
one, `canDelete(ref)` is false, so your UI offers no delete button, even though your own code may still
delete it (see [Locked annotations](#locked-annotations)). More on
[Permissions](../concepts/permissions).

## Good to know

### Locked annotations

The `locked` flag stops people from moving, resizing or deleting an annotation, and
`lockedContents` stops them from changing its text. These flags are for the people using your
app: your own code can still change a locked annotation, as the engine can. To stop the code
too, check the flags first.

### What a copy can't carry

A few annotations can't be made again from their read, and `create()` refuses them with
`invalid-input`:

- a link whose action the engine can't create, such as one that runs JavaScript;
- a popup whose annotation is gone;
- a value another app wrote that the engine can't write, such as a beveled border;
- a kind the engine doesn't know (`subtype: 'unsupported'`).

[Import](saving#export-and-import) leaves these out and tells you which ones, and why.

### Changes other people make

On a shared document, other people's changes arrive as events and show up in `list()` by
themselves. See [Events, saving & sharing](saving).
