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: Download required modules listed in the Demo Dependencies section below
Step 3: Add the ES6 JavaScript sample code provided in this guide
Demo Dependencies
This sample uses the following:
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