EmbedPDF

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

Loading live preview…

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 {}

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#

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, and resolves with { marks }, one per match.

Loading live preview…

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 {}

Review the marks#

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

pending.ts
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.

Loading live preview…

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

Apply#

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:

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.

Loading live preview…

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 {}

What shows in place of the content#

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

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.

Loading live preview…

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 {}

Methods#

Everything inject(EpdfRedaction) gives you.

MethodWhat it does
markSelection()Marks the selected text, one mark per page, and clears the selection. Resolves { marks }. 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, or the ones on some pages. It replaces the current search. Resolves { marks }.
unmark(refs)Removes marks without applying them. 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
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
applyAll()Applies every mark in the document, on pages that haven’t loaded too. Can’t be undone. 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
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

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.

FieldWhat it isRead once
pendingCount()How many marks are waiting to be appliedgetPendingCount(), or getPendingCount(page) for one page
applying()Whether marks are being applied right nowisApplying()
lastResult()The result of the last apply, from this session or another, or nullgetLastResult()

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.

SettingWhat it doesDefault
overlay.fillThe color of a redacted area'#000000'
overlay.text.colorThe color of a label'#ffffff'
overlay.text.fontFamilyThe label’s fontHelvetica
overlay.text.fontSizeThe label’s size, or 0 to fit the area (a repeated label then draws at 12pt)0

Events#

events.ts
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: '' },
  );
}
EventFires whenCarries
applied$Marks were applied and the content is goneresult, origin
pendingChanged$A mark was added, changed or removedpages
settingsChanged$The settings changedsettings, changed

Permissions#

PermissionWhat it allowsCheck
annotations:createMarkingredaction.canMark()
annotations:deleteRemoving a mark before it’s appliedredaction.canUnmark(ref)
annotations:updateChanging a mark’s labelredaction.canUpdateLabel(ref)
doc.redactApplyingredaction.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 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.

Was this page helpful?

Your feedback goes directly to the documentation team.