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
7const licenseKey = 'YOUR_WEBVIEWER_LICENSE_KEY';
8
9// Global variables to track state
10let redactionDemoFile = "https://apryse.s3.amazonaws.com/public/files/samples/sales-invoice-with-credit-cards.pdf";
11const searchResults = []; // Store search results globally for access in other functions
12
13// Function to initialize and load the Redaction Tool
14function initializeWebViewer() {
15
16 const element = document.getElementById('viewer');
17 if (!element) {
18 console.error('Viewer div not found.');
19 return;
20 }
21
22 WebViewer({
23 path: '/lib',
24 initialDoc: redactionDemoFile,
25 licenseKey: licenseKey,
26 enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
27 enableRedaction: true, // Enable redaction feature
28 backendType: WebViewer.BackendTypes.WASM, //required for redaction https://community.apryse.com/t/pdfworkererror-related-to-exclusive-lock-in-recursivesharedmutex-cpp-on-emscripten-platform/10059
29 fullAPI: true, // Required to use the PDFNet API
30 loadAsPDF: true,
31 disableElements: ['searchPanel', 'searchButton'], // Disable built-in search to prevent focus errors
32 }, element).then(instance => {
33
34 const { documentViewer } = instance.Core;
35
36 documentViewer.addEventListener('documentLoaded', () => {
37 instance.UI.openElements(['redactionPanel']);
38 instance.UI.disableElements(disabledElements);
39 instance.UI.addSearchListener(searchListener); //Handle search events to capture results for redaction
40 });
41
42 // UI Section
43 createUIElements();
44 });
45}
46
47// Function to apply redactions based on search results
48async function applyRedactions() {
49 const { documentViewer } = window.WebViewer.getInstance().Core;
50 const annotationManager = documentViewer.getAnnotationManager();
51 const annotations = await formatAnnotations(searchResults);
52 console.log('Global results', searchResults);
53
54 //Accessing the annotation manager to add and draw annotations
55 annotationManager.addAnnotations(annotations);
56 annotationManager.drawAnnotationsFromList(annotations);
57
58 // Apply redactions
59 annotationManager.applyRedactions();
60
61 // Clear search results and the searchResults array after applying redactions
62 documentViewer.clearSearchResults();
63 searchResults.length = 0;
64}
65
66// Search Listener, captures search results and adds redaction annotations
67// Only add it once to avoid multiple triggers
68const searchListener = (searchPattern, options, results) => {
69 const { UI } = window.WebViewer.getInstance();
70 addAnnotationsUsingSearchResult(results);
71 if (results.length > 0) {
72 UI.openElements(['redactionPanel']);
73 }
74 else
75 UI.closeElements(['redactionPanel']);
76
77 console.log('Search complete: ', searchPattern, options, results);
78};
79
80// Function to perform search and add redaction annotations
81function search(searchtext, searchOptions) {
82
83 const { documentViewer } = window.WebViewer.getInstance().Core;
84 const { UI } = window.WebViewer.getInstance();
85
86 const annotationManagerObj = documentViewer.getAnnotationManager();
87 const annotationList = annotationManagerObj.getAnnotationsList();
88 annotationManagerObj.deleteAnnotations(annotationList);
89 UI.searchTextFull(searchtext, searchOptions); // Perform the search with given options
90
91}
92
93// Function to format search results into redaction annotations
94async function formatAnnotations(results) {
95 const { documentViewer, Annotations } = window.WebViewer.getInstance().Core;
96 const annotationManager = documentViewer.getAnnotationManager();
97 const redactionList = annotationManager
98 .getAnnotationsList()
99 .filter((annot) => annot instanceof Annotations.RedactionAnnotation);
100
101 return await results.flatMap((r) => {
102 const annotation = new Annotations.RedactionAnnotation();
103 annotation.PageNumber = r.page_num;
104 annotation.Quads = r.quads.map((quad) => quad.getPoints());
105 annotation.StrokeColor = new Annotations.Color(0, 255, 0);
106 annotation.setContents(r.result_str);
107 annotation.Author = 'Guest';
108 annotation.setCustomData(
109 'trn-annot-preview',
110 documentViewer.getSelectedText(annotation.PageNumber)
111 );
112 if (redactionList.some((r) => r.getContents() === annotation.getContents())) {
113 return [];
114 }
115 return [annotation];
116 });
117}
118
119// Function to add annotations using search results
120// This function is called from the search listener
121async function addAnnotationsUsingSearchResult(results) {
122 const { documentViewer } = window.WebViewer.getInstance().Core;
123 const annotationManager = documentViewer.getAnnotationManager();
124
125 //Keep results in global variable to access later if needed
126 searchResults.push(...results);
127 console.log('results', results);
128 const annotations = await formatAnnotations(results);
129 annotationManager.addAnnotations(annotations);
130 annotationManager.drawAnnotationsFromList(annotations);
131};
132
133// Search options for redaction
134// You can modify these options or add more as needed
135const searchOptions = {
136 caseSensitive: true, // match case
137 wholeWord: true, // match whole words only
138 wildcard: false, // allow using '*' as a wildcard value
139 regex: false, // string is treated as a regular expression
140 searchUp: false, // search from the end of the document upwards
141 ambientString: true, // return ambient string as part of the result
142};
143
144// Sample redaction search patterns using regex
145// You can modify or add more patterns as needed
146// WebViewer implements its own pattern similar to these below, here we define our own for the redaction demo
147const redactionSearchSamples = [
148 {
149 label: 'Phone Numbers',
150 value: '\\b(?:\\+?1[-\\s]?)?(?:\\(?[0-9]{3}\\)?[-\\s]?)[0-9]{3}[-\\s]?[0-9]{4}\\b',
151 },
152 {
153 label: 'Emails',
154 value: '\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}\\b',
155 },
156 {
157 label: 'Credit Card Numbers',
158 value: '\\b(?:\\d[ -]*?){13,16}\\b',
159 },
160];
161
162//UI Elements to disable
163const disabledElements = [
164 'toolbarGroup-Shapes',
165 'toolbarGroup-View',
166 'toolbarGroup-Insert',
167 'toolbarGroup-Annotate',
168 'toolbarGroup-FillAndSign',
169 'toolbarGroup-Forms',
170 'toolbarGroup-Edit',
171 'toolbarGroup-Measure',
172];
173
174// UI Elements
175// ui-elements.js
176// Function to create and initialize UI elements
177function createUIElements() {
178 // Create a container for all controls (label, dropdown, and buttons)
179 // Dynamically load ui-elements.js if not already loaded
180 if (!window.SidePanel) {
181 const script = document.createElement('script');
182 script.src = '/showcase-demos/redaction/ui-elements.js';
183 script.onload = () => {
184 UIElements.init('viewer', searchResults);
185 UIElements.handleException(); //Add handling of Reacts focus error on this JavaScript sample.
186 };
187 document.head.appendChild(script);
188 }
189}
190
191//Make functions accessible globally
192window.redactionSearchSamples = redactionSearchSamples;
193window.searchOptions = searchOptions;
194window.applyRedactions = applyRedactions;
195window.search = search;
196
197// Initialize the WebViewer
198initializeWebViewer();
199
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