> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/get-started/samples/showcase-demo-redaction.md).

# Redaction Showcase Demo Code Sample

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 y

{% hint style="info" %}
**Requirements**

*These packages are required to use these features in production. Trial keys have unlimited access to all features*

<a href="/web/get-started/readme.md" class="button primary">Web SDK</a><a href="/web/full-api/full-api-overview.md" class="button primary">Full API</a><a href="https://apryse.com/capabilities#Redaction" class="button primary">Package: Redaction</a><a href="https://showcase.apryse.com/ocr-module" class="button primary">Live demo</a>
{% endhint %}

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:

* Support 30+ document types including PDF, MS Office (doc, docx, xlsx, pptx) and Images (jpg, png), all converted to PDF for processing
* Load form local source or URL
* Search using free-text or predefined patterns:
  * Phone Numbers
  * Credit Card Numbers
  * Emails
* Customize search types:
  * Match Case
  * Match Whole Words
  * Wildcard '\*'
  * Regular Expressions
  * Search Direction Up
  * Ambient String (returns surrounding strings to matches)
* Download document as PDF with saved redactions

**Implementation steps** To add Redaction capability with WebViewer:

Step 1: Choose your [preferred web stack](/web/get-started/readme.md) 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-license-key/apryse-license-key platform="WEB\_VIEWER" variant="compact" %}

{% tabs %}
{% tab title="index.js" %}

<pre class="language-js" data-line-numbers><code class="lang-js">// ES6 Compliant Syntax
// GitHub Copilot v1, Claude Sonnet 4 (Preview), October 5, 2025
// File: showcase-demos/redaction/index.js

import WebViewer from '@pdftron/webviewer';

const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_WEBVIEWER_LICENSE_KEY"</code>';

// Global variables to track state
let redactionDemoFile = "https://apryse.s3.amazonaws.com/public/files/samples/sales-invoice-with-credit-cards.pdf";
const searchResults = []; // Store search results globally for access in other functions

// Function to initialize and load the Redaction Tool
function initializeWebViewer() {

  const element = document.getElementById('viewer');
  if (!element) {
    console.error('Viewer div not found.');
    return;
  }

  WebViewer({
    path: '/lib',
    initialDoc: redactionDemoFile,
    licenseKey: licenseKey,
    enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File  
    enableRedaction: true, // Enable redaction feature
    backendType: WebViewer.BackendTypes.WASM, //required for redaction https://community.apryse.com/t/pdfworkererror-related-to-exclusive-lock-in-recursivesharedmutex-cpp-on-emscripten-platform/10059
    fullAPI: true, // Required to use the PDFNet API
    loadAsPDF: true,
    disableElements: ['searchPanel', 'searchButton'], // Disable built-in search to prevent focus errors
  }, element).then(instance => {

    const { documentViewer } = instance.Core;

    documentViewer.addEventListener('documentLoaded', () => {
      instance.UI.openElements(['redactionPanel']);
      instance.UI.disableElements(disabledElements);
      instance.UI.addSearchListener(searchListener); //Handle search events to capture results for redaction
    });

    // UI Section 
    createUIElements();
  });
}

// Function to apply redactions based on search results
async function applyRedactions() {
  const { documentViewer } = window.WebViewer.getInstance().Core;
  const annotationManager = documentViewer.getAnnotationManager();
  const annotations = await formatAnnotations(searchResults);
  console.log('Global results', searchResults);

  //Accessing the annotation manager to add and draw annotations
  annotationManager.addAnnotations(annotations);
  annotationManager.drawAnnotationsFromList(annotations);

  // Apply redactions
  annotationManager.applyRedactions();

  // Clear search results and the searchResults array after applying redactions
  documentViewer.clearSearchResults();
  searchResults.length = 0;
}

// Search Listener, captures search results and adds redaction annotations
// Only add it once to avoid multiple triggers
const searchListener = (searchPattern, options, results) => {
  const { UI } = window.WebViewer.getInstance();
  addAnnotationsUsingSearchResult(results);
  if (results.length > 0) {
    UI.openElements(['redactionPanel']);
  }
  else
    UI.closeElements(['redactionPanel']);

  console.log('Search complete: ', searchPattern, options, results);
};

// Function to perform search and add redaction annotations
function search(searchtext, searchOptions) {

  const { documentViewer } = window.WebViewer.getInstance().Core;
  const { UI } = window.WebViewer.getInstance();

  const annotationManagerObj = documentViewer.getAnnotationManager();
  const annotationList = annotationManagerObj.getAnnotationsList();
  annotationManagerObj.deleteAnnotations(annotationList);
  UI.searchTextFull(searchtext, searchOptions); // Perform the search with given options

}

// Function to format search results into redaction annotations
async function formatAnnotations(results) {
  const { documentViewer, Annotations } = window.WebViewer.getInstance().Core;
  const annotationManager = documentViewer.getAnnotationManager();
  const redactionList = annotationManager
    .getAnnotationsList()
    .filter((annot) => annot instanceof Annotations.RedactionAnnotation);

  return await results.flatMap((r) => {
    const annotation = new Annotations.RedactionAnnotation();
    annotation.PageNumber = r.page_num;
    annotation.Quads = r.quads.map((quad) => quad.getPoints());
    annotation.StrokeColor = new Annotations.Color(0, 255, 0);
    annotation.setContents(r.result_str);
    annotation.Author = 'Guest';
    annotation.setCustomData(
      'trn-annot-preview',
      documentViewer.getSelectedText(annotation.PageNumber)
    );
    if (redactionList.some((r) => r.getContents() === annotation.getContents())) {
      return [];
    }
    return [annotation];
  });
}

// Function to add annotations using search results
// This function is called from the search listener
async function addAnnotationsUsingSearchResult(results) {
  const { documentViewer } = window.WebViewer.getInstance().Core;
  const annotationManager = documentViewer.getAnnotationManager();

  //Keep results in global variable to access later if needed
  searchResults.push(...results);
  console.log('results', results);
  const annotations = await formatAnnotations(results);
  annotationManager.addAnnotations(annotations);
  annotationManager.drawAnnotationsFromList(annotations);
};

// Search options for redaction
// You can modify these options or add more as needed
const searchOptions = {
  caseSensitive: true,  // match case
  wholeWord: true,      // match whole words only
  wildcard: false,      // allow using '*' as a wildcard value
  regex: false,         // string is treated as a regular expression
  searchUp: false,      // search from the end of the document upwards
  ambientString: true,  // return ambient string as part of the result
};

// Sample redaction search patterns using regex
// You can modify or add more patterns as needed
// WebViewer implements its own pattern similar to these below, here we define our own for the redaction demo
const redactionSearchSamples = [
  {
    label: 'Phone Numbers',
    value: '\\b(?:\\+?1[-\\s]?)?(?:\\(?[0-9]{3}\\)?[-\\s]?)[0-9]{3}[-\\s]?[0-9]{4}\\b',
  },
  {
    label: 'Emails',
    value: '\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}\\b',
  },
  {
    label: 'Credit Card Numbers',
    value: '\\b(?:\\d[ -]*?){13,16}\\b',
  },
];

//UI Elements to disable
const disabledElements = [
  'toolbarGroup-Shapes',
  'toolbarGroup-View',
  'toolbarGroup-Insert',
  'toolbarGroup-Annotate',
  'toolbarGroup-FillAndSign',
  'toolbarGroup-Forms',
  'toolbarGroup-Edit',
  'toolbarGroup-Measure',
];

// UI Elements
// ui-elements.js
// Function to create and initialize UI elements
function createUIElements() {
  // Create a container for all controls (label, dropdown, and buttons)
  // Dynamically load ui-elements.js if not already loaded
  if (!window.SidePanel) {
    const script = document.createElement('script');
    script.src = '/showcase-demos/redaction/ui-elements.js';
    script.onload = () => {
      UIElements.init('viewer', searchResults);
      UIElements.handleException(); //Add handling of Reacts focus error on this JavaScript sample.
    };
    document.head.appendChild(script);
  }
}

//Make functions accessible globally
window.redactionSearchSamples = redactionSearchSamples;
window.searchOptions = searchOptions;
window.applyRedactions = applyRedactions;
window.search = search;

// Initialize the WebViewer
initializeWebViewer();

</code></pre>

{% endtab %}

{% tab title="ui-elements.js" %}
{% code title="ui-elements.js" lineNumbers="true" %}

```js
// ES6 Compliant Syntax
// GitHub Copilot v1, Claude Sonnet 4 (Preview), October 5, 2025
// File: showcase-demos/redaction/ui-elements.js

class UIElements {

  static init(containerId, searchResults) {

    //Add buttons to the container
    const container = document.getElementById(containerId);
    const controlsContainer = document.createElement('div');
    controlsContainer.className = 'control-container';
    controlsContainer.id = 'ui-container-panel';

    // Create container for button2 and input2
    const inputButtonGroup2 = document.createElement('div');
    inputButtonGroup2.className = 'input-button-group';

    const input2 = document.createElement('input');
    input2.type = 'text';
    input2.placeholder = 'Search text or regex';
    input2.className = 'redaction-input group-input';

    const button2 = document.createElement('button');
    button2.innerText = 'Search';
    button2.className = 'redaction-btn redaction-btn-secondary group-button';
    button2.onclick = () => {
      let options = window.searchOptions;
      options.regex = false;
      window.search(input2.value, options);
      UIElements.enableApplyRedactionsButton(searchResults.length === 0 ? true : false);
    }

    inputButtonGroup2.appendChild(input2);
    inputButtonGroup2.appendChild(button2);
    controlsContainer.appendChild(inputButtonGroup2);

    // Create container for button3 and input3
    const inputButtonGroup3 = document.createElement('div');
    inputButtonGroup3.className = 'input-button-group';


    const button3 = document.createElement('button');
    button3.innerText = 'Search';
    button3.className = 'redaction-btn redaction-btn-secondary group-button';
    button3.onclick = () => {

      // Trigger search function here

      let options = window.searchOptions;
      options.regex = true;
      let regexString = getPatternByLabel(dropdown.value);
      window.search(regexString, options);
      UIElements.enableApplyRedactionsButton(searchResults.length === 0 ? true : false);
    }

    // Function to get pattern by label
    const getPatternByLabel = (label) => {
      const sample = redactionSearchSamples.find(sample => sample.label === label);
      return sample ? sample.value : null;
    };


    //Add a dropdown with 3 values: Phone, Email, Credit Card
    const dropdown = document.createElement('select');
    dropdown.className = 'redaction-dropdown group-input';
    const option1 = document.createElement('option');
    option1.value = 'Phone Numbers';
    option1.text = 'Phone Numbers';
    option1.className = 'redaction-option';
    dropdown.appendChild(option1);

    const option2 = document.createElement('option');
    option2.value = 'Emails';
    option2.text = 'Emails';
    option2.className = 'redaction-option';
    dropdown.appendChild(option2);

    const option3 = document.createElement('option');
    option3.value = 'Credit Card Numbers';
    option3.text = 'Credit Card Numbers';
    option3.className = 'redaction-option';
    dropdown.appendChild(option3);

    // Add event listener to dropdown
    dropdown.onchange = () => {
      console.log(`Selected: ${dropdown.value}`);
    }

    inputButtonGroup3.appendChild(dropdown);
    inputButtonGroup3.appendChild(button3);
    controlsContainer.appendChild(inputButtonGroup3);

    // Add Apply Redactions button
    const applyRedactionButton = document.createElement('button');
    applyRedactionButton.id = 'apply-redaction';
    applyRedactionButton.innerText = 'Apply Redactions';
    applyRedactionButton.className = 'redaction-btn redaction-btn-primary apply-redaction-btn';
    applyRedactionButton.disabled = true; // Initially disabled
    applyRedactionButton.onclick = () => {
      console.log('Apply Redactions button clicked');
      if (window.applyRedactions) {
        window.applyRedactions();
        UIElements.enableApplyRedactionsButton(false);
      } else {
        console.error('applyRedactions function not available');
      }
    };

    controlsContainer.appendChild(applyRedactionButton);
    container.insertBefore(controlsContainer, container.firstChild);

    // Store button reference globally for enabling/disabling
    window.applyRedactionButton = applyRedactionButton;
  }

  // Function to enable/disable the Apply Redactions button
  static enableApplyRedactionsButton(enable) {
    if (window.applyRedactionButton) {
      window.applyRedactionButton.disabled = !enable;
      console.log(`Apply Redactions button ${enable ? 'enabled' : 'disabled'}`);
    }
  }

  static handleException() {

    // Prevent focus errors on null refs by overriding the focus method temporarily
    const originalFocus = HTMLElement.prototype.focus;
    HTMLElement.prototype.focus = function (...args) {
      try {
        if (this && typeof this.focus === 'function') {
          return originalFocus.apply(this, args);
        }
      } catch (error) {
        console.warn('Focus prevented on null/undefined element:', error.message);
      }
    };

    // Add global error handler for unhandled focus errors
    window.addEventListener('error', (event) => {
      if (event.error && event.error.message &&
        event.error.message.includes('Cannot read properties of null') &&
        event.error.message.includes('focus')) {
        console.warn('Prevented null focus error:', event.error.message);
        event.preventDefault();
        return false;
      }
    });

    // Handle unhandled promise rejections related to focus
    window.addEventListener('unhandledrejection', (event) => {
      if (event.reason && event.reason.message &&
        event.reason.message.includes('focus')) {
        console.warn('Prevented focus promise rejection:', event.reason.message);
        event.preventDefault();
      }
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="index.css" %}
{% code title="index.css" lineNumbers="true" %}

```css
/* Redaction Demo UI Styles */

/* Main layout - side by side containers within #viewer */
#viewer {
  display: flex;
  height: 100%;
  width: 100%;
}

#ui-container-panel {
  width: 300px;
  min-width: 300px;
  height: 100%;
  overflow-y: auto;
  background-color: #f8f9fa;
  border-right: 2px solid #e9ecef;
  box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
  flex-shrink: 0;
}

#wv-viewer {
  flex: 1;
  height: 100%;
  min-width: 0;
}

/* Main container styling */
.control-container {
  padding: 20px;
  background-color: #f8f9fa;
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
  box-sizing: border-box;
}

/* Button base styles */
.redaction-btn {
  padding: 10px 16px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
  min-height: 40px;
}

.redaction-btn-primary:hover {
  background-color: #0056b3;
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
}

.redaction-btn-primary:active {
  transform: translateY(0);
  box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3);
}

/* Secondary button styling */
.redaction-btn-secondary {
  background-color: #6c757d;
  color: white;
}

.redaction-btn-secondary:hover {
  background-color: #545b62;
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(108, 117, 125, 0.3);
}

.redaction-btn-secondary:active {
  transform: translateY(0);
  box-shadow: 0 2px 4px rgba(108, 117, 125, 0.3);
}

/* Apply Redactions button specific styling */
.apply-redaction-btn {
  background-color: #007bff !important; /* Blue background */
  color: white;
  font-weight: 600;
  margin-top: 12px;
  width: 100%;
  position: relative;
}

.apply-redaction-btn:hover:not(:disabled) {
  background-color: #0056b3 !important;
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(0, 123, 255, 0.4);
}

.apply-redaction-btn:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: 0 2px 4px rgba(0, 123, 255, 0.4);
}

.apply-redaction-btn:disabled {
  background-color: #6c757d !important;
  color: #adb5bd;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.apply-redaction-btn:disabled:hover {
  background-color: #6c757d !important;
  transform: none;
  box-shadow: none;
}

/* Input field styling */
.redaction-input {
  padding: 10px 12px;
  border: 2px solid #e9ecef;
  border-radius: 6px;
  font-size: 14px;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
  min-height: 40px;
  box-sizing: border-box;
}

.redaction-input:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}

.redaction-input::placeholder {
  color: #6c757d;
  opacity: 0.7;
}

/* Dropdown styling */
.redaction-dropdown {
  padding: 10px 12px;
  border: 2px solid #e9ecef;
  border-radius: 6px;
  font-size: 14px;
  background-color: white;
  cursor: pointer;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
  min-height: 40px;
  box-sizing: border-box;
}

.redaction-dropdown:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}

.redaction-dropdown:hover {
  border-color: #ced4da;
}

/* Dropdown option styling */
.redaction-option {
  padding: 8px 12px;
  font-size: 14px;
}

.redaction-option:hover {
  background-color: #f8f9fa;
}

/* Input-Button group styling */
.input-button-group {
  display: flex;
  gap: 0;
  align-items: stretch;
  width: 100%;
  box-sizing: border-box;
  overflow: hidden;
}

.group-input {
  flex: 1;
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  border-right: none;
  margin: 0;
  min-width: 0;
  box-sizing: border-box;
}

/* Apply group styling to both inputs and dropdowns within groups */
.input-button-group .redaction-dropdown.group-input {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  border-right: none;
  min-width: 0;
}

.group-button {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  margin: 0;
  white-space: nowrap;
  flex-shrink: 0;
  box-sizing: border-box;
}

.group-input:focus {
  z-index: 1;
  position: relative;
}

/* Responsive design */
@media (max-width: 768px) {
  #viewer {
    flex-direction: column;
  }
  
  #ui-container-panel {
    width: 100%;
    min-width: auto;
    height: auto;
    max-height: 40vh;
    border-right: none;
    border-bottom: 2px solid #e9ecef;
  }
  
  #wv-viewer {
    flex: 1;
    min-height: 60vh;
  }
  
  .control-container {
    padding: 15px;
  }
  
  .redaction-btn,
  .redaction-input,
  .redaction-dropdown {
    font-size: 16px; /* Prevent zoom on iOS */
    min-height: 44px; /* Better touch target */
  }
}

/* For very large screens, allow more space for the panel */
@media (min-width: 1200px) {
  #ui-container-panel {
    width: 300px;
    min-width: 300px;
  }
}

/* Focus indicators for accessibility */
.redaction-btn:focus,
.redaction-input:focus,
.redaction-dropdown:focus {
  box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}

/* Disabled state */
.redaction-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
}

.redaction-btn:disabled:hover {
  transform: none;
  box-shadow: none;
}

```

{% endcode %}
{% endtab %}
{% endtabs %}

[View the full sample on GitHub](https://github.com/ApryseSDK/webviewer-samples/tree/main/showcase-demos-playground)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/get-started/samples/showcase-demo-redaction.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
