---
title: "Stamps — Angular"
description: "Libraries of reusable stamps, as Acrobat-compatible PDFs, placed by click or by code, made from annotations, and kept across sessions."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/annotations/stamps"
---

# Stamps

A stamp is artwork you place on a page: "Approved", a company seal, a signature. The stamp plugin
keeps stamps in **libraries**, arms one for a click, and writes the placed stamp the way Acrobat
does, so it's the same stamp there.

Register `withStamp()` next to `withAnnotation()`, and use `inject(EpdfStamp)`. The example imports the
standard library, lists its stamps and arms one: hover a page to see where it lands, and click to
place it:

**`basic.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  input,
  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 { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfStamp, withStamp, type StampAsset } from '@embedpdf/angular/stamp';
import { loadDefaultLibrary } from '@embedpdf/default-stamps/library';
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const assetEngine = engine; // stamp libraries are PDFs; they open here too

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 stamp's button: its picture, and pressed while it's armed.
@Component({
  selector: 'button[demoStampButton]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    type: 'button',
    class: 'button',
    '[title]': 'asset().label',
    '[attr.aria-pressed]': 'armed()',
    '(click)': 'toggle()',
  },
  template: `
    @if (url(); as url) {
      <img [src]="url" [alt]="asset().label" class="preview" />
    } @else {
      {{ asset().label }}
    }
  `,
})
export class StampButton {
  readonly asset = input.required<StampAsset>();
  readonly armed = input(false);

  private readonly stamp = inject(EpdfStamp);
  protected readonly url = this.stamp.previewUrlOf(() => this.asset().id);

  protected toggle() {
    if (this.armed()) this.stamp.disarm();
    else void this.stamp.armAsset(this.asset().id);
  }
}

@Component({
  selector: 'demo-stamp-picker',
  imports: [StampButton],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      @if (library()) {
        @for (asset of shown(); track asset.id) {
          <button demoStampButton [asset]="asset" [armed]="armedAsset()?.id === asset.id"></button>
        }
        <span class="spacer"></span>
        <output class="readout">
          @if (armedAsset(); as armed) {
            Click a page to place “{{ armed.label }}”
          } @else {
            Pick a stamp
          }
        </output>
      } @else {
        <output class="readout">Loading the stamps…</output>
      }
    </div>
  `,
})
export class StampPicker {
  private readonly stamp = inject(EpdfStamp);
  protected readonly library = computed(() => this.stamp.libraries()[0]);
  protected readonly assets = this.stamp.assetsOf(() => ({ libraryId: this.library()?.id }));
  protected readonly shown = computed(() => this.assets().slice(0, 6));
  protected readonly armedAsset = this.stamp.armedAsset; // the stamp the next click places

  constructor() {
    // On load: the standard stamps, English edition, with "Approved" armed.
    void loadDefaultLibrary('en')
      .then((bytes) => this.stamp.importLibrary(bytes))
      .then(({ library }) => {
        const approved = this.stamp
          .listAssets({ libraryId: library.id })
          .find((asset) => asset.name === 'Approved');
        if (approved) return this.stamp.armAsset(approved.id);
      });
  }
}

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

**`basic.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --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;
  min-height: 34px;
  box-sizing: border-box;
  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: 7px 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:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

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

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

/* A stamp's picture, inside its button */
.preview {
  display: block;
  height: 22px;
  max-width: 120px;
}

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

```ts
const { library } = await stamp.importLibrary(await loadDefaultLibrary('en'));
await stamp.armAsset(assetId); // the next click places it
```

- **A library names itself.** A library is a PDF: its title is the library's name, and its named
  pages are the stamps. An Acrobat stamp file imports as it is.
- **Libraries are shared, placing is per document.** Every open document sees the same libraries.
  Arming and placing act on the document your component is in, like every other service.
- **The preview stays sharp.** A vector stamp is drawn for the size it's shown at, so a big stamp at
  300% is as crisp as the placed one.

## A library is a PDF

Every library is one PDF, the kind Acrobat uses for its own stamp files:

- the document's title is the library's name;
- the named pages are the stamps, as `identifier=label` pairs such as `Approved=Goedgekeurd`;
- each page is one stamp's artwork, vector or image.

The identifier is the stamp's lasting identity, and the placed annotation's `name`. The label is
what a picker shows. Two libraries can share identifiers: a Dutch and an English "Approved" are the
same stamp with different labels.

```ts
// Any PDF becomes a library. A plain PDF gives one stamp per page.
const { library } = await stamp.importLibrary(bytes);

// The library as the PDF it is: drop it into Acrobat's stamps folder
const pdf = await stamp.exportLibrary(library.id);

// Add a stamp: a one-page PDF (vector) or a PNG or JPEG
const { asset } = await stamp.createAsset({ libraryId: library.id, label: 'Paid', source: pngBytes });

// Rename it; the identifier never changes
await stamp.updateAsset(asset.id, { label: 'PAID' });
```

## Show a stamp picker

A button per stamp arms it, and each button shows the stamp's preview:

**`picker.ts`**

```typescript
import { Component, inject, input } from '@angular/core';
import { EpdfStamp } from '@embedpdf/angular/stamp';

import { StampPreview } from './preview';

@Component({
  selector: 'app-stamp-picker',
  imports: [StampPreview],
  template: `
    @for (asset of assets(); track asset.id) {
      <button (click)="stamp.armAsset(asset.id)">
        <app-stamp-preview [assetId]="asset.id" />
        {{ asset.label }}
      </button>
    }
  `,
})
export class StampPicker {
  readonly libraryId = input.required<string>();

  protected readonly stamp = inject(EpdfStamp);
  protected readonly assets = this.stamp.assetsOf(() => ({ libraryId: this.libraryId() }));
}
```

**`preview.ts`**

```typescript
import { Component, inject, input } from '@angular/core';
import { EpdfStamp } from '@embedpdf/angular/stamp';

@Component({
  selector: 'app-stamp-preview',
  template: `
    @if (url(); as url) {
      <img [src]="url" alt="" />
    }
  `,
})
export class StampPreview {
  readonly assetId = input.required<string>();

  protected readonly url = inject(EpdfStamp).previewUrlOf(this.assetId);
}
```

`stamp.libraries()` lists the libraries, and `stamp.assetsOf({ libraryId })` the stamps in one.
Both update when stamps are added, renamed or removed. `stamp.armedAsset()` is the armed stamp, to
show which button is on.

## Library kinds

A library says what it holds: `kind` is `'stamps'` (the default), `'signatures'` (a person's marks;
see [Signatures](../forms/signatures)), or a name of your own:

```ts
await stamp.createLibrary('Review marks', { kind: 'review' });
await stamp.importLibrary(bytes, { libraryKind: 'legal-seals' }); // override what the file says
```

A picker asks for the kinds it shows:

**`library-kinds.ts`**

```typescript
import { Component, computed, inject } from '@angular/core';
import { EpdfStamp } from '@embedpdf/angular/stamp';

const KINDS = ['stamps', 'legal-seals'];

@Component({
  selector: 'app-library-list',
  template: `
    <ul>
      @for (library of libraries(); track library.id) {
        <li>{{ library.name }}</li>
      }
    </ul>
  `,
})
export class LibraryList {
  private readonly stamp = inject(EpdfStamp);

  protected readonly libraries = computed(() =>
    this.stamp.libraries().filter((library) => KINDS.includes(library.kind)),
  );
}
```

## Draw or type a stamp

A stamp can be **drawn, typed, an image, or a page of a PDF**. Pass a `mark` instead of `source`:

```ts
await stamp.createAsset({ libraryId, label: 'Signature', mark: { kind: 'ink', strokes, strokeWidth: 2.5 } });
await stamp.createAsset({ libraryId, label: 'Initials', mark: { kind: 'text', text: 'AL', fontFamily: 'times-italic' } });
await stamp.createAsset({ libraryId, label: 'Logo', mark: { kind: 'image', source: pngBytes } });
await stamp.createAsset({ libraryId, label: 'Seal', mark: { kind: 'pdf', source: pdfBytes, pageIndex: 2 } });
```

Ink and text are drawn by the engine as vector artwork, exactly as a placed annotation would show.
The example makes a drawn signature and typed initials; type more, and place them:

**`marks.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  input,
  signal,
  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 { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfStamp, withStamp, type StampAsset } from '@embedpdf/angular/stamp';
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const assetEngine = engine; // stamp libraries are PDFs; they open here too

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 signature as a pen would draw it: two strokes, in points.
const loop = Array.from({ length: 48 }, (_, i) => ({
  x: i * 4,
  y: 30 - Math.sin(i / 3) * 16 - i * 0.2,
}));
const underline = [
  { x: 10, y: 52 },
  { x: 180, y: 46 },
];

@Component({
  selector: 'button[demoMarkButton]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    type: 'button',
    class: 'button',
    '[title]': "'Place “' + asset().label + '”'",
    '[attr.aria-pressed]': 'armed()',
    '(click)': 'toggle()',
  },
  template: `
    @if (url(); as url) {
      <img [src]="url" [alt]="asset().label" class="preview" />
    } @else {
      {{ asset().label }}
    }
  `,
})
export class MarkButton {
  readonly asset = input.required<StampAsset>();
  readonly armed = input(false);

  private readonly stamp = inject(EpdfStamp);
  protected readonly url = this.stamp.previewUrlOf(() => this.asset().id);

  protected toggle() {
    if (this.armed()) this.stamp.disarm();
    else void this.stamp.armAsset(this.asset().id);
  }
}

@Component({
  selector: 'demo-marks',
  imports: [MarkButton],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      @for (asset of marks(); track asset.id) {
        <button demoMarkButton [asset]="asset" [armed]="armedAsset()?.id === asset.id"></button>
      }
      <span class="spacer"></span>
      <form class="type" (submit)="$event.preventDefault(); typeMark()">
        <input
          #field
          class="field"
          aria-label="Text of a typed stamp"
          [value]="text()"
          (input)="text.set(field.value)"
        />
        <button type="submit" class="button" [disabled]="!library() || !text().trim()">
          Type a stamp
        </button>
      </form>
    </div>
  `,
})
export class Marks {
  private readonly stamp = inject(EpdfStamp);
  protected readonly library = computed(() => this.stamp.libraries()[0]);
  protected readonly marks = this.stamp.assetsOf(() => ({ libraryId: this.library()?.id }));
  protected readonly armedAsset = this.stamp.armedAsset;
  protected readonly text = signal('Ada L.');

  constructor() {
    // On load: a drawn signature and typed initials, in a library of their own.
    void this.stamp.createLibrary('Ada Lovelace').then(async ({ library }) => {
      await this.stamp.createAsset({
        libraryId: library.id,
        label: 'Signature',
        mark: { kind: 'ink', strokes: [loop, underline], strokeWidth: 2.5, color: '#1d2b53' },
      });
      await this.stamp.createAsset({
        libraryId: library.id,
        label: 'Initials',
        mark: { kind: 'text', text: 'AL', fontFamily: 'times-italic', color: '#1d2b53' },
      });
    });
  }

  protected async typeMark() {
    const library = this.library();
    const text = this.text().trim();
    if (!library || !text) return;
    const { asset } = await this.stamp.createAsset({
      libraryId: library.id,
      label: text,
      mark: { kind: 'text', text, fontFamily: 'times-italic', color: '#1d2b53' },
    });
    await this.stamp.armAsset(asset.id);
  }
}

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

**`marks.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --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;
  min-height: 34px;
  box-sizing: border-box;
  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: 7px 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:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

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

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

/* A stamp's picture, inside its button */
.preview {
  display: block;
  height: 22px;
  max-width: 120px;
}

/* A text box or a menu in the toolbar */
.field {
  appearance: none;
  min-width: 0;
  box-sizing: border-box;
  min-height: 34px;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

select.field {
  appearance: auto;
  padding-right: 6px;
  cursor: pointer;
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
}

/* Type a stamp: the text and its button, together */
.type {
  display: flex;
  flex: 0 1 280px;
  gap: 8px;
  min-width: 0;
}

.type .field {
  flex: 1 1 auto;
  width: 100%;
}

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

## Place a stamp with code

A click after `armAsset()` and a call to `placeAsset()` make the same annotation from the same
inputs:

**`programmatic.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
  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 { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotation, EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfStamp, withStamp } from '@embedpdf/angular/stamp';
import { loadDefaultLibrary } from '@embedpdf/default-stamps/library';
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const assetEngine = engine; // stamp libraries are PDFs; they open here too

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

// The middle of a Letter page, and the cover's empty corner, in page coordinates.
const MIDDLE = { x: 306, y: 396 };
const CORNER = { x: 60, y: 590, width: 220, height: 180 };

/** Next to `<epdf-stage #stage="epdfStage">`: `<demo-place-stamps [stage]="stage" />`. */
@Component({
  selector: 'demo-place-stamps',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <button type="button" class="button" [disabled]="!approved()" (click)="approve()">
        Approve this page
      </button>
      <button type="button" class="button" [disabled]="!draft()" (click)="draftEverywhere()">
        “Draft” on every page
      </button>
      <span class="spacer"></span>
      <output class="readout">
        {{ stamps().length }} {{ stamps().length === 1 ? 'stamp' : 'stamps' }} in the document
      </output>
    </div>
  `,
})
export class PlaceStamps {
  readonly stage = input.required<EpdfStage>();

  private readonly stamp = inject(EpdfStamp);
  private readonly annotation = inject(EpdfAnnotation);
  private readonly assets = this.stamp.assetsOf();
  protected readonly approved = computed(() =>
    this.assets().find((asset) => asset.name === 'Approved'),
  );
  protected readonly draft = computed(() => this.assets().find((asset) => asset.name === 'Draft'));
  protected readonly stamps = this.annotation.watch({ subtype: 'stamp' });
  private started = false;

  constructor() {
    // On load: the standard stamps, and "Approved" in the cover's empty corner, scrolled into view.
    effect(() => {
      if (this.annotation.status() !== 'ready' || this.started) return;
      this.started = true;
      const stage = this.stage();
      void loadDefaultLibrary('en')
        .then((bytes) => this.stamp.importLibrary(bytes))
        .then(({ library }) => {
          const asset = this.stamp
            .listAssets({ libraryId: library.id })
            .find((candidate) => candidate.name === 'Approved');
          if (!asset) return;
          return this.stamp.placeAsset(asset.id, {
            page: 0,
            center: { x: 170, y: 680 },
            targetWidth: 180,
            rotation: -8,
          });
        })
        .then(() => stage.reveal(0, { rect: CORNER }));
    });
  }

  protected approve() {
    const approved = this.approved();
    if (!approved) return;
    const page = this.stage().currentPageIndex();
    void this.stamp.placeAsset(approved.id, { page, center: MIDDLE, select: true });
  }

  protected draftEverywhere() {
    const draft = this.draft();
    if (!draft) return;
    void this.stamp.placeAssetOnPages(draft.id, 'all', {
      center: MIDDLE,
      targetWidth: 320,
      rotation: -30,
    });
  }
}

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

**`programmatic.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --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;
  min-height: 34px;
  box-sizing: border-box;
  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: 7px 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:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

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

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

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

```ts
const { annotation } = await stamp.placeAsset(assetId, {
  page: page.ref,
  center: { x: 120, y: 90 }, // where the stamp's middle goes, in page coordinates
  targetWidth: 160, // optional: the stamp's own size by default
  rotation: 0, // optional: degrees clockwise
});

// The same stamp on several pages, such as "DRAFT" on every page
await stamp.placeAssetOnPages(assetId, 'all', { center: { x: 300, y: 400 } });
```

The box keeps the stamp's proportions and stays on the page. The placed annotation carries the
stamp's identifier as its `name`. A placement from code is selected only when you ask, with
`select: true`, like any [annotation you create](with-code#create-an-annotation).

## Make a stamp from annotations

Select annotations on a page and turn them into a stamp. They become one stamp exactly as they look
on the page, vector and all:

**`from-selection.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
  untracked,
  viewChild,
  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 { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotation, EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfStamp, withStamp, type StampAsset } from '@embedpdf/angular/stamp';
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const assetEngine = engine; // stamp libraries are PDFs; they open here too

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 MY_STAMPS = 'my-stamps';
// An empty corner of the cover, in page coordinates.
const CORNER = { x: 70, y: 600, width: 190, height: 64 };

@Component({
  selector: 'button[demoMyStamp]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    type: 'button',
    class: 'button',
    '[title]': "'Place “' + asset().label + '”'",
    '[attr.aria-pressed]': 'armed()',
    '(click)': 'toggle()',
  },
  template: `
    @if (url(); as url) {
      <img [src]="url" [alt]="asset().label" class="preview" />
    } @else {
      {{ asset().label }}
    }
  `,
})
export class MyStamp {
  readonly asset = input.required<StampAsset>();
  readonly armed = input(false);

  private readonly stamp = inject(EpdfStamp);
  protected readonly url = this.stamp.previewUrlOf(() => this.asset().id);

  protected toggle() {
    if (this.armed()) this.stamp.disarm();
    else void this.stamp.armAsset(this.asset().id);
  }
}

@Component({
  selector: 'demo-make-stamp',
  imports: [MyStamp],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <button type="button" class="button" [disabled]="!canMake()" (click)="make()">
        Make a stamp of the selection
      </button>
      @for (asset of mine(); track asset.id) {
        <button demoMyStamp [asset]="asset" [armed]="armedAsset()?.id === asset.id"></button>
      }
      <span class="spacer"></span>
      <output class="readout">
        {{ armedAsset() ? 'Click a page to place it' : selected().length + ' selected' }}
      </output>
    </div>
  `,
})
export class MakeStamp {
  private readonly stamp = inject(EpdfStamp);
  protected readonly selected = inject(EpdfAnnotation).selected;
  protected readonly mine = this.stamp.assetsOf({ libraryId: MY_STAMPS });
  protected readonly armedAsset = this.stamp.armedAsset;

  // A stamp is one page of artwork: the selection must be on one page.
  private readonly page = computed(() => this.selected()[0]?.page);
  protected readonly canMake = computed(() => {
    const page = this.page();
    const onePage = this.selected().every(
      (annotation) => annotation.page.objectNumber === page?.objectNumber,
    );
    return !!page && onePage && this.stamp.canCreateFromAnnotations();
  });

  protected async make() {
    const page = this.page();
    if (!page) return;
    // The library is made on first use.
    if (!this.stamp.getLibrary(MY_STAMPS)) {
      await this.stamp.createLibrary('My stamps', { id: MY_STAMPS });
    }
    const { asset } = await this.stamp.createAssetFromAnnotations(
      page,
      this.selected().map((annotation) => annotation.ref),
      { libraryId: MY_STAMPS, label: `My stamp ${this.mine().length + 1}` },
    );
    await this.stamp.armAsset(asset.id);
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    MakeStamp,
  ],
  providers: [
    provideEmbedPdf(
      { engine, initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
      withStamp({ assetEngine }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './from-selection.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-make-stamp />
      <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 stage = viewChild(EpdfStage);
  private drawn = false;

  constructor() {
    // On load: a framed text stamp in the cover's empty corner, both parts selected, scrolled
    // into view.
    effect(() => {
      const stage = this.stage();
      if (!stage || this.annotation.status() !== 'ready' || this.drawn) return;
      this.drawn = true;
      untracked(() => this.drawSeal(stage));
    });
  }

  private drawSeal(stage: EpdfStage) {
    void Promise.all([
      this.annotation.create(0, {
        subtype: 'square',
        box: CORNER,
        color: '#c4262e',
        strokeWidth: 4,
      }),
      this.annotation.create(0, {
        subtype: 'free-text',
        intent: 'free-text',
        box: CORNER,
        contents: 'CHECKED',
        fontFamily: 'helvetica-bold',
        fontSize: 30,
        textAlign: 'center',
        verticalAlign: 'middle',
        fontColor: '#c4262e',
        strokeWidth: 0,
      }),
    ]).then((created) => {
      this.annotation.selection.set(created.map((made) => made.annotation.ref));
      stage.reveal(0, { rect: CORNER });
    });
  }
}
```

**`from-selection.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --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;
  min-height: 34px;
  box-sizing: border-box;
  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: 7px 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:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

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

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

/* A stamp's picture, inside its button */
.preview {
  display: block;
  height: 22px;
  max-width: 120px;
}

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

```ts
const { asset } = await stamp.createAssetFromAnnotations(page.ref, refs, {
  libraryId, // omit to create a library named after the label
  label: 'Custom stamp 1',
});
```

It's all or nothing: a hidden annotation, one without an appearance, or one on another page fails
the whole call.

## Keep libraries between sessions

The plugin tells you when a library changes and gives you its bytes; where you store them is up to
you. `persistStampLibraries` and `restoreStampLibraries` connect a library to a store:

**`persist.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  DestroyRef,
  inject,
  input,
  signal,
  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 { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import {
  EpdfStamp,
  indexedDbByteStore,
  persistStampLibraries,
  restoreStampLibraries,
  withStamp,
  type StampAsset,
  type StampLibrary,
} from '@embedpdf/angular/stamp';
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const assetEngine = engine; // stamp libraries are PDFs; they open here too

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

// Where the libraries live between visits: this browser's IndexedDB.
const store = indexedDbByteStore('embedpdf-stamp-example');

@Component({
  selector: 'button[demoStampButton]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    type: 'button',
    class: 'button',
    '[title]': "'Place “' + asset().label + '”'",
    '[attr.aria-pressed]': 'armed()',
    '(click)': 'toggle()',
  },
  template: `
    @if (url(); as url) {
      <img [src]="url" [alt]="asset().label" class="preview" />
    } @else {
      {{ asset().label }}
    }
  `,
})
export class StampButton {
  readonly asset = input.required<StampAsset>();
  readonly armed = input(false);

  private readonly stamp = inject(EpdfStamp);
  protected readonly url = this.stamp.previewUrlOf(() => this.asset().id);

  protected toggle() {
    if (this.armed()) this.stamp.disarm();
    else void this.stamp.armAsset(this.asset().id);
  }
}

@Component({
  selector: 'demo-libraries',
  imports: [StampButton],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      @for (asset of assets(); track asset.id) {
        <button demoStampButton [asset]="asset" [armed]="armedAsset()?.id === asset.id"></button>
      }
      @for (library of libraries(); track library.id) {
        <span class="group">
          <button type="button" class="button" (click)="addStamp(library.id)">Add a stamp</button>
          <button
            type="button"
            class="button"
            title="The library as the PDF it is: open it in Acrobat, or import it again"
            (click)="download(library)"
          >
            Download “{{ library.name }}”
          </button>
        </span>
      }
      <span class="spacer"></span>
      <output class="readout">
        {{
          restored() === null
            ? 'Restoring…'
            : restored() + ' restored: add a stamp, then reload the page'
        }}
      </output>
    </div>
  `,
})
export class Libraries {
  private readonly stamp = inject(EpdfStamp);
  protected readonly libraries = this.stamp.libraries;
  protected readonly assets = this.stamp.assetsOf();
  protected readonly armedAsset = this.stamp.armedAsset;
  protected readonly restored = signal<number | null>(null);

  constructor() {
    // Every change is written to the store from now on.
    inject(DestroyRef).onDestroy(persistStampLibraries(this.stamp, store));

    // On load: what the store kept. The first visit starts a library of its own.
    void restoreStampLibraries(this.stamp, store).then(async (ids) => {
      this.restored.set(ids.length);
      if (ids.length > 0) return;
      const { library } = await this.stamp.createLibrary('My stamps');
      await this.stamp.createAsset({
        libraryId: library.id,
        label: 'Checked',
        mark: { kind: 'text', text: 'Checked', fontFamily: 'times-bold-italic', color: '#1f7a3f' },
      });
    });
  }

  protected addStamp(libraryId: string) {
    const label = `Stamp ${this.assets().length + 1}`;
    void this.stamp.createAsset({
      libraryId,
      label,
      mark: { kind: 'text', text: label, fontFamily: 'helvetica-bold', color: '#054fb3' },
    });
  }

  protected download(library: StampLibrary) {
    void this.stamp
      .exportLibrary(library.id)
      .then((bytes) => saveFile(bytes, `${library.name}.pdf`, 'application/pdf'));
  }
}

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

**`persist.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --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;
  min-height: 34px;
  box-sizing: border-box;
  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: 7px 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:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

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

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

/* A stamp's picture, inside its button */
.preview {
  display: block;
  height: 22px;
  max-width: 120px;
}

/* A library's two buttons sit in the row like the rest */
.group {
  display: contents;
}

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

**`persist.ts`**

```typescript
import { Component, DestroyRef, inject } from '@angular/core';
import {
  EpdfStamp,
  indexedDbByteStore,
  persistStampLibraries,
  restoreStampLibraries,
} from '@embedpdf/angular/stamp';

const store = indexedDbByteStore('stamps');

@Component({
  selector: 'app-stamp-libraries',
  template: `
    <ul>
      @for (library of stamp.libraries(); track library.id) {
        <li>{{ library.name }}</li>
      }
    </ul>
  `,
})
export class StampLibraries {
  protected readonly stamp = inject(EpdfStamp);

  constructor() {
    void restoreStampLibraries(this.stamp, store); // import every stored library
    const stop = persistStampLibraries(this.stamp, store, { except: ['embedpdf-standard'] });
    inject(DestroyRef).onDestroy(stop);
  }
}
```

A store has three calls, `list`, `put` and `delete`, over bytes by library id.
`indexedDbByteStore` is the browser default, and `memoryStampStore` is for tests. Writes are
batched per library, so a burst of edits saves once.

## Dynamic stamps

A stamp can carry form fields, such as the user's name and today's date. When the
[actions plugin](../documents/actions) has JavaScript on, the stamp fills them in when it's placed,
with the user's identity and the current time, and the result is flattened into the placed artwork.
The library itself never changes. `withStamp({ dynamic: false })` keeps stamps as they are.

## Standard stamps in eight languages

`@embedpdf/default-stamps` has the standard set as one library per language (`en`, `de`, `nl`, `fr`,
`es`, `zh-CN`, `sv`, `ja`), each with the same identifiers and translated labels. It loads from your
own build, so nothing is fetched from anywhere else. `negotiateLocale`, from `@embedpdf/angular/i18n`,
picks the one closest to the reader's language:

```ts
import { LOCALES, loadDefaultLibrary } from '@embedpdf/default-stamps/library';

const locale = negotiateLocale(LOCALES, navigator.languages) ?? 'en';
await stamp.importLibrary(await loadDefaultLibrary(locale));
```

Pick a language: the same stamps come back with their labels translated.

**`languages.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
  signal,
  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 { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfStamp, withStamp, type StampAsset } from '@embedpdf/angular/stamp';
import { LOCALES, loadDefaultLibrary } from '@embedpdf/default-stamps/library';
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const assetEngine = engine; // stamp libraries are PDFs; they open here too

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: 'button[demoStampButton]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    type: 'button',
    class: 'button',
    '[title]': 'asset().label',
    '[attr.aria-pressed]': 'armed()',
    '(click)': 'toggle()',
  },
  template: `
    @if (url(); as url) {
      <img [src]="url" [alt]="asset().label" class="preview" />
    } @else {
      {{ asset().label }}
    }
  `,
})
export class StampButton {
  readonly asset = input.required<StampAsset>();
  readonly armed = input(false);

  private readonly stamp = inject(EpdfStamp);
  protected readonly url = this.stamp.previewUrlOf(() => this.asset().id);

  protected toggle() {
    if (this.armed()) this.stamp.disarm();
    else void this.stamp.armAsset(this.asset().id);
  }
}

@Component({
  selector: 'demo-language-picker',
  imports: [StampButton],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <select #picker class="field" aria-label="Language" (change)="locale.set(picker.value)">
        @for (code of locales; track code) {
          <option [value]="code" [selected]="code === locale()">{{ code }}</option>
        }
      </select>
      @for (asset of shown(); track asset.id) {
        <button demoStampButton [asset]="asset" [armed]="armedAsset()?.id === asset.id"></button>
      }
      <span class="spacer"></span>
      <output class="readout">{{ library()?.name ?? 'Loading…' }}</output>
    </div>
  `,
})
export class LanguagePicker {
  private readonly stamp = inject(EpdfStamp);
  protected readonly locales = LOCALES;
  protected readonly locale = signal('nl');
  protected readonly library = computed(() => this.stamp.libraries()[0]);
  private readonly assets = this.stamp.assetsOf(() => ({ libraryId: this.library()?.id }));
  protected readonly shown = computed(() => this.assets().slice(0, 4));
  protected readonly armedAsset = this.stamp.armedAsset;

  constructor() {
    // The library of the chosen language replaces the one before: the same identifiers,
    // translated labels. Dutch on load.
    effect((onCleanup) => {
      const locale = this.locale();
      let current = true;
      onCleanup(() => (current = false));
      void loadDefaultLibrary(locale).then(async (bytes) => {
        if (!current) return;
        for (const old of this.stamp.listLibraries()) await this.stamp.deleteLibrary(old.id);
        if (current) await this.stamp.importLibrary(bytes);
      });
    });
  }
}

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

**`languages.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --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;
  min-height: 34px;
  box-sizing: border-box;
  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: 7px 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:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

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

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

/* A stamp's picture, inside its button */
.preview {
  display: block;
  height: 22px;
  max-width: 120px;
}

/* A text box or a menu in the toolbar */
.field {
  appearance: none;
  min-width: 0;
  box-sizing: border-box;
  min-height: 34px;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

select.field {
  appearance: auto;
  padding-right: 6px;
  cursor: pointer;
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
}

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

## Methods

Everything `inject(EpdfStamp)` gives you.

| Method                                                                    | What it does                                                                                                                                                                                                    |
| :------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `listLibraries({ kind? })`                                                | Every library, or the ones of the kinds you ask for. In a component, use `stamp.libraries()`. [Library kinds](#library-kinds)                                                                                   |
| `getLibrary(libraryId)`                                                   | One library, or `null`                                                                                                                                                                                          |
| `listAssets({ libraryId? })`                                              | Every stamp, or the ones in one library, in the order a picker shows them. In a component, use `stamp.assetsOf({ libraryId })`. [Show a stamp picker](#show-a-stamp-picker)                                     |
| `getAsset(assetId)`                                                       | One stamp, or `null`                                                                                                                                                                                            |
| `getAssetPreview(assetId)`, `renderAssetPreview(assetId, { width })`      | A stamp's picture as `{ bytes, mimeType }`, or `null`: the thumbnail at the `previewWidth` setting, or one drawn at any width. In a component, `stamp.previewUrlOf(assetId)` gives an image URL.                |
| `readAssetBytes(assetId)`                                                 | A copy of the stamp's artwork as a one-page PDF, or `null`                                                                                                                                                      |
| `createLibrary(name, { kind? })`                                          | Creates an empty library. Resolves `{ library }`. [Library kinds](#library-kinds)                                                                                                                               |
| `updateLibrary(libraryId, changes)`                                       | Renames a library, or changes its categories. Resolves `{ library }`.                                                                                                                                           |
| `deleteLibrary(libraryId)`                                                | Removes a library and every stamp in it                                                                                                                                                                         |
| `importLibrary(bytes, { libraryKind? })`                                  | Makes a library from a PDF: an Acrobat stamp file as it is, a plain PDF with one stamp per page. Resolves `{ library }`. [A library is a PDF](#a-library-is-a-pdf)                                              |
| `exportLibrary(libraryId)`                                                | The library as the PDF it is, to store or to open in Acrobat                                                                                                                                                    |
| `createAsset({ libraryId?, label, source })`                              | Adds a stamp from a one-page PDF, a PNG or a JPEG, or from a `mark` instead of `source`. Without `libraryId`, it gets a library of its own. Resolves `{ asset }`. [Draw or type a stamp](#draw-or-type-a-stamp) |
| `createAssetFromAnnotations(page, refs, { libraryId?, label })`           | Turns annotations on a page into one stamp, exactly as they look. Resolves `{ asset }`. [Make a stamp from annotations](#make-a-stamp-from-annotations)                                                         |
| `updateAsset(assetId, changes)`                                           | Changes a stamp's label, subject or categories; its identifier never changes. Resolves `{ asset }`.                                                                                                             |
| `deleteAsset(assetId)`                                                    | Removes a stamp from its library                                                                                                                                                                                |
| `moveAsset(assetId, { libraryId })`                                       | Moves a stamp to another library. Resolves `{ asset }`, which has a new id there.                                                                                                                               |
| `duplicateAsset(assetId, { label? })`                                     | Copies a stamp within its library. Resolves `{ asset }`.                                                                                                                                                        |
| `armAsset(assetId, { targetWidth? })`                                     | Arms a stamp: the next click on a page places it, with a preview under the pointer. [Show a stamp picker](#show-a-stamp-picker)                                                                                 |
| `disarm()`                                                                | Disarms the stamp without placing it                                                                                                                                                                            |
| `placeAsset(assetId, { page, center, targetWidth?, rotation?, select? })` | Places a stamp without the pointer, its middle on `center`. Resolves `{ annotation }`. [Place a stamp with code](#place-a-stamp-with-code)                                                                      |
| `placeAssetOnPages(assetId, pages, { center, … })`                        | Places the same stamp at the same spot on several pages, or on `'all'`. Resolves `{ applied, skipped, failed }`: the placed stamps, and the pages that were skipped or failed.                                  |
| `canPlace()`, `canCreateFromAnnotations()`                                | Whether placing a stamp, and making one from annotations, are allowed. [Permissions](#permissions)                                                                                                              |
| `canImport()`                                                             | Whether libraries can be imported here. It turns `false` once an import finds no local engine to open them in; see [Settings](#settings).                                                                       |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()`             | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `inject(EpdfStamp).settings()`. [Settings](#settings)                                             |

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

## State

Every value is a signal on `inject(EpdfStamp)`: 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         |
| :------------- | :---------------------------------------------------------- | :---------------- |
| `armedAsset()` | The stamp the next click on this document places, or `null` | `getArmedAsset()` |

The libraries and their stamps come from `stamp.libraries()` and `stamp.assetsOf({ libraryId })`.

## Settings

Set them when you register the plugin, and change them while the app runs with
`stamp.updateSettings({ … })`; `stamp.resetSettings()` goes back to what you registered, and
`inject(EpdfStamp).settings()` gives you the current settings.

| Setting        | What it does                               | Default                     |
| :------------- | :----------------------------------------- | :-------------------------- |
| `assetEngine`  | The engine stamp libraries open in         | the viewer's engine (local) |
| `previewWidth` | The width of stamp previews, in pixels     | `256`                       |
| `dynamic`      | Fill in dynamic stamps when they're placed | `true`                      |

## Events

**`events.ts`**

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

@Component({
  selector: 'app-library-status',
  template: `<p aria-live="polite">{{ message() }}</p>`,
})
export class LibraryStatus {
  private readonly stamp = inject(EpdfStamp);

  protected readonly message = toSignal(
    this.stamp.libraryChanged$.pipe(map(({ libraryId, reason }) => `${libraryId}: ${reason}`)),
    { initialValue: '' },
  );
}
```

| Event                                                   | Fires when                                           | Carries                         |
| :------------------------------------------------------ | :--------------------------------------------------- | :------------------------------ |
| `libraryChanged$`                                       | Anything in a library changed: the one to save it on | `libraryId`, `reason`           |
| `libraryCreated$`, `libraryUpdated$`, `libraryDeleted$` | A library was added, changed or removed              | `libraryId`, `library`          |
| `assetCreated$`, `assetUpdated$`, `assetDeleted$`       | A stamp was added, changed or removed                | `assetId`, `libraryId`, `asset` |
| `armChanged$`                                           | A stamp was armed or disarmed                        | `documentId`, `asset`           |
| `settingsChanged$`                                      | The settings changed                                 | `settings`, `changed`           |

`reason` is `'created'`, `'imported'`, `'updated'`, `'asset-added'`, `'asset-updated'`,
`'asset-removed'` or `'removed'`. `library` and `asset` are the new values, and `null` in the
deleted events; in the arm event, `asset` is `null` when the stamp was disarmed.

## Permissions

| Permission           | What it allows                                         | Check                              |
| :------------------- | :----------------------------------------------------- | :--------------------------------- |
| `annotations:create` | Placing a stamp                                        | `stamp.canPlace()`                 |
| `doc.download`       | Making a stamp from annotations, which copies them out | `stamp.canCreateFromAnnotations()` |

Libraries live in the browser and touch no document, so creating and importing them needs no
document permission.

## Good to know

### The identifier

A stamp made here gets an identifier in Acrobat's own form, so it keeps its identity when its library
is opened in Acrobat.

### Stamps in the engine

A placed stamp is a normal stamp annotation. To place one without the plugin, or copy one, see
[Stamps](https://www.embedpdf.com/docs/engine/annotations/stamps).
