EmbedPDF

Annotations

Annotations are what people add on top of a page: highlights, sticky notes, shapes, stamps and text boxes. The annotation plugin shows the ones a PDF already has, lets people draw and edit new ones, and gives your code the same abilities. Every annotation is saved into the PDF, so it looks the same in every other PDF app.

Show the annotations in a PDF#

Register withInteraction() and withAnnotation(), then put an <epdf-annotation-layer> above the rendered page:

Loading live preview…

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  untracked,
  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,
} 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()) };
};

@Component({
  selector: 'demo-status',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <p class="hint">{{ text() }}</p>
    </div>
  `,
})
export class Status {
  private readonly annotation = inject(EpdfAnnotation);

  protected readonly text = computed(() => {
    const selected = this.annotation.selected();
    if (this.annotation.status() === 'loading') return 'Loading the annotations…';
    if (selected.length > 0) return `Selected: ${selected.map((a) => a.subtype).join(', ')}`;
    return 'Click an annotation to select it';
  });
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    Status,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './layer.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-status />
      <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 document = inject(EpdfDocument);
  private added = false;

  constructor() {
    // On load: a highlight, a rectangle, a sticky note and a text box on the cover.
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (this.annotation.status() !== 'ready' || !cover || this.added) return;
      this.added = true;
      untracked(() => {
        void this.annotation.create(cover, {
          subtype: 'highlight',
          quadPoints: [
            {
              upperLeft: { x: 106, y: 218 },
              upperRight: { x: 458, y: 218 },
              lowerLeft: { x: 106, y: 267 },
              lowerRight: { x: 458, y: 267 },
            },
          ],
          color: '#ffcd45',
        });
        void this.annotation.create(cover, {
          subtype: 'square',
          box: { x: 96, y: 506, width: 178, height: 54 },
          color: '#e5484d',
          strokeWidth: 3,
        });
        void this.annotation.create(cover, {
          subtype: 'text',
          rect: { x: 470, y: 232, width: 20, height: 20 },
          contents: 'A good title',
          color: '#facc15',
        });
        void this.annotation.create(cover, {
          subtype: 'free-text',
          box: { x: 300, y: 512, width: 230, height: 40 },
          contents: 'Double-click to type here',
          fontSize: 16,
          fontColor: '#1a2748',
          interiorColor: '#fffbe6',
        });
      });
    });
  }
}

Try it: click the rectangle and drag it, or a corner to resize it. Double-click the text box to type in it.

layer.ts
import { Component } from '@angular/core';
import { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';

import { engine } from './setup';

@Component({
  selector: 'app-document-view',
  imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfAnnotationLayer],
  providers: [
    provideEmbedPdf({ engine }, withStage(), withRender(), withInteraction(), withAnnotation()),
  ],
  template: `
    <epdf-stage>
      <ng-template epdfPage>
        <epdf-render-layer />
        <epdf-annotation-layer />
      </ng-template>
    </epdf-stage>
  `,
})
export class DocumentView {}

While the annotation layer is on the page, the render layer leaves the annotations out of the page picture by itself. The annotation layer draws them instead, so they can be selected, moved and changed.

withInteraction() is the shared input system. Every tool, from text selection to drawing, speaks through it; see Selecting & copying for how it works.

Let people draw#

A tool says what a click or a drag on the page creates. Activate one and people can draw:

Loading live preview…

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  ViewEncapsulation,
} from '@angular/core';
import { 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 { EpdfInteraction, withInteraction } from '@embedpdf/angular/interaction';
import { 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()) };
};

const TOOLS = [
  { id: 'pointer', label: 'Select' },
  { id: 'square', label: 'Rectangle' },
  { id: 'ink', label: 'Pen' },
  { id: 'highlight', label: 'Highlight' },
  { id: 'note', label: 'Note' },
];

@Component({
  selector: 'demo-toolbar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <div class="segmented" role="group" aria-label="Tool">
        @for (tool of tools; track tool.id) {
          <button
            type="button"
            [attr.aria-pressed]="interaction.activeToolId() === tool.id"
            (click)="interaction.activateTool(tool.id)"
          >
            {{ tool.label }}
          </button>
        }
      </div>
      <output class="readout">
        {{ count() }} {{ count() === 1 ? 'annotation' : 'annotations' }}
      </output>
    </div>
  `,
})
export class Toolbar {
  protected readonly tools = TOOLS;
  protected readonly interaction = inject(EpdfInteraction);
  private readonly annotations = inject(EpdfAnnotation).watch();
  protected readonly count = computed(() => this.annotations().length);

  constructor() {
    // The rectangle tool is active on load: drag on the page to draw one.
    this.interaction.activateTool('square');
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    EpdfAnnotationLayer,
    Toolbar,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      // The selection plugin lets the highlight tool mark the text you select.
      withSelection(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './draw.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-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 {}

Try it: drag on the page to draw a rectangle, then pick another tool. Highlight marks the text you select.

toolbar.ts
import { Component, inject } from '@angular/core';
import { EpdfInteraction } from '@embedpdf/angular/interaction';

@Component({
  selector: 'app-toolbar',
  template: `
    @for (id of tools; track id) {
      <button
        [attr.aria-pressed]="interaction.activeToolId() === id"
        (click)="interaction.activateTool(id)"
      >
        {{ id }}
      </button>
    }
  `,
})
export class Toolbar {
  protected readonly interaction = inject(EpdfInteraction);
  protected readonly tools = ['pointer', 'square', 'ink', 'highlight', 'note'];
}
  • pointer selects: click an annotation to select it, then move, resize or turn it.
  • square draws a rectangle: drag, or just click for a default-size one.
  • ink draws freehand.
  • highlight highlights the text you select.
  • note drops a sticky note where you click.

After you draw, the new annotation is selected and the tool stays active, so you can draw another. You can change both; see What happens after drawing.

How it fits together#

Everything on these pages is one of five things:

ThingWhat it isWhere you use it
AnnotationA record in the PDF: its kind, position, colors and text.annotation.list()
ToolWhat a click or drag creates, with which defaults, cursor and preview.withAnnotation({ tools })
LayerWhat each page draws: the annotations, the selection box and handles, the preview.<epdf-annotation-layer>
OverlayYour own UI attached to an annotation, such as a menu or a hover card.<epdf-annotation-menu>
APIReading, creating, changing and deleting annotations, and listening for changes.inject(EpdfAnnotation)

An annotation here is the same record the engine reads and writes: the same kinds, the same fields and the same names. What you learn in the engine’s annotation pages holds here too.

The built-in tools#

ToolMakesHow
square, circleA rectangle or an ellipseDrag, or click for a default size
lineA lineDrag, or click for a default length
polygon, polylineA closed or open shape of pointsClick each point, double-click to end
ink, ink-highlightFreehand drawing, or a see-through markerDraw
free-text, free-text-calloutA text box, or one with a line pointing outClick or drag, then type
highlight, underline, strikeout, squigglyA mark over textSelect text
insert-text, replace-textA suggested insertion or replacementSelect text
noteA sticky noteClick
stampAn image or a PDF drawingClick, then pick a file
attachmentA file pinned to a spotClick, then pick a file
linkAn area that opens a page or a websiteDrag

Measurement tools (distance, perimeter, area) and redaction (redact) are annotation tools too. Their plugins add the rest of what they need.

You can change any tool, or add your own; see Tools & defaults.

Who wrote it#

Every annotation records who made it and who last changed it (author, createdAt, modifiedAt). You don’t set these on the annotation: they come from the user the document was opened for.

Tell the viewer who the user is, once. Every document it opens uses it:

Loading live preview…

import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  untracked,
  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 { EpdfInteraction, 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()) };
};

// On load: a sticky note on the cover. It's Dana's, like everything drawn here.
@Component({
  selector: 'demo-add-note',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: '',
})
export class AddNote {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly document = inject(EpdfDocument);
  private added = false;

  constructor() {
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (this.annotation.status() !== 'ready' || !cover || this.added) return;
      this.added = true;
      untracked(() => {
        void this.annotation.create(cover, {
          subtype: 'text',
          rect: { x: 470, y: 232, width: 20, height: 20 },
          contents: 'Can we shorten the title?',
          color: '#facc15',
        });
      });
    });
  }
}

const TOOLS = [
  { id: 'pointer', label: 'Select' },
  { id: 'square', label: 'Rectangle' },
  { id: 'note', label: 'Note' },
];

@Component({
  selector: 'demo-toolbar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <div class="segmented" role="group" aria-label="Tool">
        @for (tool of tools; track tool.id) {
          <button
            type="button"
            [attr.aria-pressed]="interaction.activeToolId() === tool.id"
            (click)="interaction.activateTool(tool.id)"
          >
            {{ tool.label }}
          </button>
        }
      </div>
    </div>
  `,
})
export class Toolbar {
  protected readonly tools = TOOLS;
  protected readonly interaction = inject(EpdfInteraction);
}

const time = (date: string | null) =>
  date ? new Date(date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';

// Who wrote each annotation, and when: the engine fills these in from the identity.
@Component({
  selector: 'ul[demoAuthors]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'panel authors' },
  template: `
    @if (annotations().length === 0) {
      <li class="empty">Draw something</li>
    }
    @for (annotation of annotations(); track key(annotation.ref)) {
      <li class="author">
        <span class="kind">{{ annotation.subtype }}</span>
        <span>{{ annotation.author ?? 'Nobody' }} · {{ time(annotation.createdAt) }}</span>
      </li>
    }
  `,
})
export class Authors {
  protected readonly annotations = inject(EpdfAnnotation).watch();
  protected readonly key = annotationKey;
  protected readonly time = time;
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    AddNote,
    Toolbar,
    Authors,
  ],
  providers: [
    provideEmbedPdf(
      {
        engine: () => localEngine(),
        identity: { userId: 'u_381', displayName: 'Dana Smith' },
        initialDocuments: [{ source: ebook }],
      },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './identity.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-add-note />
      <demo-toolbar />
      <div class="viewer">
        <epdf-stage class="stage">
          <ng-template epdfPage>
            <epdf-render-layer />
            <epdf-annotation-layer />
          </ng-template>
        </epdf-stage>
        <ul demoAuthors></ul>
      </div>
    </ng-container>

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

Try it: draw a rectangle. The list says Dana Smith made it, and when.

identity.ts
import { Component } from '@angular/core';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';

import { Pages } from './pages';
import { contract, engine, features } from './setup';

@Component({
  selector: 'app-review',
  imports: [Pages],
  providers: [
    provideEmbedPdf(
      {
        engine,
        identity: { userId: 'u_381', displayName: 'Dana Smith' },
        initialDocuments: [{ source: contract }],
      },
      ...features,
    ),
  ],
  template: `<app-pages />`,
})
export class Review {}

displayName becomes the author other PDF apps show, and userId records who owns the annotation. The same identity decides which annotations count as the user’s own when permissions say “only their own”.

To open one document as someone else, give its entry in initialDocuments its own identity. It replaces the viewer’s for that document:

const initialDocuments = [
  { source: contract, identity: { userId: 'bot', displayName: 'Import job' } },
];

A document keeps the identity it was opened with.

To change the viewer’s identity, for example when someone else signs in, use inject(EpdfViewer).updateSettings({ identity }). Documents you open afterwards use the new one. To follow your app’s login:

effect(() => viewer.updateSettings({ identity: auth.identity() }));

Settings#

Set them when you register the plugin. snap, chrome, afterCreate and editable can change while the app runs, with annotation.updateSettings({ … }); annotation.resetSettings() goes back to what you registered, and inject(EpdfAnnotation).settings() gives you the current settings.

SettingWhat it doesPage
toolsChange the built-in tools, or add your ownTools & defaults
afterCreateSelect a new annotation? Keep the tool active?What happens after drawing
chromeThe selection’s outline, handles and rotation handle, and their colorsChange the handles
snapSnapping to other annotations and to anglesSnapping
editableWhich annotations people may change by hand: true, false, or kindsWhat people may change

Every color in chrome can also come from CSS, for dark mode or a different look per viewer: see Colors & theming.

Next steps#

Was this page helpful?

Your feedback goes directly to the documentation team.