> 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-ocr-module.md).

# OCR Showcase Demo Code Sample

Sample code will help you generate searchable PDFs from scanned documents or images using OCR with Apryse Server SDK, and view with WebViewer.

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

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

<a href="/core/get-started/get-started.md" class="button primary">Server SDK</a><a href="/web/get-started/readme.md" class="button primary">Web SDK</a><a href="https://apryse.com/capabilities#OpticalCharacterRecognition(OCR)" class="button primary">Package: OCR</a><a href="https://docs.apryse.com/core/guides/info/modules#ocr-module" class="button primary">Module: OCR</a><a href="https://showcase.apryse.com/ocr-module" class="button primary">Live demo</a>
{% endhint %}

Generate searchable PDFs from scanned documents or images using OCR (Optical Character Recognition). The scanned file that contains characters is converted into machine-readable searchable and selectable text as a PDF document.

This sample code includes Server SDK processing in JavaScript, with UI provided by WebViewer. If a viewer is not needed, or you want to work with a different language or framework for the Server SDK, please check out our [Server SDK OCR Sample Code](/core/get-started/samples/ocrtest.md).

This demo allows you to:

* Choose your own PDF or image file
* Select default documents in various languages: English, French, Spanish, Italian, German, Russian
* Convert into characters that are selectable and searchable
* Extract the characters as string

### **Implementation steps**

To add OCR Module capability with Server SDK, and view with WebViewer:

Step 1: Follow get-started in [JavaScript for Server SDK ](/core/get-started/frameworks/nodejs.md)Step 2: Follow get-started in your [preferred web stack for WebViewer](/web/get-started/readme.md) Step 3: [Download OCR Module](/core/learn-more/modules.md#ocr-module) Step 4: Add the ES6 JavaScript sample code provided in this guide

> **Note:** Only the first page is processed in this demo.

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.0 - GPT-4 Model - September 24, 2024
// File: ocr-module/client/index.js

import WebViewer from '@pdftron/webviewer';

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

function initializeWebViewer() {

  console.log('Initializing WebViewer...');
  const viewerElement = document.getElementById('viewer');
  if (!viewerElement) {
    console.error('❌ Viewer element not found. Please ensure there is a div with id "viewer" in your HTML.');
    return;
  }

  WebViewer({
    path: '/lib',
    initialDoc: UIElements.demoFilesPath + UIElements.ocrdemofiles['eng'],
    enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
    enableMeasurement: false,
    loadAsPDF: true,
    licenseKey: licenseKey,
  }, viewerElement).then(instance => {

    // Once the PDF document is loaded, send it to the server.
    // The sent PDF document will be processed by the server,
    // by extracting form fields JSON data when the user clicks the "Detect Form Fields" button.
    instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
      if (!UIElements.loadingFromList) {
        UIElements.loadedFromPicker = true;
        UIElements.loadLabelText = ' Use file picker to load your own file';
      }
      
      // if next time we load from list, this will be set to true again
      UIElements.loadingFromList = false; 
      
      // Customize the main webviewer left panel after loading completes
      UIElements.customizeUI(instance, performOCR);
    });
    console.log('✅ WebViewer loaded successfully.');
  }).catch((error) => {
    console.error('Failed to initialize WebViewer:', error);
  });
}

// Perform OCR by sending the current PDF page to the server
const performOCR = async (instance) => {
  // Reset JSON data
  UIElements.jsonData = null;
  let resultText = '';

  // Preparation of the PDF blob to be sent to the server
  const doc = instance.Core.documentViewer.getDocument();
  const currentPage = instance.Core.documentViewer.getCurrentPage();
  const xfdfString = await instance.Core.annotationManager.exportAnnotations(); // obtaining annotations in the loaded document
  const data = await doc.getFileData({ xfdfString });
  const arr = new Uint8Array(data);
  const blob = new Blob([arr], { type: 'application/pdf' });
  const formData = new FormData();
  formData.append(doc.filename, blob, doc.filename);
  // Send the PDF blob to the server for processing
  new Promise(function (resolve, reject) {
    console.log('🚀 Sending PDF to server for processing...');
    fetch(`http://localhost:5050/server/handler.js?filename=${doc.filename}&#x26;currentPage=${currentPage}&#x26;lang=${UIElements.selectedLanguage}`, {
      method: 'POST',
      body: formData,
    }).then(function (response) {

      if (response.status === 200) {
        response.text().then(function (json) {
          UIElements.jsonData = json;
          const ocrResult = JSON.parse(UIElements.jsonData);

          for (const page of ocrResult.Page) {
            for (const para of page.Para) {
              for (const line of para.Line) {
                for (const word of line.Word) {
                  resultText += word.text + ' ';
                }
                resultText += '\n';
              }
            }
          }
          resolve();
        })
      } else {
        const errorText = `❌ Server responded with status: ${response.status}`;
        resultText = errorText + resultText;
        console.error(resultText);
        reject(new Error(`Server error: ${response.status}`));
      }
    }).catch(function (error) {
      let errorText = '❌ Failed to connect to server: ' + error;
      errorText += '\n📍 Attempted URL: http://localhost:5050/server/handler.js';
      errorText += '\n🔍 This likely means the OCR server is not running on port 5050';
      console.error(errorText);
      resultText = errorText + resultText;
      reject(error);
    });
  }).catch(function (error) {
    const errorText = '❌ Error in PDF upload promise: ' + error;
    console.error(errorText);
    resultText = errorText + resultText;
  }).finally(function () {
    UIElements.ocrText.textContent = resultText;
    UIElements.setLoading(instance, false);
  });
}


function loadUIElementsScript() {
  return new Promise((resolve, reject) => {
    if (window.UIElements) {
      console.log('UIElements already loaded');
      resolve();
      return;
    }
    
    const script = document.createElement('script');
    script.src = '/showcase-demos/ocr-module/client/ui-elements.js';
    script.onload = function () {
      console.log('✅ UIElements script loaded successfully');
      resolve();
    };
    script.onerror = function () {
      console.error('Failed to load UIElements script');
      reject(new Error('Failed to load ui-elements.js'));
    };
    document.head.appendChild(script);
  });
}

// Load UIElements script first, then initialize WebViewer
loadUIElementsScript().then(() => {
  initializeWebViewer();
}).catch((error) => {
  console.error('Failed to load UIElements:', error);
});
</code></pre>

{% endtab %}

{% tab title="handler.js" %}

<pre class="language-js" data-line-numbers><code class="lang-js">const { PDFNet } = require('@pdftron/pdfnet-node');
const path = require('path');
const fs = require('fs');

// **Important** 
// You must get a license key from Apryse for the server to run. 
// A trial key can be obtained from:
// https://docs.apryse.com/core/guides/get-started/trial-key 
const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_SERVER_LICENSE_KEY"</code>';
const multer = require('multer');
const { response } = require('express');
const upload = multer();
const serverFolder = 'server';
const sentImages = 'sentImages';
const serverHandler = `/${serverFolder}/handler.js`;

module.exports = async (app) => {

  async function initializeServer() {
    const ocrOK = await PDFNet.OCRModule.isModuleAvailable();
    if (!ocrOK) {
      console.log('\nUnable to run OCR Demo: Apryse SDK OCR module not available.');
      console.log('---------------------------------------------------------------');
      console.log('The OCR module is an optional add-on, available for download');
      console.log('at https://docs.apryse.com/core/guides/info/modules#ocr-module . If you have already downloaded this');
      console.log('module, ensure that the SDK is able to find the required files');
      console.log('using the PDFNet.addResourceSearchPath() function.\n');
    }
    else {
      console.log('Apryse SDK OCR module is available.');
      if (!fs.existsSync(sentImages))
        fs.mkdirSync(sentImages);
    }
  }

  // Handle POST request sent to '/server/handler.js'
  // This endpoint receives the currently loaded PDF file in the Apryse webviewer, converts the current page to an image,
  // recognizes text from the image, then sends it back to the client as JSON data
  app.post(serverHandler, upload.any(), async (request, response) => {
    const sentPdf = path.resolve(__dirname, `./${sentImages.split('/').pop()}/${request.query.filename}`);
    const currentPage = request.query.currentPage ? parseInt(request.query.currentPage) : 1;
    const draw = await PDFNet.PDFDraw.create();  // PDFDraw class is used to rasterize PDF pages.
    const doc = await PDFNet.PDFDoc.createFromBuffer(request.files[0].buffer);
    draw.setDPI(300);
    const ocrPage = await (await doc.getPageIterator(currentPage)).current();
    const imageName = sentPdf + `_page${currentPage}.png`;
    await draw.export(ocrPage, imageName);

    const opts = new PDFNet.OCRModule.OCROptions();
    const useIRIS = await PDFNet.OCRModule.isIRISModuleAvailable();
    if(useIRIS) opts.setOCREngine('iris');
    let json = null;
    response.header('Content-Type', 'application/json');
    try {
      const doc = await PDFNet.PDFDoc.create();
      opts.addLang(request.query.lang || 'eng');
      json = await PDFNet.OCRModule.getOCRJsonFromImage(doc, imageName, opts);
      await fs.promises.unlink(imageName); // delete the image after OCR
      //json = await PDFNet.OCRModule.getOCRJsonFromPDF(doc, opts);
      response.status(200).send(json);
    } catch (e) {
      response.status(500).send(`Error extracting JSON text from PDF file ${request.query.filename}`);
    }
  });

  // Initialize PDFNet
  PDFNet.runWithoutCleanup(initializeServer, licenseKey).then(
    function onFulfilled() {
      response.status(200);
    },
    function onRejected(error) {
      // log error and close response
      console.error('Error initializing PDFNet', error);
      response.status(503).send();
    }
  );
};
</code></pre>

{% endtab %}

{% tab title="server.js" %}
{% code title="server.js" lineNumbers="true" %}

```js

const express = require('express');
const fs = require('fs');
const bodyParser = require('body-parser');
const open = (...args) => import('open').then(({ default: open }) => open(...args));
const handler = require('./handler.js');
const port = process.env.PORT || 5050;
const app = express();
const sentPdfs = 'sentPdfs';

// CORS middleware to allow cross-origin requests from the playground
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  
  // Handle preflight OPTIONS requests
  if (req.method === 'OPTIONS') {
    res.sendStatus(200);
  } else {
    next();
  }
});

app.use(bodyParser.text());
app.use('/client', express.static('../client')); // For statically serving 'client' folder at '/'

handler(app);

// Run server
const server = app.listen(port, 'localhost', (err) => {
  if (err) {
    console.error(err);
  } else {
    console.info(`Server is listening at http://localhost:${port}`);
    
  }
});

// Server shutdown and cleanup
function shutdown() {
  console.log('Cleanup started...');

  // Example: Close server
  server.close(() => {
    console.log('Server closed.');

    // Removes sent PDFs folder
    if (fs.existsSync(sentPdfs))
      fs.rmdirSync(sentPdfs, { recursive: true });

    // If no async cleanup, exit directly
    process.exit(0);
  });
}

// Handle shutdown signals
process.on('SIGINT', shutdown);   // Ctrl+C
process.on('SIGTERM', shutdown);  // kill command or Docker stop
process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  shutdown();
});
```

{% endcode %}
{% endtab %}

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

```js
// Class with static UI elements and related functions for the OCR demo

class UIElements {
    static languages = {
        eng: 'English',
        fra: 'French',
        spa: 'Spanish',
        ita: 'Italian',
        deu: 'German',
        rus: 'Russian',
    };

    static demoFilesPath = "https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/";
    static ocrdemofiles = {
        eng: 'scanned_sublease_agreement.pdf',
        fra: 'scanned_sublease_agreement_french.pdf',
        spa: 'scanned_sublease_agreement_spanish.pdf',
        ita: 'scanned_sublease_agreement_italian.pdf',
        deu: 'scanned_sublease_agreement_german.pdf',
        rus: 'scanned_sublease_agreement_russian.pdf',
    };

    static loadingFromList = true; // initially true because we load the first document from the list
    static loadedFromPicker = false; // to differentiate if the document is loaded from the list or from file picker
    static jsonData = null;
    static loadLabelText = ' Select language to load sample';
    static ocrText = null; // element to show OCR result text
    static selectedLanguage = 'eng'; // Default language
    static fileLabel = null;
    static filesArray = []; // Files array for gallery picker

    // The list of registered panels in the main webviewer
    static viewerPanels = null;

    // The tab panel, representing the webviewer left panel
    static tabPanel = {
        handle: null,
        dataElement: 'tabPanel'
    };

    // The custom form sub-panel to be registered
    static formPanel = {
        handle: null,
        dataElement: 'formPanel',
        render: null,
    };

    // Function to set WebViewer 'loading' state
    static setLoading(inst, isLoading) {
        if (isLoading) {
            inst.UI.openElements(['loadingModal']);
        } else {
            inst.UI.closeElements(['loadingModal']);
        }
    }

    static setButtonStyle(button) {
        button.style.margin = '10px';
        button.style.padding = '5px 10px';
        button.style.border = '1px solid #ccc';
        button.style.borderRadius = '4px';
        button.style.cursor = 'pointer';
        button.style.fontSize = '14px';
        button.style.fontWeight = 'bold';
        button.style.transition = 'all 0.2s ease';
        button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
        button.style.color = 'white';
        button.style.backgroundColor = '#007bff';
    }

    // Customize the main webviewer left panel after the load completion
    static customizeUI(instance, performOCR) {
        const { UI } = instance;

        // close the tab panel (if it's open) for refreshment.
        UI.closeElements([this.tabPanel.dataElement]);

        // Get the list of registered panels in the main webviewer
        this.viewerPanels = UI.getPanels();

        // Find the Tab Panel to modify. The form sub-panel will be added to this Tab panel.
        this.tabPanel.handle = this.viewerPanels.find((panel) => panel.dataElement === this.tabPanel.dataElement);

        // Register the custom form sub-panel
        this.RegisterFormPanel(instance);

        const languageLabel = document.createElement('h3');
        languageLabel.textContent = "Select Language:";
        this.formPanel.render.appendChild(languageLabel);

        const languageSelect = document.createElement('select');
        languageSelect.id = 'languageSelect';
        for (const [langCode, langName] of Object.entries(this.languages)) {
            const option = document.createElement('option');
            option.value = langCode;
            option.textContent = langName;
            languageSelect.appendChild(option);
        }
        languageSelect.value = this.selectedLanguage;
        languageSelect.onchange = () => {
            this.selectedLanguage = languageSelect.value;
            if (this.loadedFromPicker) // if the document is loaded from file picker once in the past, do not load using list again
                return;
            this.loadingFromList = true;
            instance.UI.loadDocument(this.demoFilesPath + this.ocrdemofiles[this.selectedLanguage]);
        };
        this.formPanel.render.appendChild(languageSelect);

        const loadLabel = document.createElement('label');
        loadLabel.textContent = this.loadLabelText;
        this.formPanel.render.appendChild(loadLabel);

        this.fileLabel = document.createElement('h4');
        this.fileLabel.textContent = `Current file: ${instance.Core.documentViewer.getDocument().getFilename()}`;
        this.formPanel.render.appendChild(this.fileLabel);

        // OCR button
        const recognizeButton = document.createElement('button');
        recognizeButton.textContent = 'Recognize Text';
        recognizeButton.id = 'recognizeTextButton';
        this.setButtonStyle(recognizeButton);

        recognizeButton.onclick = async () => {
            this.ocrText.textContent = "Performing OCR, please wait...";
            this.setLoading(instance, true);
            this.formPanel.render.style.cursor = "wait";
            await performOCR(instance); // Perform text recognition
            this.formPanel.render.style.cursor = "default";
        }
        this.formPanel.render.appendChild(recognizeButton);

        let jsonDiv = document.createElement('div');
        jsonDiv.id = 'json';
        const jsonTitle = document.createElement('h3');
        jsonTitle.textContent = "Exportable text";
        jsonDiv.appendChild(jsonTitle);
        jsonDiv.appendChild(document.createElement('p'));

        this.jsonData = null;
        this.ocrText = document.createElement('textarea');
        this.ocrText.style.width = "350px";
        this.ocrText.style.height = "350px";
        this.ocrText.style.whiteSpace = "nowrap";
        this.ocrText.readOnly = true;
        this.ocrText.style.width = "350px";
        this.ocrText.style.height = "350px";
        this.ocrText.style.whiteSpace = "nowrap";
        this.ocrText.readOnly = true;
        this.ocrText.textContent = "Try applying OCR to this document\nby pressing the button above!";
        jsonDiv.appendChild(this.ocrText);

        this.formPanel.render.appendChild(jsonDiv);

        // Add the new custom form sub-panel to list of sub-panels under the Tab Panel
        this.formPanel.handle = { render: this.formPanel.dataElement };
        this.tabPanel.handle.panelsList = [this.formPanel.handle, ...this.tabPanel.handle.panelsList];

        UI.openElements([this.tabPanel.dataElement]);
        UI.setPanelWidth(this.tabPanel.dataElement, 400);
    };

    // Register the custom form sub-panel
    static RegisterFormPanel(instance) {
        this.formPanel.render = this.createFormPanelElements(instance);
        instance.UI.addPanel({
            dataElement: this.formPanel.dataElement,
            location: 'left',
            icon: '<svg width="18px" height="18px" viewBox="0 0 24 24" id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#080808;}</style></defs><title>form</title><path class="cls-1" d="M21,.5H3a2,2,0,0,0-2,2V22a2,2,0,0,0,2,2H21a2,2,0,0,0,2-2V2.5A2,2,0,0,0,21,.5Zm0,2v2H3v-2ZM3,22V6.5H21V22Z"/><path class="cls-1" d="M12.5,4H20a.5.5,0,0,0,0-1H12.5a.5.5,0,0,0,0,1Z"/><path class="cls-1" d="M4.5,4a.43.43,0,0,0,.19,0,.35.35,0,0,0,.16-.11A.47.47,0,0,0,5,3.5a.43.43,0,0,0,0-.19.36.36,0,0,0-.11-.16.5.5,0,0,0-.7,0A.35.35,0,0,0,4,3.31.43.43,0,0,0,4,3.5a.51.51,0,0,0,.5.5Z"/><path class="cls-1" d="M5.65,3.85A.36.36,0,0,0,5.81,4,.44.44,0,0,0,6,4a.47.47,0,0,0,.35-.15.36.36,0,0,0,.11-.16.6.6,0,0,0,0-.19.51.51,0,0,0-.15-.35A.49.49,0,0,0,5.81,3a.36.36,0,0,0-.16.11.47.47,0,0,0-.15.35.4.4,0,0,0,0,.19A.35.35,0,0,0,5.65,3.85Z"/><path class="cls-1" d="M8,8H4.5a1,1,0,0,0,0,2H8A1,1,0,0,0,8,8Z"/><path class="cls-1" d="M8,11.67H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M8,15.33H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M8,19H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,8H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,11.67H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,15.33H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,19H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,8h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,11.67h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,15.33h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,19h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/></svg>',
            title: 'Form',
            render: () => this.formPanel.render,
        });
    };

    // Create the form panel elements.
    static createFormPanelElements() {
        let panelDiv = document.createElement('div');
        panelDiv.id = 'form';
        let paragraph = document.createTextNode('A demo of Apryse SDK OCR. Take a PDF and find text. (Note: This demo only processes the current page).');
        panelDiv.appendChild(paragraph);

        let dividerDiv = document.createElement('div');
        dividerDiv.style.borderTop = '1px solid #ccc';
        dividerDiv.style.margin = '10px 0';
        panelDiv.appendChild(dividerDiv);

        panelDiv.appendChild(document.createElement('p'));
        return panelDiv;
    }
}

```

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


---

# 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-ocr-module.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.
