Quickly search and redact sensitive text within documents, all handled securely on the client side. Redaction is performed entirely within your private network, ensuring sensitive data never leaves your environment.
This demo allows you to:
Implementation steps
To add Redaction capability with WebViewer:
Step 1: Choose your preferred web stack
Step 2: Add the ES6 JavaScript sample code provided in this guide.
Once you generate your license key, it will automatically be included in your sample code below.
Apryse collects some data regarding your usage of the SDK for product improvement.
The data that Apryse collects include:
For clarity, no other data is collected by the SDK and Apryse has no access to the contents of your documents.
If you wish to continue without data collection, contact us and we will email you a no-tracking trial key for you to get started.
1// ES6 Compliant Syntax
2// GitHub Copilot v1, Claude Sonnet 4 (Preview), October 5, 2025
3// File: showcase-demos/redaction/index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7// Global variables to track state.
8let redactionDemoFile = "https://apryse.s3.amazonaws.com/public/files/samples/sales-invoice-with-credit-cards.pdf";
9const searchResults = []; // Store search results globally for access in other functions.
10
11// Function to initialize and load the Redaction Tool.
12function initializeWebViewer() {
13
14 const element = document.getElementById('viewer');
15 if (!element) {
16 console.error('Viewer div not found.');
17 return;
18 }
19
20 WebViewer({
21 path: '/lib',
22 initialDoc: redactionDemoFile,
23 licenseKey: 'YOUR_LICENSE_KEY',
24 enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File.
25 enableRedaction: true, // Enable redaction feature.
26 backendType: WebViewer.BackendTypes.WASM, // Required for redaction. https://community.apryse.com/t/pdfworkererror-related-to-exclusive-lock-in-recursivesharedmutex-cpp-on-emscripten-platform/10059
27 fullAPI: true, // Required to use the PDFNet API.
28 loadAsPDF: true,
29 disableElements: ['searchPanel', 'searchButton'], // Disable built-in search to prevent focus errors.
30 }, element).then(instance => {
31
32 const { documentViewer } = instance.Core;
33
34 documentViewer.addEventListener('documentLoaded', () => {
35 instance.UI.openElements(['redactionPanel']);
36 instance.UI.disableElements(disabledElements);
37 instance.UI.addSearchListener(searchListener); //Handle search events to capture results for redaction.
38 });
39
40 // UI Section
41 createUIElements();
42 });
43}
44
45// Function to apply redactions based on search results.
46async function applyRedactions() {
47 const { documentViewer } = window.WebViewer.getInstance().Core;
48 const annotationManager = documentViewer.getAnnotationManager();
49 const annotations = await formatAnnotations(searchResults);
50 console.log('Global results', searchResults);
51
52 //Accessing the annotation manager to add and draw annotations.
53 annotationManager.addAnnotations(annotations);
54 annotationManager.drawAnnotationsFromList(annotations);
55
56 // Apply redactions.
57 annotationManager.applyRedactions();
58
59 // Clear search results and the searchResults array after applying redactions.
60 documentViewer.clearSearchResults();
61 searchResults.length = 0;
62}
63
64// Search Listener, captures search results and adds redaction annotations.
65// Only add it once to avoid multiple triggers.
66const searchListener = (searchPattern, options, results) => {
67 const { UI } = window.WebViewer.getInstance();
68 addAnnotationsUsingSearchResult(results);
69 if (results.length > 0) {
70 UI.openElements(['redactionPanel']);
71 }
72 else
73 UI.closeElements(['redactionPanel']);
74
75 console.log('Search complete: ', searchPattern, options, results);
76};
77
78// Function to perform search and add redaction annotations.
79function search(searchtext, searchOptions) {
80
81 const { documentViewer } = window.WebViewer.getInstance().Core;
82 const { UI } = window.WebViewer.getInstance();
83
84 const annotationManagerObj = documentViewer.getAnnotationManager();
85 const annotationList = annotationManagerObj.getAnnotationsList();
86 annotationManagerObj.deleteAnnotations(annotationList);
87 UI.searchTextFull(searchtext, searchOptions); // Perform the search with given options.
88
89}
90
91// Function to format search results into redaction annotations.
92async function formatAnnotations(results) {
93 const { documentViewer, Annotations } = window.WebViewer.getInstance().Core;
94 const annotationManager = documentViewer.getAnnotationManager();
95 const redactionList = annotationManager
96 .getAnnotationsList()
97 .filter((annot) => annot instanceof Annotations.RedactionAnnotation);
98
99 return await results.flatMap((r) => {
100 const annotation = new Annotations.RedactionAnnotation();
101 annotation.PageNumber = r.page_num;
102 annotation.Quads = r.quads.map((quad) => quad.getPoints());
103 annotation.StrokeColor = new Annotations.Color(0, 255, 0);
104 annotation.setContents(r.result_str);
105 annotation.Author = 'Guest';
106 annotation.setCustomData(
107 'trn-annot-preview',
108 documentViewer.getSelectedText(annotation.PageNumber)
109 );
110 if (redactionList.some((r) => r.getContents() === annotation.getContents())) {
111 return [];
112 }
113 return [annotation];
114 });
115}
116
117// Function to add annotations using search results.
118// This function is called from the search listener.
119async function addAnnotationsUsingSearchResult(results) {
120 const { documentViewer } = window.WebViewer.getInstance().Core;
121 const annotationManager = documentViewer.getAnnotationManager();
122
123 // Keep results in global variable to access later, if needed.
124 searchResults.push(...results);
125 console.log('results', results);
126 const annotations = await formatAnnotations(results);
127 annotationManager.addAnnotations(annotations);
128 annotationManager.drawAnnotationsFromList(annotations);
129};
130
131// Search options for redaction
132// You can modify these options or add more as needed.
133const searchOptions = {
134 caseSensitive: true, // Match case.
135 wholeWord: true, // Match whole words only.
136 wildcard: false, // Allow using '*' as a wildcard value.
137 regex: false, // String is treated as a regular expression.
138 searchUp: false, // Search from the end of the document upwards.
139 ambientString: true, // Return ambient string as part of the result.
140};
141
142// Sample redaction search patterns using regex.
143// You can modify or add more patterns as needed.
144// WebViewer implements its own pattern similar to these below. Here, we define our own for the redaction demo,
145const redactionSearchSamples = [
146 {
147 label: 'Phone Numbers',
148 value: '\\b(?:\\+?1[-\\s]?)?(?:\\(?[0-9]{3}\\)?[-\\s]?)[0-9]{3}[-\\s]?[0-9]{4}\\b',
149 },
150 {
151 label: 'Emails',
152 value: '\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}\\b',
153 },
154 {
155 label: 'Credit Card Numbers',
156 value: '\\b(?:\\d[ -]*?){13,16}\\b',
157 },
158];
159
160//UI Elements to disable
161const disabledElements = [
162 'toolbarGroup-Shapes',
163 'toolbarGroup-View',
164 'toolbarGroup-Insert',
165 'toolbarGroup-Annotate',
166 'toolbarGroup-FillAndSign',
167 'toolbarGroup-Forms',
168 'toolbarGroup-Edit',
169 'toolbarGroup-Measure',
170];
171
172// UI Elements
173// ui-elements.js
174// Function to create and initialize UI elements.
175function createUIElements() {
176 // Create a container for all controls (label, dropdown, and buttons).
177 // Dynamically load ui-elements.js, if not already loaded.
178 if (!window.SidePanel) {
179 const script = document.createElement('script');
180 script.src = '/showcase-demos/redaction/ui-elements.js';
181 script.onload = () => {
182 UIElements.init('viewer', searchResults);
183 UIElements.handleException(); // Add handling of Reacts focus error on this JavaScript sample.
184 };
185 document.head.appendChild(script);
186 }
187}
188
189// Make functions accessible globally.
190window.redactionSearchSamples = redactionSearchSamples;
191window.searchOptions = searchOptions;
192window.applyRedactions = applyRedactions;
193window.search = search;
194
195// Initialize the WebViewer.
196initializeWebViewer();
197
1// ES6 Compliant Syntax
2// GitHub Copilot v1, Claude Sonnet 4 (Preview), October 5, 2025
3// File: showcase-demos/redaction/ui-elements.js
4
5class UIElements {
6
7 static init(containerId, searchResults) {
8
9 // Add buttons to the container.
10 const container = document.getElementById(containerId);
11 const controlsContainer = document.createElement('div');
12 controlsContainer.className = 'control-container';
13 controlsContainer.id = 'ui-container-panel';
14
15 // Create container for button2 and input2.
16 const inputButtonGroup2 = document.createElement('div');
17 inputButtonGroup2.className = 'input-button-group';
18
19 const input2 = document.createElement('input');
20 input2.type = 'text';
21 input2.placeholder = 'Search text or regex';
22 input2.className = 'redaction-input group-input';
23
24 const button2 = document.createElement('button');
25 button2.innerText = 'Search';
26 button2.className = 'redaction-btn redaction-btn-secondary group-button';
27 button2.onclick = () => {
28 let options = window.searchOptions;
29 options.regex = false;
30 window.search(input2.value, options);
31 UIElements.enableApplyRedactionsButton(searchResults.length === 0 ? true : false);
32 }
33
34 inputButtonGroup2.appendChild(input2);
35 inputButtonGroup2.appendChild(button2);
36 controlsContainer.appendChild(inputButtonGroup2);
37
38 // Create container for button3 and input3.
39 const inputButtonGroup3 = document.createElement('div');
40 inputButtonGroup3.className = 'input-button-group';
41
42
43 const button3 = document.createElement('button');
44 button3.innerText = 'Search';
45 button3.className = 'redaction-btn redaction-btn-secondary group-button';
46 button3.onclick = () => {
47
48 // Trigger search function here.
49
50 let options = window.searchOptions;
51 options.regex = true;
52 let regexString = getPatternByLabel(dropdown.value);
53 window.search(regexString, options);
54 UIElements.enableApplyRedactionsButton(searchResults.length === 0 ? true : false);
55 }
56
57 // Function to get pattern by label.
58 const getPatternByLabel = (label) => {
59 const sample = redactionSearchSamples.find(sample => sample.label === label);
60 return sample ? sample.value : null;
61 };
62
63
64 // Add a dropdown with 3 values: Phone, Email, Credit Card.
65 const dropdown = document.createElement('select');
66 dropdown.className = 'redaction-dropdown group-input';
67 const option1 = document.createElement('option');
68 option1.value = 'Phone Numbers';
69 option1.text = 'Phone Numbers';
70 option1.className = 'redaction-option';
71 dropdown.appendChild(option1);
72
73 const option2 = document.createElement('option');
74 option2.value = 'Emails';
75 option2.text = 'Emails';
76 option2.className = 'redaction-option';
77 dropdown.appendChild(option2);
78
79 const option3 = document.createElement('option');
80 option3.value = 'Credit Card Numbers';
81 option3.text = 'Credit Card Numbers';
82 option3.className = 'redaction-option';
83 dropdown.appendChild(option3);
84
85 // Add event listener to dropdown.
86 dropdown.onchange = () => {
87 console.log(`Selected: ${dropdown.value}`);
88 }
89
90 inputButtonGroup3.appendChild(dropdown);
91 inputButtonGroup3.appendChild(button3);
92 controlsContainer.appendChild(inputButtonGroup3);
93
94 // Add Apply Redactions button.
95 const applyRedactionButton = document.createElement('button');
96 applyRedactionButton.id = 'apply-redaction';
97 applyRedactionButton.innerText = 'Apply Redactions';
98 applyRedactionButton.className = 'redaction-btn redaction-btn-primary apply-redaction-btn';
99 applyRedactionButton.disabled = true; // Initially disabled
100 applyRedactionButton.onclick = () => {
101 console.log('Apply Redactions button clicked');
102 if (window.applyRedactions) {
103 window.applyRedactions();
104 UIElements.enableApplyRedactionsButton(false);
105 } else {
106 console.error('applyRedactions function not available');
107 }
108 };
109
110 controlsContainer.appendChild(applyRedactionButton);
111 container.insertBefore(controlsContainer, container.firstChild);
112
113 // Store button reference globally for enabling/disabling.
114 window.applyRedactionButton = applyRedactionButton;
115 }
116
117 // Function to enable/disable the Apply Redactions button.
118 static enableApplyRedactionsButton(enable) {
119 if (window.applyRedactionButton) {
120 window.applyRedactionButton.disabled = !enable;
121 console.log(`Apply Redactions button ${enable ? 'enabled' : 'disabled'}`);
122 }
123 }
124
125 static handleException() {
126
127 // Prevent focus errors on null refs by overriding the focus method temporarily.
128 const originalFocus = HTMLElement.prototype.focus;
129 HTMLElement.prototype.focus = function (...args) {
130 try {
131 if (this && typeof this.focus === 'function') {
132 return originalFocus.apply(this, args);
133 }
134 } catch (error) {
135 console.warn('Focus prevented on null/undefined element:', error.message);
136 }
137 };
138
139 // Add global error handler for unhandled focus errors.
140 window.addEventListener('error', (event) => {
141 if (event.error && event.error.message &&
142 event.error.message.includes('Cannot read properties of null') &&
143 event.error.message.includes('focus')) {
144 console.warn('Prevented null focus error:', event.error.message);
145 event.preventDefault();
146 return false;
147 }
148 });
149
150 // Handle unhandled promise rejections related to focus.
151 window.addEventListener('unhandledrejection', (event) => {
152 if (event.reason && event.reason.message &&
153 event.reason.message.includes('focus')) {
154 console.warn('Prevented focus promise rejection:', event.reason.message);
155 event.preventDefault();
156 }
157 });
158 }
159}
160
1/* Redaction Demo UI Styles */
2
3/* Main layout - side by side containers within #viewer */
4#viewer {
5 display: flex;
6 height: 100%;
7 width: 100%;
8}
9
10#ui-container-panel {
11 width: 300px;
12 min-width: 300px;
13 height: 100%;
14 overflow-y: auto;
15 background-color: #f8f9fa;
16 border-right: 2px solid #e9ecef;
17 box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
18 flex-shrink: 0;
19}
20
21#wv-viewer {
22 flex: 1;
23 height: 100%;
24 min-width: 0;
25}
26
27/* Main container styling */
28.control-container {
29 padding: 20px;
30 background-color: #f8f9fa;
31 display: flex;
32 flex-direction: column;
33 gap: 12px;
34 width: 100%;
35 box-sizing: border-box;
36}
37
38/* Button base styles */
39.redaction-btn {
40 padding: 10px 16px;
41 border: none;
42 border-radius: 6px;
43 font-size: 14px;
44 font-weight: 500;
45 cursor: pointer;
46 transition: all 0.2s ease;
47 min-height: 40px;
48}
49
50.redaction-btn-primary:hover {
51 background-color: #0056b3;
52 transform: translateY(-1px);
53 box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
54}
55
56.redaction-btn-primary:active {
57 transform: translateY(0);
58 box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3);
59}
60
61/* Secondary button styling */
62.redaction-btn-secondary {
63 background-color: #6c757d;
64 color: white;
65}
66
67.redaction-btn-secondary:hover {
68 background-color: #545b62;
69 transform: translateY(-1px);
70 box-shadow: 0 4px 8px rgba(108, 117, 125, 0.3);
71}
72
73.redaction-btn-secondary:active {
74 transform: translateY(0);
75 box-shadow: 0 2px 4px rgba(108, 117, 125, 0.3);
76}
77
78/* Apply Redactions button specific styling. */
79.apply-redaction-btn {
80 background-color: #007bff !important; /* Blue background */
81 color: white;
82 font-weight: 600;
83 margin-top: 12px;
84 width: 100%;
85 position: relative;
86}
87
88.apply-redaction-btn:hover:not(:disabled) {
89 background-color: #0056b3 !important;
90 transform: translateY(-1px);
91 box-shadow: 0 4px 8px rgba(0, 123, 255, 0.4);
92}
93
94.apply-redaction-btn:active:not(:disabled) {
95 transform: translateY(0);
96 box-shadow: 0 2px 4px rgba(0, 123, 255, 0.4);
97}
98
99.apply-redaction-btn:disabled {
100 background-color: #6c757d !important;
101 color: #adb5bd;
102 cursor: not-allowed;
103 transform: none;
104 box-shadow: none;
105}
106
107.apply-redaction-btn:disabled:hover {
108 background-color: #6c757d !important;
109 transform: none;
110 box-shadow: none;
111}
112
113/* Input field styling */
114.redaction-input {
115 padding: 10px 12px;
116 border: 2px solid #e9ecef;
117 border-radius: 6px;
118 font-size: 14px;
119 transition: border-color 0.2s ease, box-shadow 0.2s ease;
120 min-height: 40px;
121 box-sizing: border-box;
122}
123
124.redaction-input:focus {
125 outline: none;
126 border-color: #007bff;
127 box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
128}
129
130.redaction-input::placeholder {
131 color: #6c757d;
132 opacity: 0.7;
133}
134
135/* Dropdown styling */
136.redaction-dropdown {
137 padding: 10px 12px;
138 border: 2px solid #e9ecef;
139 border-radius: 6px;
140 font-size: 14px;
141 background-color: white;
142 cursor: pointer;
143 transition: border-color 0.2s ease, box-shadow 0.2s ease;
144 min-height: 40px;
145 box-sizing: border-box;
146}
147
148.redaction-dropdown:focus {
149 outline: none;
150 border-color: #007bff;
151 box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
152}
153
154.redaction-dropdown:hover {
155 border-color: #ced4da;
156}
157
158/* Dropdown option styling */
159.redaction-option {
160 padding: 8px 12px;
161 font-size: 14px;
162}
163
164.redaction-option:hover {
165 background-color: #f8f9fa;
166}
167
168/* Input-Button group styling */
169.input-button-group {
170 display: flex;
171 gap: 0;
172 align-items: stretch;
173 width: 100%;
174 box-sizing: border-box;
175 overflow: hidden;
176}
177
178.group-input {
179 flex: 1;
180 border-top-right-radius: 0;
181 border-bottom-right-radius: 0;
182 border-right: none;
183 margin: 0;
184 min-width: 0;
185 box-sizing: border-box;
186}
187
188/* Apply group styling to both inputs and dropdowns within groups. */
189.input-button-group .redaction-dropdown.group-input {
190 border-top-right-radius: 0;
191 border-bottom-right-radius: 0;
192 border-right: none;
193 min-width: 0;
194}
195
196.group-button {
197 border-top-left-radius: 0;
198 border-bottom-left-radius: 0;
199 margin: 0;
200 white-space: nowrap;
201 flex-shrink: 0;
202 box-sizing: border-box;
203}
204
205.group-input:focus {
206 z-index: 1;
207 position: relative;
208}
209
210/* Responsive design */
211@media (max-width: 768px) {
212 #viewer {
213 flex-direction: column;
214 }
215
216 #ui-container-panel {
217 width: 100%;
218 min-width: auto;
219 height: auto;
220 max-height: 40vh;
221 border-right: none;
222 border-bottom: 2px solid #e9ecef;
223 }
224
225 #wv-viewer {
226 flex: 1;
227 min-height: 60vh;
228 }
229
230 .control-container {
231 padding: 15px;
232 }
233
234 .redaction-btn,
235 .redaction-input,
236 .redaction-dropdown {
237 font-size: 16px; /* Prevent zoom on iOS */
238 min-height: 44px; /* Better touch target */
239 }
240}
241
242/* For very large screens, allow more space for the panel. */
243@media (min-width: 1200px) {
244 #ui-container-panel {
245 width: 300px;
246 min-width: 300px;
247 }
248}
249
250/* Focus indicators for accessibility */
251.redaction-btn:focus,
252.redaction-input:focus,
253.redaction-dropdown:focus {
254 box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
255}
256
257/* Disabled state */
258.redaction-btn:disabled {
259 opacity: 0.6;
260 cursor: not-allowed;
261 transform: none;
262}
263
264.redaction-btn:disabled:hover {
265 transform: none;
266 box-shadow: none;
267}
268
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales