---
title: "Redaction — Svelte"
description: "Mark text and areas for removal, review the marks, then remove the content for good."
framework: "Svelte"
source: "https://www.embedpdf.com/docs/headless/svelte/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 `redactionPlugin()` next to `interactionPlugin()` and `annotationPlugin()`, and use the
`redact` tool: select text to mark it, or drag to mark an area:

**`bar.svelte`**

```svelte
<script lang="ts">
  import { useInteraction } from '@embedpdf/svelte/interaction';
  import { useRedaction, useRedactionState } from '@embedpdf/svelte/redaction';

  const interaction = useInteraction();
  const redaction = useRedaction();
  const state = useRedactionState();
</script>

<button onclick={() => interaction.activateTool('redact')}>Mark for redaction</button>
<button disabled={!state.pendingCount || state.applying} onclick={() => redaction.applyAll()}>
  Redact {state.pendingCount} marks
</button>
```

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { SelectionLayer, selectionPlugin } from '@embedpdf/svelte/selection';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { redactionPlugin } from '@embedpdf/svelte/redaction';
  import { localEngine } from '@embedpdf/engine';
  import RedactBar from './RedactBar.svelte';

  import '../basic.css';

  const engine = localEngine();
  // The selection plugin lets the redact tool mark the text you select.
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin(),
    selectionPlugin(),
    annotationPlugin(),
    redactionPlugin(),
  ];

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

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <RedactBar />
    <Stage class="stage">
      <RenderLayer />
      <SelectionLayer />
      <AnnotationLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

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

**`RedactBar.svelte`**

```svelte
<script lang="ts">
  import { untrack } from 'svelte';
  import { useStage } from '@embedpdf/svelte/stage';
  import { useInteraction, useInteractionState } from '@embedpdf/svelte/interaction';
  import { useAnnotationState } from '@embedpdf/svelte/annotation';
  import { useRedaction, useRedactionState } from '@embedpdf/svelte/redaction';

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

  const interaction = useInteraction();
  const activeToolId = useInteractionState((state) => state.activeToolId);
  const stage = useStage();
  const redaction = useRedaction();
  const redactionState = useRedactionState();
  const ready = useAnnotationState((state) => state.status === 'ready');

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

  const marking = $derived(activeToolId.current === 'redact');
</script>

<div class="toolbar">
  <button
    type="button"
    class="button"
    aria-pressed={marking}
    onclick={() => interaction.activateTool(marking ? 'pointer' : 'redact')}
  >
    Mark for redaction
  </button>
  <button
    type="button"
    class="button danger"
    disabled={!redactionState.pendingCount || redactionState.applying}
    onclick={() => void redaction.applyAll()}
  >
    Redact {redactionState.pendingCount}
    {redactionState.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>
```

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.

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { SelectionLayer, selectionPlugin } from '@embedpdf/svelte/selection';
  import { searchPlugin } from '@embedpdf/svelte/search';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { redactionPlugin } from '@embedpdf/svelte/redaction';
  import { localEngine } from '@embedpdf/engine';
  import MarkFromCode from './MarkFromCode.svelte';

  import '../from-code.css';

  const engine = localEngine();
  // Marking matches runs a search; marking the selected text needs a selection.
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin(),
    selectionPlugin(),
    searchPlugin(),
    annotationPlugin(),
    redactionPlugin(),
  ];

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

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <MarkFromCode />
    <Stage class="stage">
      <RenderLayer />
      <SelectionLayer />
      <AnnotationLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

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

**`MarkFromCode.svelte`**

```svelte
<script lang="ts">
  import { untrack } from 'svelte';
  import { useStageState } from '@embedpdf/svelte/stage';
  import { useSelectionState } from '@embedpdf/svelte/selection';
  import { useAnnotationState } from '@embedpdf/svelte/annotation';
  import { useRedaction, useRedactionState } from '@embedpdf/svelte/redaction';

  const redaction = useRedaction();
  const pendingCount = useRedactionState((state) => state.pendingCount);
  const hasSelection = useSelectionState((state) => state.hasSelection);
  const currentPage = useStageState((state) => state.currentPageIndex);
  const ready = useAnnotationState((state) => state.status === 'ready');
  let text = $state('EmbedPDF');

  // On load: every "EmbedPDF" in the document.
  let started = false;
  $effect(() => {
    if (!ready.current || started) return;
    started = true;
    untrack(() => void redaction.markMatches({ text: 'EmbedPDF' }));
  });

  function markEveryMatch(event: SubmitEvent) {
    event.preventDefault();
    if (text.trim()) void redaction.markMatches({ text: text.trim() });
  }
</script>

<div class="toolbar">
  <form class="query" onsubmit={markEveryMatch}>
    <input class="field" type="search" aria-label="Text to mark" bind:value={text} />
    <button type="submit" class="button" disabled={!text.trim()}>Every match</button>
  </form>
  <button
    type="button"
    class="button"
    disabled={!hasSelection.current}
    title="Select some text on the page first"
    onclick={() => void redaction.markSelection()}
  >
    The selected text
  </button>
  <button
    type="button"
    class="button"
    onclick={() => void redaction.markPage(currentPage.current)}
  >
    This page
  </button>
  <button
    type="button"
    class="button"
    disabled={!pendingCount.current}
    onclick={() => void redaction.clearPending()}
  >
    Remove all
  </button>
  <span class="spacer"></span>
  <output class="readout">
    {pendingCount.current}
    {pendingCount.current === 1 ? 'mark' : 'marks'}
  </output>
</div>
```

## Review the marks

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

**`pending.svelte`**

```svelte
<script lang="ts">
  import { annotationKey } from '@embedpdf/svelte/annotation';
  import { usePendingRedactions, useRedaction } from '@embedpdf/svelte/redaction';
  import { useStage } from '@embedpdf/svelte/stage';

  const stage = useStage();
  const redaction = useRedaction();
  const marks = usePendingRedactions();
</script>

<ul>
  {#each marks.current as mark (annotationKey(mark.ref))}
    <li>
      <button onclick={() => stage.reveal(mark.page, { rect: mark.bounds })}>
        Page {mark.pageIndex + 1}: {mark.kind === 'text' ? 'text' : 'area'}
      </button>
      <button onclick={() => redaction.unmark([mark.ref])}>Remove</button>
    </li>
  {/each}
</ul>
```

`redaction.clearPending()` removes every mark. `usePendingRedactions({ page: page.ref })`
lists one page's.

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { searchPlugin } from '@embedpdf/svelte/search';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { redactionPlugin } from '@embedpdf/svelte/redaction';
  import { localEngine } from '@embedpdf/engine';
  import MarkSome from './MarkSome.svelte';
  import PendingMarks from './PendingMarks.svelte';

  import '../review.css';

  const engine = localEngine();
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin(),
    searchPlugin(),
    annotationPlugin(),
    redactionPlugin(),
  ];

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

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <MarkSome />
    <div class="viewer">
      <Stage class="stage">
        <RenderLayer />
        <AnnotationLayer />
      </Stage>
      <PendingMarks />
    </div>
  </DocumentGate>
</Viewer>
```

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

**`MarkSome.svelte`**

```svelte
<!-- On load: the author's name, and every "commercial" in the document. -->
<script lang="ts">
  import { untrack } from 'svelte';
  import { useAnnotationState } from '@embedpdf/svelte/annotation';
  import { useRedaction } from '@embedpdf/svelte/redaction';

  const redaction = useRedaction();
  const ready = useAnnotationState((state) => state.status === 'ready');

  let started = false;
  $effect(() => {
    if (!ready.current || started) return;
    started = true;
    untrack(() => {
      void redaction
        .markArea(0, { x: 100, y: 508, width: 172, height: 50 })
        .then(() => redaction.markMatches({ text: 'commercial' }));
    });
  });
</script>
```

**`PendingMarks.svelte`**

```svelte
<script lang="ts">
  import { useStage } from '@embedpdf/svelte/stage';
  import { annotationKey } from '@embedpdf/svelte/annotation';
  import {
    usePendingRedactions,
    useRedaction,
    useRedactionState,
  } from '@embedpdf/svelte/redaction';

  const stage = useStage();
  const redaction = useRedaction();
  const marks = usePendingRedactions(); // the marks not applied yet, in page order
  const pendingCount = useRedactionState((state) => state.pendingCount);
</script>

<div class="panel">
  <div class="panel-head">
    <span class="readout">
      {pendingCount.current}
      {pendingCount.current === 1 ? 'mark' : 'marks'}
    </span>
    <button
      type="button"
      class="button"
      disabled={!pendingCount.current}
      onclick={() => void redaction.clearPending()}
    >
      Remove all
    </button>
  </div>
  <ul class="marks">
    {#each marks.current as mark (annotationKey(mark.ref))}
      <li class="mark">
        <button
          type="button"
          class="mark-go"
          onclick={() => 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)}
          onclick={() => void redaction.unmark([mark.ref])}
        >
          Remove
        </button>
      </li>
    {/each}
  </ul>
</div>
```

## 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).

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { redactionPlugin } from '@embedpdf/svelte/redaction';
  import { localEngine } from '@embedpdf/engine';
  import ApplyBar from './ApplyBar.svelte';

  import '../apply.css';

  const engine = localEngine();
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin(),
    annotationPlugin(),
    redactionPlugin(),
  ];

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

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <ApplyBar />
    <Stage class="stage">
      <RenderLayer />
      <AnnotationLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

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

**`ApplyBar.svelte`**

```svelte
<script lang="ts">
  import { untrack } from 'svelte';
  import { saveFile, useDocuments } from '@embedpdf/svelte/runtime';
  import { useAnnotationState } from '@embedpdf/svelte/annotation';
  import { useRedaction, useRedactionState } from '@embedpdf/svelte/redaction';

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

  const redaction = useRedaction();
  const documents = useDocuments();
  const redactionState = useRedactionState();
  const ready = useAnnotationState((state) => state.status === 'ready');
  let asking = $state(false);

  // On load: the title marked.
  let started = false;
  $effect(() => {
    if (!ready.current || started) return;
    started = true;
    untrack(() => void redaction.markArea(0, TITLE));
  });

  const marks = (count: number) => `${count} ${count === 1 ? 'mark' : 'marks'}`;

  // The other annotations applying removes too: they could show what was there. Read as the
  // question is drawn, next to the mark count, so it follows the marks.
  function collateral() {
    const { count } = redaction.estimateCollateral();
    return count > 0 ? `${count} ${count === 1 ? 'annotation' : 'annotations'} under them go too. ` : '';
  }

  function redactForGood() {
    void redaction.applyAll().finally(() => (asking = false));
  }

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

{#if asking}
  <div class="toolbar confirm" role="alertdialog" aria-label="Redact for good?">
    <span class="readout">
      Redact {marks(redactionState.pendingCount)}? {collateral()}This can’t be undone.
    </span>
    <span class="spacer"></span>
    <button type="button" class="button" onclick={() => (asking = false)}>Cancel</button>
    <button
      type="button"
      class="button danger"
      disabled={redactionState.applying}
      onclick={redactForGood}
    >
      Redact for good
    </button>
  </div>
{:else}
  <div class="toolbar">
    <button
      type="button"
      class="button danger"
      disabled={!redactionState.pendingCount || !redaction.canApply()}
      onclick={() => (asking = true)}
    >
      Redact…
    </button>
    <button
      type="button"
      class="button"
      disabled={!redactionState.lastResult}
      title="A fresh file, without the earlier revision that still holds the content"
      onclick={download}
    >
      Download
    </button>
    <span class="spacer"></span>
    <output class="readout">
      {redactionState.lastResult
        ? `Gone for good, with ${redactionState.lastResult.removedAnnotationCount} other annotations`
        : `${marks(redactionState.pendingCount)} waiting`}
    </output>
  </div>
{/if}
```

## 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.

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { redactionPlugin } from '@embedpdf/svelte/redaction';
  import { localEngine } from '@embedpdf/engine';
  import LabelBar from './LabelBar.svelte';

  import '../label.css';

  const engine = localEngine();
  // What applying paints over every mark: a dark blue area, its label in white.
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin(),
    annotationPlugin(),
    redactionPlugin({ overlay: { fill: '#1a2748', text: { color: '#ffffff' } } }),
  ];

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

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <LabelBar />
    <Stage class="stage">
      <RenderLayer />
      <AnnotationLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

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

**`LabelBar.svelte`**

```svelte
<script lang="ts">
  import { untrack } from 'svelte';
  import { useStage } from '@embedpdf/svelte/stage';
  import { useAnnotationState } from '@embedpdf/svelte/annotation';
  import {
    usePendingRedactions,
    useRedaction,
    useRedactionState,
  } from '@embedpdf/svelte/redaction';

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

  const redaction = useRedaction();
  const stage = useStage();
  const redactionState = useRedactionState();
  const pending = usePendingRedactions();
  const mark = $derived(pending.current[0]);
  const ready = useAnnotationState((state) => state.status === 'ready');
  let text = $state('Classified');

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

  function setLabel(event: SubmitEvent) {
    event.preventDefault();
    if (mark) void redaction.updateLabel(mark.ref, { overlayText: text.trim() || null });
  }

  function setRepeat(event: Event & { currentTarget: HTMLInputElement }) {
    if (mark) void redaction.updateLabel(mark.ref, { repeat: event.currentTarget.checked });
  }
</script>

<div class="toolbar">
  <form class="label-form" onsubmit={setLabel}>
    <input class="field" aria-label="Label" bind:value={text} disabled={!mark} />
    <button type="submit" class="button" disabled={!mark}>Set label</button>
  </form>
  <label class="label">
    <input type="checkbox" checked={mark?.repeat ?? false} disabled={!mark} onchange={setRepeat} />
    Repeat
  </label>
  <button
    type="button"
    class="button danger"
    disabled={!mark || redactionState.applying}
    onclick={() => void redaction.applyAll()}
  >
    Redact
  </button>
  <span class="spacer"></span>
  <output class="readout">
    {redactionState.lastResult ? 'The label is part of the page now' : 'Redact to see the label'}
  </output>
</div>
```

## Methods

Everything `useRedaction()` 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 `usePendingRedactions()`.                                                                       |
| `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 `useRedactionSettings()`. [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

Everything `useRedactionState()` gives you, as one reactive object: read its fields, and your markup updates when they change. 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 `usePendingRedactions()`.

## 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
`useRedactionSettings()` 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.svelte`**

```svelte
<script lang="ts">
  import { useRedactionEvent } from '@embedpdf/svelte/redaction';

  let message = $state('');

  useRedactionEvent(
    (redaction) => redaction.onApplied,
    () => (message = 'Redacted: the content under the marks is gone'),
  );
</script>

<p aria-live="polite">{message}</p>
```

| Event               | Fires when                                 | Carries               |
| :------------------ | :----------------------------------------- | :-------------------- |
| `onApplied`         | Marks were applied and the content is gone | `result`, `origin`    |
| `onPendingChanged`  | A mark was added, changed or removed       | `pages`               |
| `onSettingsChanged` | 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/svelte/annotations/redaction)
