---
title: "Redaction — Angular"
description: "Mark text and areas for removal, review the marks, then remove the content for good."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/annotations/redaction"
---

# Redaction

Redaction removes content for good: the text, images and drawings under a mark are deleted from the
file, not just covered up. It happens in two steps, as in Acrobat: first people **mark** what should
go, and review it; then they **apply** the marks, and the content is gone.

Register `withRedaction()` next to `withInteraction()` and `withAnnotation()`, and use the
`redact` tool: select text to mark it, or drag to mark an area:

**`bar.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { EpdfInteraction } from '@embedpdf/angular/interaction';
import { EpdfRedaction } from '@embedpdf/angular/redaction';

@Component({
  selector: 'app-redact-bar',
  template: `
    <button (click)="interaction.activateTool('redact')">Mark for redaction</button>
    <button
      [disabled]="!redaction.pendingCount() || redaction.applying()"
      (click)="redaction.applyAll()"
    >
      Redact {{ redaction.pendingCount() }} marks
    </button>
  `,
})
export class RedactBar {
  protected readonly interaction = inject(EpdfInteraction);
  protected readonly redaction = inject(EpdfRedaction);
}
```

**`basic.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 { EpdfInteraction, withInteraction } from '@embedpdf/angular/interaction';
import { EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { EpdfAnnotation, EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfRedaction, withRedaction } from '@embedpdf/angular/redaction';
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()) };
};

// The author's name on the cover, in page coordinates.
const AUTHOR = { x: 100, y: 508, width: 172, height: 50 };

/** Next to `<epdf-stage #stage="epdfStage">`: `<demo-redact-bar [stage]="stage" />`. */
@Component({
  selector: 'demo-redact-bar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <button
        type="button"
        class="button"
        [attr.aria-pressed]="marking()"
        (click)="interaction.activateTool(marking() ? 'pointer' : 'redact')"
      >
        Mark for redaction
      </button>
      <button
        type="button"
        class="button danger"
        [disabled]="!redaction.pendingCount() || redaction.applying()"
        (click)="redaction.applyAll()"
      >
        Redact {{ redaction.pendingCount() }}
        {{ redaction.pendingCount() === 1 ? 'mark' : 'marks' }}
      </button>
      <span class="spacer"></span>
      <output class="readout">
        {{ marking() ? 'Select text, or drag over an area' : 'Click a mark to move it' }}
      </output>
    </div>
  `,
})
export class RedactBar {
  readonly stage = input.required<EpdfStage>();

  protected readonly interaction = inject(EpdfInteraction);
  protected readonly redaction = inject(EpdfRedaction);
  private readonly annotation = inject(EpdfAnnotation);
  protected readonly marking = computed(() => this.interaction.activeToolId() === 'redact');
  private started = false;

  constructor() {
    // On load: the author's name marked, and the redact tool on.
    effect(() => {
      if (this.annotation.status() !== 'ready' || this.started) return;
      this.started = true;
      const stage = this.stage();
      void this.redaction.markArea(0, AUTHOR).then(() => stage.reveal(0, { rect: AUTHOR }));
      this.interaction.activateTool('redact');
    });
  }
}

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

**`basic.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --danger: #c4262e;
  --danger-strong: #a81f26;
  --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 one button that can't be undone */
.button.danger {
  color: var(--surface);
  background: var(--danger);
  border-color: var(--danger);
}

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

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

A mark is an annotation, so until it's applied, people can move it, resize it, delete it, and see it
in other PDF apps. Applying it is what removes the content.

## Mark from code

```ts
await redaction.markSelection(); // the selected text
await redaction.markArea(page.ref, { x: 72, y: 100, width: 200, height: 40 });
await redaction.markPage(page.ref); // the whole page
await redaction.markMatches({ text: 'Jane Doe' }); // every match in the document
await redaction.markMatches({ text: '\\d{3}-\\d{2}-\\d{4}', regex: true }); // like a social security number
```

`markMatches` takes the same query as [search](../text/search), and resolves with `{ marks }`, one per
match.

**`from-code.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 { EpdfSelection, EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { withSearch } from '@embedpdf/angular/search';
import { EpdfAnnotation, EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfRedaction, withRedaction } from '@embedpdf/angular/redaction';
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()) };
};

/** Next to `<epdf-stage #stage="epdfStage">`: `<demo-mark-from-code [stage]="stage" />`. */
@Component({
  selector: 'demo-mark-from-code',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <form class="query" (submit)="$event.preventDefault(); markMatches()">
        <input
          #field
          class="field"
          type="search"
          aria-label="Text to mark"
          [value]="text()"
          (input)="text.set(field.value)"
        />
        <button type="submit" class="button" [disabled]="!text().trim()">Every match</button>
      </form>
      <button
        type="button"
        class="button"
        [disabled]="!selection.hasSelection()"
        title="Select some text on the page first"
        (click)="redaction.markSelection()"
      >
        The selected text
      </button>
      <button type="button" class="button" (click)="markThisPage()">This page</button>
      <button
        type="button"
        class="button"
        [disabled]="!redaction.pendingCount()"
        (click)="redaction.clearPending()"
      >
        Remove all
      </button>
      <span class="spacer"></span>
      <output class="readout">{{ marks() }}</output>
    </div>
  `,
})
export class MarkFromCode {
  readonly stage = input.required<EpdfStage>();

  protected readonly redaction = inject(EpdfRedaction);
  protected readonly selection = inject(EpdfSelection);
  private readonly annotation = inject(EpdfAnnotation);
  protected readonly text = signal('EmbedPDF');
  protected readonly marks = computed(() => {
    const count = this.redaction.pendingCount();
    return `${count} ${count === 1 ? 'mark' : 'marks'}`;
  });
  private started = false;

  constructor() {
    // On load: every "EmbedPDF" in the document.
    effect(() => {
      if (this.annotation.status() !== 'ready' || this.started) return;
      this.started = true;
      void this.redaction.markMatches({ text: 'EmbedPDF' });
    });
  }

  protected markMatches() {
    const text = this.text().trim();
    if (text) void this.redaction.markMatches({ text });
  }

  protected markThisPage() {
    void this.redaction.markPage(this.stage().currentPageIndex());
  }
}

// Marking matches runs a search; marking the selected text needs a selection.
@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    EpdfAnnotationLayer,
    MarkFromCode,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
      withSearch(),
      withAnnotation(),
      withRedaction(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './from-code.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-mark-from-code [stage]="stage" />
      <epdf-stage #stage="epdfStage" 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-code.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 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 text to mark and its button, together */
.query {
  display: flex;
  flex: 0 1 280px;
  gap: 8px;
  min-width: 0;
}

.query .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);
}
```

## Review the marks

`redaction.pending()` lists the marks that haven't been applied yet, for a review panel:

**`pending.ts`**

```typescript
import { Component, inject, input } from '@angular/core';
import { annotationKey } from '@embedpdf/angular/annotation';
import { EpdfRedaction } from '@embedpdf/angular/redaction';
import type { EpdfStage } from '@embedpdf/angular/stage';

/** Next to `<epdf-stage #stage="epdfStage">`: `<app-pending-redactions [stage]="stage" />`. */
@Component({
  selector: 'app-pending-redactions',
  template: `
    <ul>
      @for (mark of redaction.pending(); track annotationKey(mark.ref)) {
        <li>
          <button (click)="stage().reveal(mark.page, { rect: mark.bounds })">
            Page {{ mark.pageIndex + 1 }}: {{ mark.kind === 'text' ? 'text' : 'area' }}
          </button>
          <button (click)="redaction.unmark([mark.ref])">Remove</button>
        </li>
      }
    </ul>
  `,
})
export class PendingRedactions {
  readonly stage = input.required<EpdfStage>();

  protected readonly redaction = inject(EpdfRedaction);
  protected readonly annotationKey = annotationKey;
}
```

`redaction.clearPending()` removes every mark. `redaction.pendingOn(page.ref)` lists one page's.

**`review.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 { withSearch } from '@embedpdf/angular/search';
import {
  annotationKey,
  EpdfAnnotation,
  EpdfAnnotationLayer,
  withAnnotation,
} from '@embedpdf/angular/annotation';
import { EpdfRedaction, withRedaction } from '@embedpdf/angular/redaction';
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()) };
};

/** Next to `<epdf-stage #stage="epdfStage">`: `<div demoPendingMarks [stage]="stage"></div>`. */
@Component({
  selector: 'div[demoPendingMarks]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'panel' },
  template: `
    <div class="panel-head">
      <span class="readout">{{ count() }}</span>
      <button
        type="button"
        class="button"
        [disabled]="!redaction.pendingCount()"
        (click)="redaction.clearPending()"
      >
        Remove all
      </button>
    </div>
    <ul class="marks">
      @for (mark of redaction.pending(); track key(mark.ref)) {
        <li class="mark">
          <button
            type="button"
            class="mark-go"
            (click)="stage().reveal(mark.page, { rect: mark.bounds })"
          >
            <span class="mark-page">Page {{ mark.pageIndex + 1 }}</span>
            <span class="mark-kind">{{ mark.kind === 'text' ? 'Text' : 'Area' }}</span>
          </button>
          <button
            type="button"
            class="button"
            [disabled]="!redaction.canUnmark(mark.ref)"
            (click)="redaction.unmark([mark.ref])"
          >
            Remove
          </button>
        </li>
      }
    </ul>
  `,
})
export class PendingMarks {
  readonly stage = input.required<EpdfStage>();

  // pending(): the marks not applied yet, in page order.
  protected readonly redaction = inject(EpdfRedaction);
  protected readonly key = annotationKey;
  protected readonly count = computed(() => {
    const count = this.redaction.pendingCount();
    return `${count} ${count === 1 ? 'mark' : 'marks'}`;
  });
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    PendingMarks,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSearch(),
      withAnnotation(),
      withRedaction(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './review.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 demoPendingMarks [stage]="stage"></div>
      </div>
    </ng-container>

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

  constructor() {
    // On load: the author's name, and every "commercial" in the document.
    effect(() => {
      if (this.annotation.status() !== 'ready' || this.started) return;
      this.started = true;
      void this.redaction
        .markArea(0, { x: 100, y: 508, width: 172, height: 50 })
        .then(() => this.redaction.markMatches({ text: 'commercial' }));
    });
  }
}
```

**`review.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 and the marks: stacked, and side by side where there's room */
.viewer {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.viewer .stage {
  flex: 1 1 100%;
  box-sizing: border-box;
  height: 340px;
}

.panel {
  flex: 1 1 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  max-height: 260px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 1px 2px var(--shadow);
}

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

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

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

/* One mark: where it is, and a way to remove it */
.marks {
  flex: 1 1 auto;
  overflow-y: auto;
  margin: 0;
  padding: 6px;
  list-style: none;
}

.mark {
  display: flex;
  align-items: center;
  gap: 6px;
}

.mark-go {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  font: inherit;
  text-align: left;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 9px;
  padding: 8px 10px;
  cursor: pointer;
}

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

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

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

.mark-kind {
  font-size: 13px;
  font-weight: 550;
}

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

## Apply

```ts
const result = await redaction.applyAll(); // every mark
await redaction.apply([mark.ref]); // some marks
await redaction.applyPages([page.ref]); // the marks on some pages
```

This can't be undone. Ask first, and say what else goes with it: annotations a mark overlaps are
removed too, because they could show what was there. `estimateCollateral()` tells you how many:

```ts
const { count } = redaction.estimateCollateral();
if (!(await confirm(`Redact? ${count} annotations under the marks will be removed too.`))) return;
await redaction.applyAll();
```

After applying, download the document with `{ mode: 'rewrite' }`, so the removed content isn't left in
an earlier revision of the file. See [Saving & downloading](../documents/saving).

**`apply.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import {
  EpdfDocumentGate,
  EpdfDocuments,
  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 { EpdfAnnotation, EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfRedaction, withRedaction } from '@embedpdf/angular/redaction';
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()) };
};

// The cover's title, in page coordinates: the shapes drawn over it go with it.
const TITLE = { x: 100, y: 212, width: 392, height: 156 };

@Component({
  selector: 'demo-apply-bar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (asking()) {
      <div class="toolbar confirm" role="alertdialog" aria-label="Redact for good?">
        <span class="readout">{{ question() }}</span>
        <span class="spacer"></span>
        <button type="button" class="button" (click)="asking.set(false)">Cancel</button>
        <button
          type="button"
          class="button danger"
          [disabled]="redaction.applying()"
          (click)="redactForGood()"
        >
          Redact for good
        </button>
      </div>
    } @else {
      <div class="toolbar">
        <button
          type="button"
          class="button danger"
          [disabled]="!redaction.pendingCount() || !redaction.canApply()"
          (click)="ask()"
        >
          Redact…
        </button>
        <button
          type="button"
          class="button"
          [disabled]="!redaction.lastResult()"
          title="A fresh file, without the earlier revision that still holds the content"
          (click)="download()"
        >
          Download
        </button>
        <span class="spacer"></span>
        <output class="readout">
          @if (redaction.lastResult(); as result) {
            Gone for good, with {{ result.removedAnnotationCount }} other annotations
          } @else {
            {{ marks() }} waiting
          }
        </output>
      </div>
    }
  `,
})
export class ApplyBar {
  protected readonly redaction = inject(EpdfRedaction);
  private readonly documents = inject(EpdfDocuments);
  private readonly annotation = inject(EpdfAnnotation);
  protected readonly asking = signal(false);
  protected readonly collateral = signal(0);
  protected readonly marks = computed(() => {
    const count = this.redaction.pendingCount();
    return `${count} ${count === 1 ? 'mark' : 'marks'}`;
  });
  protected readonly question = computed(() => {
    const count = this.collateral();
    const others =
      count > 0 ? `${count} ${count === 1 ? 'annotation' : 'annotations'} under them go too. ` : '';
    return `Redact ${this.marks()}? ${others}This can’t be undone.`;
  });
  private started = false;

  constructor() {
    // On load: the title marked.
    effect(() => {
      if (this.annotation.status() !== 'ready' || this.started) return;
      this.started = true;
      void this.redaction.markArea(0, TITLE);
    });
  }

  // The other annotations applying removes too: they could show what was there.
  protected ask() {
    this.collateral.set(this.redaction.estimateCollateral().count);
    this.asking.set(true);
  }

  protected redactForGood() {
    void this.redaction.applyAll().finally(() => this.asking.set(false));
  }

  protected download() {
    void this.documents
      .download(undefined, { mode: 'rewrite' })
      .then((bytes) => saveFile(bytes, 'redacted.pdf', 'application/pdf'));
  }
}

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

**`apply.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --danger: #c4262e;
  --danger-strong: #a81f26;
  --danger-tint: #fdeced;
  --danger-line: #f3c4c7;
  --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 one button that can't be undone */
.button.danger {
  color: var(--surface);
  background: var(--danger);
  border-color: var(--danger);
}

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

/* Asking before what can't be undone */
.confirm {
  padding: 8px 8px 8px 14px;
  background: var(--danger-tint);
  border: 1px solid var(--danger-line);
  border-radius: 12px;
}

.confirm .readout {
  flex: 1 1 260px;
  font-weight: 550;
  line-height: 1.4;
}

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

## What shows in place of the content

By default a redacted area is black. Give it a label, such as "Classified", on a mark:

```ts
await redaction.updateLabel(mark.ref, { overlayText: 'Classified', repeat: true });
```

`repeat` fills the area with the label over and over; otherwise it shows once. The color and font of
every mark are settings.

**`label.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 { EpdfAnnotation, EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfRedaction, withRedaction } from '@embedpdf/angular/redaction';
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()) };
};

// The cover's subtitle, in page coordinates.
const SUBTITLE = { x: 100, y: 378, width: 352, height: 114 };

/** Next to `<epdf-stage #stage="epdfStage">`: `<demo-label-bar [stage]="stage" />`. */
@Component({
  selector: 'demo-label-bar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <form class="label-form" (submit)="$event.preventDefault(); setLabel()">
        <input
          #field
          class="field"
          aria-label="Label"
          [value]="text()"
          [disabled]="!mark()"
          (input)="text.set(field.value)"
        />
        <button type="submit" class="button" [disabled]="!mark()">Set label</button>
      </form>
      <label class="label">
        <input
          #repeat
          type="checkbox"
          [checked]="mark()?.repeat ?? false"
          [disabled]="!mark()"
          (change)="setRepeat(repeat.checked)"
        />
        Repeat
      </label>
      <button
        type="button"
        class="button danger"
        [disabled]="!mark() || redaction.applying()"
        (click)="redaction.applyAll()"
      >
        Redact
      </button>
      <span class="spacer"></span>
      <output class="readout">
        {{
          redaction.lastResult() ? 'The label is part of the page now' : 'Redact to see the label'
        }}
      </output>
    </div>
  `,
})
export class LabelBar {
  readonly stage = input.required<EpdfStage>();

  protected readonly redaction = inject(EpdfRedaction);
  private readonly annotation = inject(EpdfAnnotation);
  protected readonly mark = computed(() => this.redaction.pending()[0]);
  protected readonly text = signal('Classified');
  private started = false;

  constructor() {
    // On load: the subtitle marked, with a label that fills the area.
    effect(() => {
      if (this.annotation.status() !== 'ready' || this.started) return;
      this.started = true;
      const stage = this.stage();
      void this.redaction
        .markArea(0, SUBTITLE)
        .then(({ mark }) =>
          this.redaction.updateLabel(mark.ref, { overlayText: 'Classified', repeat: true }),
        )
        .then(() => stage.reveal(0, { rect: SUBTITLE }));
    });
  }

  protected setLabel() {
    const mark = this.mark();
    if (mark)
      void this.redaction.updateLabel(mark.ref, { overlayText: this.text().trim() || null });
  }

  protected setRepeat(repeat: boolean) {
    const mark = this.mark();
    if (mark) void this.redaction.updateLabel(mark.ref, { repeat });
  }
}

// What applying paints over every mark: a dark blue area, its label in white.
@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    LabelBar,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
      withRedaction({ overlay: { fill: '#1a2748', text: { color: '#ffffff' } } }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './label.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-label-bar [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 {}
```

**`label.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --danger: #c4262e;
  --danger-strong: #a81f26;
  --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 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 one button that can't be undone */
.button.danger {
  color: var(--surface);
  background: var(--danger);
  border-color: var(--danger);
}

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

/* The label and its button, together */
.label-form {
  display: flex;
  flex: 0 1 260px;
  gap: 8px;
  min-width: 0;
}

.label-form .field {
  flex: 1 1 auto;
  width: 100%;
}

.label input {
  accent-color: var(--accent);
}

/* 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(EpdfRedaction)` gives you.

| Method                                                             | What it does                                                                                                                                                                 |
| :----------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `markSelection()`                                                  | Marks the selected text, one mark per page, and clears the selection. Resolves `{ marks }`. [Mark from code](#mark-from-code)                                                |
| `markArea(page, rect)`, `markPage(page)`                           | Marks an area of a page, or the whole page. Resolves `{ mark }`.                                                                                                             |
| `markMatches(query, { pages? })`                                   | Marks every match of a [search](../text/search), or the ones on some pages. It replaces the current search. Resolves `{ marks }`.                                            |
| `unmark(refs)`                                                     | Removes marks without applying them. [Review the marks](#review-the-marks)                                                                                                   |
| `clearPending()`                                                   | Removes every mark                                                                                                                                                           |
| `updateLabel(ref, { overlayText, repeat? })`                       | Sets the label a mark shows once it's applied; `overlayText: null` clears it. Resolves `{ mark }`. [What shows in place of the content](#what-shows-in-place-of-the-content) |
| `listPending({ page? })`                                           | The marks not applied yet, in page order, or one page's. In a component, use `redaction.pending()`.                                                                          |
| `getPending(ref)`                                                  | One mark, or `null` when it isn't a mark waiting to be applied                                                                                                               |
| `estimateCollateral(refs?)`                                        | The other annotations applying would remove too, as `{ count, refs }`, for every mark or for the ones you pass. [Apply](#apply)                                              |
| `applyAll()`                                                       | Applies every mark in the document, on pages that haven't loaded too. Can't be undone. [Apply](#apply)                                                                       |
| `apply(refs)`, `applyPages(pages)`                                 | Applies some marks, or the marks on some pages. Can't be undone.                                                                                                             |
| `canMark()`, `canUnmark(ref)`, `canUpdateLabel(ref)`, `canApply()` | Whether marking, removing a mark, changing its label and applying are allowed. [Permissions](#permissions)                                                                   |
| `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(EpdfRedaction).settings()`. [Settings](#settings)      |

Applying resolves the result: each page's `status` (`'applied'` or `'unchanged'`) and how many
other annotations were removed. Applying is all or nothing: when a page fails, no page changes and
the promise rejects. Every method that returns a promise also
takes a `signal` in its options, to cancel it.

## State

Every value is a signal on `inject(EpdfRedaction)`: 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                                                    |
| :--------------- | :-------------------------------------------------------------------- | :----------------------------------------------------------- |
| `pendingCount()` | How many marks are waiting to be applied                              | `getPendingCount()`, or `getPendingCount(page)` for one page |
| `applying()`     | Whether marks are being applied right now                             | `isApplying()`                                               |
| `lastResult()`   | The result of the last apply, from this session or another, or `null` | `getLastResult()`                                            |

The marks themselves come from `redaction.pending()`.

## Settings

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

| Setting                   | What it does                                                                   | Default     |
| :------------------------ | :----------------------------------------------------------------------------- | :---------- |
| `overlay.fill`            | The color of a redacted area                                                   | `'#000000'` |
| `overlay.text.color`      | The color of a label                                                           | `'#ffffff'` |
| `overlay.text.fontFamily` | The label's font                                                               | Helvetica   |
| `overlay.text.fontSize`   | The label's size, or `0` to fit the area (a repeated label then draws at 12pt) | `0`         |

## Events

**`events.ts`**

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

@Component({
  selector: 'app-redaction-notice',
  template: `<p aria-live="polite">{{ message() }}</p>`,
})
export class RedactionNotice {
  private readonly redaction = inject(EpdfRedaction);

  protected readonly message = toSignal(
    this.redaction.applied$.pipe(map(() => 'Redacted: the content under the marks is gone')),
    { initialValue: '' },
  );
}
```

| Event              | Fires when                                 | Carries               |
| :----------------- | :----------------------------------------- | :-------------------- |
| `applied$`         | Marks were applied and the content is gone | `result`, `origin`    |
| `pendingChanged$`  | A mark was added, changed or removed       | `pages`               |
| `settingsChanged$` | The settings changed                       | `settings`, `changed` |

## Permissions

| Permission           | What it allows                      | Check                           |
| :------------------- | :---------------------------------- | :------------------------------ |
| `annotations:create` | Marking                             | `redaction.canMark()`           |
| `annotations:delete` | Removing a mark before it's applied | `redaction.canUnmark(ref)`      |
| `annotations:update` | Changing a mark's label             | `redaction.canUpdateLabel(ref)` |
| `doc.redact`         | Applying                            | `redaction.canApply()`          |

A mark is an annotation, so removing it or changing its label follows the same rules as any
annotation, per mark. Applying also rewrites the pages and removes annotations, so `canApply()` needs
`doc.pages.modify` and `doc.annotate.modify` as well. A reviewer who may mark but not apply can
prepare redactions for someone else to approve.

## Good to know

### What is removed

The characters under a mark are removed, so they can't be selected, searched or copied; the pixels
of images inside it are erased; and the parts of lines, shapes and fills inside it are cut away. See
[Redaction](https://www.embedpdf.com/docs/engine/documents/redaction) for the details.

### Where the original is

The local engine redacts a copy of the file the browser already has, so the original was on the
user's device before it was redacted.

> **Redact before anyone receives the file**
>
> With CloudPDF, redaction happens on your server. People who may only see the redacted version never
> receive the original: not the file, not its text, not its images.
>
> [Redact with CloudPDF](https://www.cloudpdf.com/docs/headless/angular/annotations/redaction)
