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:
import { useInteraction } from '@embedpdf/react/interaction';
import { useRedaction, useRedactionState } from '@embedpdf/react/redaction';
export function RedactBar() {
const interaction = useInteraction();
const redaction = useRedaction();
const { pendingCount, applying } = useRedactionState();
return (
<>
<button onClick={() => interaction.activateTool('redact')}>Mark for redaction</button>
<button disabled={!pendingCount || applying} onClick={() => redaction.applyAll()}>
Redact {pendingCount} marks
</button>
</>
);
}Loading live preview…
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import {
interactionPlugin,
useInteraction,
useInteractionState,
} from '@embedpdf/react/interaction';
import { SelectionLayer, selectionPlugin } from '@embedpdf/react/selection';
import { AnnotationLayer, annotationPlugin, useAnnotationState } from '@embedpdf/react/annotation';
import { redactionPlugin, useRedaction, useRedactionState } from '@embedpdf/react/redaction';
import { localEngine } from '@embedpdf/engine';
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()) };
};
// The author's name on the cover, in page coordinates.
const AUTHOR = { x: 100, y: 508, width: 172, height: 50 };
function RedactBar() {
const interaction = useInteraction();
const { activeToolId } = useInteractionState();
const stage = useStage();
const redaction = useRedaction();
const { pendingCount, applying } = useRedactionState();
const ready = useAnnotationState((state) => state.status === 'ready');
const started = useRef(false);
// On load: the author's name marked, and the redact tool on.
useEffect(() => {
if (!ready || started.current) return;
started.current = true;
void redaction.markArea(0, AUTHOR).then(() => stage.reveal(0, { rect: AUTHOR }));
interaction.activateTool('redact');
}, [redaction, interaction, stage, ready]);
const marking = activeToolId === 'redact';
return (
<div className="toolbar">
<button
type="button"
className="button"
aria-pressed={marking}
onClick={() => interaction.activateTool(marking ? 'pointer' : 'redact')}
>
Mark for redaction
</button>
<button
type="button"
className="button danger"
disabled={!pendingCount || applying}
onClick={() => void redaction.applyAll()}
>
Redact {pendingCount} {pendingCount === 1 ? 'mark' : 'marks'}
</button>
<span className="spacer" />
<output className="readout">
{marking ? 'Select text, or drag over an area' : 'Click a mark to move it'}
</output>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<RedactBar />
<Stage className="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 numbermarkMatches takes the same query as search, and resolves with { marks }, one per
match.
Loading live preview…
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { SelectionLayer, selectionPlugin, useSelectionState } from '@embedpdf/react/selection';
import { searchPlugin } from '@embedpdf/react/search';
import { AnnotationLayer, annotationPlugin, useAnnotationState } from '@embedpdf/react/annotation';
import { redactionPlugin, useRedaction, useRedactionState } from '@embedpdf/react/redaction';
import { localEngine } from '@embedpdf/engine';
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()) };
};
function MarkFromCode() {
const redaction = useRedaction();
const { pendingCount } = useRedactionState();
const { hasSelection } = useSelectionState();
const currentPage = useStageState((state) => state.currentPageIndex);
const ready = useAnnotationState((state) => state.status === 'ready');
const [text, setText] = useState('EmbedPDF');
const started = useRef(false);
// On load: every "EmbedPDF" in the document.
useEffect(() => {
if (!ready || started.current) return;
started.current = true;
void redaction.markMatches({ text: 'EmbedPDF' });
}, [redaction, ready]);
return (
<div className="toolbar">
<form
className="query"
onSubmit={(event) => {
event.preventDefault();
if (text.trim()) void redaction.markMatches({ text: text.trim() });
}}
>
<input
className="field"
type="search"
aria-label="Text to mark"
value={text}
onChange={(event) => setText(event.target.value)}
/>
<button type="submit" className="button" disabled={!text.trim()}>
Every match
</button>
</form>
<button
type="button"
className="button"
disabled={!hasSelection}
title="Select some text on the page first"
onClick={() => void redaction.markSelection()}
>
The selected text
</button>
<button type="button" className="button" onClick={() => void redaction.markPage(currentPage)}>
This page
</button>
<button
type="button"
className="button"
disabled={!pendingCount}
onClick={() => void redaction.clearPending()}
>
Remove all
</button>
<span className="spacer" />
<output className="readout">
{pendingCount} {pendingCount === 1 ? 'mark' : 'marks'}
</output>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<MarkFromCode />
<Stage className="stage">
{() => (
<>
<RenderLayer />
<SelectionLayer />
<AnnotationLayer />
</>
)}
</Stage>
</DocumentGate>
</Viewer>
);
}Review the marks#
usePendingRedactions() lists the marks that haven’t been applied yet, for a review panel:
import { annotationKey } from '@embedpdf/react/annotation';
import { usePendingRedactions, useRedaction } from '@embedpdf/react/redaction';
import { useStage } from '@embedpdf/react/stage';
export function Pending() {
const stage = useStage();
const redaction = useRedaction();
const marks = usePendingRedactions();
return (
<ul>
{marks.map((mark) => (
<li key={annotationKey(mark.ref)}>
<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>
))}
</ul>
);
}redaction.clearPending() removes every mark. usePendingRedactions({ page: page.ref })
lists one page’s.
Loading live preview…
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { searchPlugin } from '@embedpdf/react/search';
import {
AnnotationLayer,
annotationKey,
annotationPlugin,
useAnnotationState,
} from '@embedpdf/react/annotation';
import {
redactionPlugin,
usePendingRedactions,
useRedaction,
useRedactionState,
} from '@embedpdf/react/redaction';
import { localEngine } from '@embedpdf/engine';
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()) };
};
// On load: the author's name, and every "commercial" in the document.
function MarkSome() {
const redaction = useRedaction();
const ready = useAnnotationState((state) => state.status === 'ready');
const started = useRef(false);
useEffect(() => {
if (!ready || started.current) return;
started.current = true;
void redaction
.markArea(0, { x: 100, y: 508, width: 172, height: 50 })
.then(() => redaction.markMatches({ text: 'commercial' }));
}, [redaction, ready]);
return null;
}
function PendingMarks() {
const stage = useStage();
const redaction = useRedaction();
const marks = usePendingRedactions(); // the marks not applied yet, in page order
const { pendingCount } = useRedactionState();
return (
<div className="panel">
<div className="panel-head">
<span className="readout">
{pendingCount} {pendingCount === 1 ? 'mark' : 'marks'}
</span>
<button
type="button"
className="button"
disabled={!pendingCount}
onClick={() => void redaction.clearPending()}
>
Remove all
</button>
</div>
<ul className="marks">
{marks.map((mark) => (
<li key={annotationKey(mark.ref)} className="mark">
<button
type="button"
className="mark-go"
onClick={() => stage.reveal(mark.page, { rect: mark.bounds })}
>
<span className="mark-page">Page {mark.pageIndex + 1}</span>
<span className="mark-kind">{mark.kind === 'text' ? 'Text' : 'Area'}</span>
</button>
<button
type="button"
className="button"
disabled={!redaction.canUnmark(mark.ref)}
onClick={() => void redaction.unmark([mark.ref])}
>
Remove
</button>
</li>
))}
</ul>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<MarkSome />
<div className="viewer">
<Stage className="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 pagesThis can’t be undone. Ask first, and say what else goes with it: annotations a mark overlaps are
removed too, because they could show what was there. estimateCollateral() tells you how many:
const { count } = redaction.estimateCollateral();
if (!(await confirm(`Redact? ${count} annotations under the marks will be removed too.`))) return;
await redaction.applyAll();After applying, download the document with { mode: 'rewrite' }, so the removed content isn’t left in
an earlier revision of the file. See Saving & downloading.
Loading live preview…
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, saveFile, useDocuments } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin, useAnnotationState } from '@embedpdf/react/annotation';
import { redactionPlugin, useRedaction, useRedactionState } from '@embedpdf/react/redaction';
import { localEngine } from '@embedpdf/engine';
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()) };
};
// 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 };
function ApplyBar() {
const redaction = useRedaction();
const documents = useDocuments();
const { pendingCount, applying, lastResult } = useRedactionState();
const ready = useAnnotationState((state) => state.status === 'ready');
const [asking, setAsking] = useState(false);
const started = useRef(false);
// On load: the title marked.
useEffect(() => {
if (!ready || started.current) return;
started.current = true;
void redaction.markArea(0, TITLE);
}, [redaction, ready]);
// The other annotations applying removes too: they could show what was there.
const { count } = redaction.estimateCollateral();
if (asking) {
return (
<div className="toolbar confirm" role="alertdialog" aria-label="Redact for good?">
<span className="readout">
Redact {pendingCount} {pendingCount === 1 ? 'mark' : 'marks'}?{' '}
{count > 0 &&
`${count} ${count === 1 ? 'annotation' : 'annotations'} under them go too. `}
This can’t be undone.
</span>
<span className="spacer" />
<button type="button" className="button" onClick={() => setAsking(false)}>
Cancel
</button>
<button
type="button"
className="button danger"
disabled={applying}
onClick={() => void redaction.applyAll().finally(() => setAsking(false))}
>
Redact for good
</button>
</div>
);
}
return (
<div className="toolbar">
<button
type="button"
className="button danger"
disabled={!pendingCount || !redaction.canApply()}
onClick={() => setAsking(true)}
>
Redact…
</button>
<button
type="button"
className="button"
disabled={!lastResult}
title="A fresh file, without the earlier revision that still holds the content"
onClick={() =>
void documents
.download(undefined, { mode: 'rewrite' })
.then((bytes) => saveFile(bytes, 'redacted.pdf', 'application/pdf'))
}
>
Download
</button>
<span className="spacer" />
<output className="readout">
{lastResult
? `Gone for good, with ${lastResult.removedAnnotationCount} other annotations`
: `${pendingCount} ${pendingCount === 1 ? 'mark' : 'marks'} waiting`}
</output>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<ApplyBar />
<Stage className="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…
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin, useAnnotationState } from '@embedpdf/react/annotation';
import {
redactionPlugin,
usePendingRedactions,
useRedaction,
useRedactionState,
} from '@embedpdf/react/redaction';
import { localEngine } from '@embedpdf/engine';
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()) };
};
// The cover's subtitle, in page coordinates.
const SUBTITLE = { x: 100, y: 378, width: 352, height: 114 };
function LabelBar() {
const redaction = useRedaction();
const stage = useStage();
const { applying, lastResult } = useRedactionState();
const [mark] = usePendingRedactions();
const ready = useAnnotationState((state) => state.status === 'ready');
const [text, setText] = useState('Classified');
const started = useRef(false);
// On load: the subtitle marked, with a label that fills the area.
useEffect(() => {
if (!ready || started.current) return;
started.current = true;
void redaction
.markArea(0, SUBTITLE)
.then(({ mark }) =>
redaction.updateLabel(mark.ref, { overlayText: 'Classified', repeat: true }),
)
.then(() => stage.reveal(0, { rect: SUBTITLE }));
}, [redaction, stage, ready]);
return (
<div className="toolbar">
<form
className="label-form"
onSubmit={(event) => {
event.preventDefault();
if (mark) void redaction.updateLabel(mark.ref, { overlayText: text.trim() || null });
}}
>
<input
className="field"
aria-label="Label"
value={text}
disabled={!mark}
onChange={(event) => setText(event.target.value)}
/>
<button type="submit" className="button" disabled={!mark}>
Set label
</button>
</form>
<label className="label">
<input
type="checkbox"
checked={mark?.repeat ?? false}
disabled={!mark}
onChange={(event) =>
mark && void redaction.updateLabel(mark.ref, { repeat: event.target.checked })
}
/>
Repeat
</label>
<button
type="button"
className="button danger"
disabled={!mark || applying}
onClick={() => void redaction.applyAll()}
>
Redact
</button>
<span className="spacer" />
<output className="readout">
{lastResult ? 'The label is part of the page now' : 'Redact to see the label'}
</output>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<LabelBar />
<Stage className="stage">
{() => (
<>
<RenderLayer />
<AnnotationLayer />
</>
)}
</Stage>
</DocumentGate>
</Viewer>
);
}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 |
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. Your component re-renders 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(). Keeping them apart means a component that
only shows the count doesn’t re-render for every mark.
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#
import { useState } from 'react';
import { useRedactionEvent } from '@embedpdf/react/redaction';
export function RedactionNotice() {
const [message, setMessage] = useState('');
useRedactionEvent(
(redaction) => redaction.onApplied,
() => setMessage('Redacted: the content under the marks is gone'),
);
return <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 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.
Your feedback goes directly to the documentation team.