---
title: "Redaction — Vue"
description: "Mark text and areas for removal, review the marks, then remove the content for good."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/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.vue`**

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

const interaction = useInteraction();
const redaction = useRedaction();
const { pendingCount, applying } = useRedactionState();
</script>

<template>
  <button @click="interaction.activateTool('redact')">Mark for redaction</button>
  <button :disabled="!pendingCount || applying" @click="redaction.applyAll()">
    Redact {{ pendingCount }} marks
  </button>
</template>
```

**`App.vue`**

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

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <RedactBar />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <SelectionLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`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.vue`**

```vue
<script setup lang="ts">
import { computed, watch } from 'vue';
import { useStage } from '@embedpdf/vue/stage';
import { useInteraction, useInteractionState } from '@embedpdf/vue/interaction';
import { useAnnotationState } from '@embedpdf/vue/annotation';
import { useRedaction, useRedactionState } from '@embedpdf/vue/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();
const stage = useStage();
const redaction = useRedaction();
const { pendingCount, applying } = useRedactionState();
const ready = useAnnotationState((state) => state.status === 'ready');

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

const marking = computed(() => activeToolId.value === 'redact');
</script>

<template>
  <div class="toolbar">
    <button
      type="button"
      class="button"
      :aria-pressed="marking"
      @click="interaction.activateTool(marking ? 'pointer' : 'redact')"
    >
      Mark for redaction
    </button>
    <button
      type="button"
      class="button danger"
      :disabled="!pendingCount || applying"
      @click="redaction.applyAll()"
    >
      Redact {{ pendingCount }} {{ pendingCount === 1 ? 'mark' : 'marks' }}
    </button>
    <span class="spacer" />
    <output class="readout">
      {{ marking ? 'Select text, or drag over an area' : 'Click a mark to move it' }}
    </output>
  </div>
</template>
```

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.vue`**

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

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <MarkFromCode />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <SelectionLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`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.vue`**

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

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

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

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

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

## Review the marks

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

**`pending.vue`**

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

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

<template>
  <ul>
    <li v-for="mark in marks" :key="annotationKey(mark.ref)">
      <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>
</template>
```

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

**`App.vue`**

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

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <MarkSome />
      <div class="viewer">
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <AnnotationLayer />
          </template>
        </Stage>
        <PendingMarks />
      </div>
    </DocumentGate>
  </Viewer>
</template>
```

**`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.vue`**

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

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

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

<template></template>
```

**`PendingMarks.vue`**

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

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

<template>
  <div class="panel">
    <div class="panel-head">
      <span class="readout">{{ pendingCount }} {{ pendingCount === 1 ? 'mark' : 'marks' }}</span>
      <button
        type="button"
        class="button"
        :disabled="!pendingCount"
        @click="redaction.clearPending()"
      >
        Remove all
      </button>
    </div>
    <ul class="marks">
      <li v-for="mark in marks" :key="annotationKey(mark.ref)" 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>
  </div>
</template>
```

## 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.vue`**

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

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <ApplyBar />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`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.vue`**

```vue
<script setup lang="ts">
import { ref, watch } from 'vue';
import { saveFile, useDocuments } from '@embedpdf/vue/runtime';
import { useAnnotationState } from '@embedpdf/vue/annotation';
import { useRedaction, useRedactionState } from '@embedpdf/vue/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 { pendingCount, applying, lastResult } = useRedactionState();
const ready = useAnnotationState((state) => state.status === 'ready');
const asking = ref(false);

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

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

// The question, with the other annotations applying removes too: they could show what was there.
// Read as the template renders, so it follows the marks.
function question() {
  const { count } = redaction.estimateCollateral();
  const collateral =
    count > 0 ? `${count} ${count === 1 ? 'annotation' : 'annotations'} under them go too. ` : '';
  return `Redact ${marks(pendingCount.value)}? ${collateral}This can’t be undone.`;
}

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

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

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

## 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.vue`**

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

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <LabelBar />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`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.vue`**

```vue
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useStage } from '@embedpdf/vue/stage';
import { useAnnotationState } from '@embedpdf/vue/annotation';
import { usePendingRedactions, useRedaction, useRedactionState } from '@embedpdf/vue/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 { applying, lastResult } = useRedactionState();
const pending = usePendingRedactions();
const mark = computed(() => pending.value[0]);
const ready = useAnnotationState((state) => state.status === 'ready');
const text = ref('Classified');

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

function setLabel() {
  if (!mark.value) return;
  void redaction.updateLabel(mark.value.ref, { overlayText: text.value.trim() || null });
}

function setRepeat(event: Event) {
  const repeat = (event.target as HTMLInputElement).checked;
  if (mark.value) void redaction.updateLabel(mark.value.ref, { repeat });
}
</script>

<template>
  <div class="toolbar">
    <form class="label-form" @submit.prevent="setLabel">
      <input v-model="text" class="field" aria-label="Label" :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"
        @change="setRepeat"
      />
      Repeat
    </label>
    <button
      type="button"
      class="button danger"
      :disabled="!mark || applying"
      @click="redaction.applyAll()"
    >
      Redact
    </button>
    <span class="spacer" />
    <output class="readout">
      {{ lastResult ? 'The label is part of the page now' : 'Redact to see the label' }}
    </output>
  </div>
</template>
```

## 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 refs: your template updates when a value 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 `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.vue`**

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

const message = ref('');

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

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

| 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/vue/annotations/redaction)
