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 redactionPlugin() next to interactionPlugin() and annotationPlugin(), and use the redact tool: select text to mark it, or drag to mark an area:

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

Loading live preview…

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

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…

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

Review the marks#

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

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

Loading live preview…

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

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…

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

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…

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

Methods#

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

FieldWhat it isRead once
pendingCountHow many marks are waiting to be appliedgetPendingCount(), or getPendingCount(page) for one page
applyingWhether marks are being applied right nowisApplying()
lastResultThe result of the last apply, from this session or another, or nullgetLastResult()

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.

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.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>
EventFires whenCarries
onAppliedMarks were applied and the content is goneresult, origin
onPendingChangedA mark was added, changed or removedpages
onSettingsChangedThe 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.