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.
This demo allows you to:
To add OCR Module capability with Server SDK, and view with WebViewer:
Step 1: Follow get-started in JavaScript for Server SDK
Step 2: Follow get-started in your preferred web stack for WebViewer
Step 3: Download OCR Module
Step 4: Add the ES6 JavaScript sample code provided in this guide
Note: Only the first page is processed in this demo.
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0 - GPT-4 Model - September 24, 2024
3// File: ocr-module/client/index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7function initializeWebViewer() {
8
9 console.log('Initializing WebViewer...');
10 const viewerElement = document.getElementById('viewer');
11 if (!viewerElement) {
12 console.error('❌ Viewer element not found. Please ensure there is a div with id "viewer" in your HTML.');
13 return;
14 }
15
16 WebViewer({
17 path: '/lib',
18 initialDoc: UIElements.demoFilesPath + UIElements.ocrdemofiles['eng'],
19 enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File.
20 enableMeasurement: false,
21 loadAsPDF: true,
22 licenseKey: 'YOUR_LICENSE_KEY',
23 }, viewerElement).then(instance => {
24
25 // Once the PDF document is loaded, send it to the server.
26 // The sent PDF document will be processed by the server,
27 // by extracting form fields JSON data when the user clicks the Detect Form Fields button.
28 instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
29 if (!UIElements.loadingFromList) {
30 UIElements.loadedFromPicker = true;
31 UIElements.loadLabelText = ' Use file picker to load your own file';
32 }
33
34 // If next time we load from list, this will be set to true again.
35 UIElements.loadingFromList = false;
36
37 // Customize the main webviewer left panel after loading completes.
38 UIElements.customizeUI(instance, performOCR);
39 });
40 console.log('✅ WebViewer loaded successfully.');
41 }).catch((error) => {
42 console.error('Failed to initialize WebViewer:', error);
43 });
44}
45
46// Perform OCR by sending the current PDF page to the server.
47const performOCR = async (instance) => {
48 // Reset JSON data
49 UIElements.jsonData = null;
50 let resultText = '';
51
52 // Preparation of the PDF blob to be sent to the server.
53 const doc = instance.Core.documentViewer.getDocument();
54 const currentPage = instance.Core.documentViewer.getCurrentPage();
55 const xfdfString = await instance.Core.annotationManager.exportAnnotations(); // obtaining annotations in the loaded document
56 const data = await doc.getFileData({ xfdfString });
57 const arr = new Uint8Array(data);
58 const blob = new Blob([arr], { type: 'application/pdf' });
59 const formData = new FormData();
60 formData.append(doc.filename, blob, doc.filename);
61 // Send the PDF blob to the server for processing.
62 new Promise(function (resolve, reject) {
63 console.log('🚀 Sending PDF to server for processing...');
64 fetch(`http://localhost:5050/server/handler.js?filename=${doc.filename}¤tPage=${currentPage}&lang=${UIElements.selectedLanguage}`, {
65 method: 'POST',
66 body: formData,
67 }).then(function (response) {
68
69 if (response.status === 200) {
70 response.text().then(function (json) {
71 UIElements.jsonData = json;
72 const ocrResult = JSON.parse(UIElements.jsonData);
73
74 for (const page of ocrResult.Page) {
75 for (const para of page.Para) {
76 for (const line of para.Line) {
77 for (const word of line.Word) {
78 resultText += word.text + ' ';
79 }
80 resultText += '\n';
81 }
82 }
83 }
84 resolve();
85 })
86 } else {
87 const errorText = `❌ Server responded with status: ${response.status}`;
88 resultText = errorText + resultText;
89 console.error(resultText);
90 reject(new Error(`Server error: ${response.status}`));
91 }
92 }).catch(function (error) {
93 let errorText = '❌ Failed to connect to server: ' + error;
94 errorText += '\n📍 Attempted URL: http://localhost:5050/server/handler.js';
95 errorText += '\n🔍 This likely means the OCR server is not running on port 5050';
96 console.error(errorText);
97 resultText = errorText + resultText;
98 reject(error);
99 });
100 }).catch(function (error) {
101 const errorText = '❌ Error in PDF upload promise: ' + error;
102 console.error(errorText);
103 resultText = errorText + resultText;
104 }).finally(function () {
105 UIElements.ocrText.textContent = resultText;
106 UIElements.setLoading(instance, false);
107 });
108}
109
110
111function loadUIElementsScript() {
112 return new Promise((resolve, reject) => {
113 if (window.UIElements) {
114 console.log('UIElements already loaded');
115 resolve();
116 return;
117 }
118
119 const script = document.createElement('script');
120 script.src = '/showcase-demos/ocr-module/client/ui-elements.js';
121 script.onload = function () {
122 console.log('✅ UIElements script loaded successfully');
123 resolve();
124 };
125 script.onerror = function () {
126 console.error('Failed to load UIElements script');
127 reject(new Error('Failed to load ui-elements.js'));
128 };
129 document.head.appendChild(script);
130 });
131}
132
133// Load UIElements script first, then initialize WebViewer.
134loadUIElementsScript().then(() => {
135 initializeWebViewer();
136}).catch((error) => {
137 console.error('Failed to load UIElements:', error);
138});
1const { PDFNet } = require('@pdftron/pdfnet-node');
2const path = require('path');
3const fs = require('fs');
4
5// **Important**
6// You must get a license key from Apryse for the server to run.
7// A trial key can be obtained from:
8// https://docs.apryse.com/core/guides/get-started/trial-key
9const licenseKey = 'YOUR_LICENSE_KEY';
10const multer = require('multer');
11const { response } = require('express');
12const upload = multer();
13const serverFolder = 'server';
14const sentImages = 'sentImages';
15const serverHandler = `/${serverFolder}/handler.js`;
16
17module.exports = async (app) => {
18
19 async function initializeServer() {
20 const ocrOK = await PDFNet.OCRModule.isModuleAvailable();
21 if (!ocrOK) {
22 console.log('\nUnable to run OCR Demo: Apryse SDK OCR module not available.');
23 console.log('---------------------------------------------------------------');
24 console.log('The OCR module is an optional add-on, available for download');
25 console.log('at https://docs.apryse.com/core/guides/info/modules#ocr-module . If you have already downloaded this');
26 console.log('module, ensure that the SDK is able to find the required files');
27 console.log('using the PDFNet.addResourceSearchPath() function.\n');
28 }
29 else {
30 console.log('Apryse SDK OCR module is available.');
31 if (!fs.existsSync(sentImages))
32 fs.mkdirSync(sentImages);
33 }
34 }
35
36 // Handle POST request sent to '/server/handler.js'.
37 // This endpoint receives the currently loaded PDF file in the Apryse webviewer, converts the current page to an image,
38 // recognizes text from the image, then sends it back to the client as JSON data.
39 app.post(serverHandler, upload.any(), async (request, response) => {
40 const sentPdf = path.resolve(__dirname, `./${sentImages.split('/').pop()}/${request.query.filename}`);
41 const currentPage = request.query.currentPage ? parseInt(request.query.currentPage) : 1;
42 const draw = await PDFNet.PDFDraw.create(); // PDFDraw class is used to rasterize PDF pages.
43 const doc = await PDFNet.PDFDoc.createFromBuffer(request.files[0].buffer);
44 draw.setDPI(300);
45 const ocrPage = await (await doc.getPageIterator(currentPage)).current();
46 const imageName = sentPdf + `_page${currentPage}.png`;
47 await draw.export(ocrPage, imageName);
48
49 const opts = new PDFNet.OCRModule.OCROptions();
50 const useIRIS = await PDFNet.OCRModule.isIRISModuleAvailable();
51 if(useIRIS) opts.setOCREngine('iris');
52 let json = null;
53 response.header('Content-Type', 'application/json');
54 try {
55 const doc = await PDFNet.PDFDoc.create();
56 opts.addLang(request.query.lang || 'eng');
57 json = await PDFNet.OCRModule.getOCRJsonFromImage(doc, imageName, opts);
58 await fs.promises.unlink(imageName); // Delete the image after OCR
59 //json = await PDFNet.OCRModule.getOCRJsonFromPDF(doc, opts);
60 response.status(200).send(json);
61 } catch (e) {
62 response.status(500).send(`Error extracting JSON text from PDF file ${request.query.filename}`);
63 }
64 });
65
66 // Initialize PDFNet.
67 PDFNet.runWithoutCleanup(initializeServer, licenseKey).then(
68 function onFulfilled() {
69 response.status(200);
70 },
71 function onRejected(error) {
72 // Log error and close response.
73 console.error('Error initializing PDFNet', error);
74 response.status(503).send();
75 }
76 );
77};
1
2const express = require('express');
3const fs = require('fs');
4const bodyParser = require('body-parser');
5const open = (...args) => import('open').then(({ default: open }) => open(...args));
6const handler = require('./handler.js');
7const port = process.env.PORT || 5050;
8const app = express();
9const sentPdfs = 'sentPdfs';
10
11// CORS middleware to allow cross-origin requests from the playground.
12app.use((req, res, next) => {
13 res.header('Access-Control-Allow-Origin', '*');
14 res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
15 res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
16
17 // Handle preflight OPTIONS requests.
18 if (req.method === 'OPTIONS') {
19 res.sendStatus(200);
20 } else {
21 next();
22 }
23});
24
25app.use(bodyParser.text());
26app.use('/client', express.static('../client')); // For statically serving 'client' folder at '/'.
27
28handler(app);
29
30// Run server.
31const server = app.listen(port, 'localhost', (err) => {
32 if (err) {
33 console.error(err);
34 } else {
35 console.info(`Server is listening at http://localhost:${port}`);
36
37 }
38});
39
40// Server shutdown and cleanup.
41function shutdown() {
42 console.log('Cleanup started...');
43
44 // Example: Close server
45 server.close(() => {
46 console.log('Server closed.');
47
48 // Removes sent PDFs folder.
49 if (fs.existsSync(sentPdfs))
50 fs.rmdirSync(sentPdfs, { recursive: true });
51
52 // If no async cleanup, exit directly.
53 process.exit(0);
54 });
55}
56
57// Handle shutdown signals.
58process.on('SIGINT', shutdown); // Ctrl+C
59process.on('SIGTERM', shutdown); // Kill command or Docker stop
60process.on('uncaughtException', (err) => {
61 console.error('Uncaught Exception:', err);
62 shutdown();
63});
1// Class with static UI elements and related functions for the OCR demo.
2
3class UIElements {
4 static languages = {
5 eng: 'English',
6 fra: 'French',
7 spa: 'Spanish',
8 ita: 'Italian',
9 deu: 'German',
10 rus: 'Russian',
11 };
12
13 static demoFilesPath = "https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/";
14 static ocrdemofiles = {
15 eng: 'scanned_sublease_agreement.pdf',
16 fra: 'scanned_sublease_agreement_french.pdf',
17 spa: 'scanned_sublease_agreement_spanish.pdf',
18 ita: 'scanned_sublease_agreement_italian.pdf',
19 deu: 'scanned_sublease_agreement_german.pdf',
20 rus: 'scanned_sublease_agreement_russian.pdf',
21 };
22
23 static loadingFromList = true; // Initially true because we load the first document from the list
24 static loadedFromPicker = false; // to differentiate if the document is loaded from the list or from file picker.
25 static jsonData = null;
26 static loadLabelText = ' Select language to load sample';
27 static ocrText = null; // Element to show OCR result text.
28 static selectedLanguage = 'eng'; // Default language
29 static fileLabel = null;
30 static filesArray = []; // Files array for gallery picker.
31
32 // The list of registered panels in the main webviewer.
33 static viewerPanels = null;
34
35 // The Tab panel, representing the webviewer left panel.
36 static tabPanel = {
37 handle: null,
38 dataElement: 'tabPanel'
39 };
40
41 // The custom form sub-panel to be registered.
42 static formPanel = {
43 handle: null,
44 dataElement: 'formPanel',
45 render: null,
46 };
47
48 // Function to set WebViewer 'loading' state.
49 static setLoading(inst, isLoading) {
50 if (isLoading) {
51 inst.UI.openElements(['loadingModal']);
52 } else {
53 inst.UI.closeElements(['loadingModal']);
54 }
55 }
56
57 static setButtonStyle(button) {
58 button.style.margin = '10px';
59 button.style.padding = '5px 10px';
60 button.style.border = '1px solid #ccc';
61 button.style.borderRadius = '4px';
62 button.style.cursor = 'pointer';
63 button.style.fontSize = '14px';
64 button.style.fontWeight = 'bold';
65 button.style.transition = 'all 0.2s ease';
66 button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
67 button.style.color = 'white';
68 button.style.backgroundColor = '#007bff';
69 }
70
71 // Customize the main webviewer left panel after the load completion.
72 static customizeUI(instance, performOCR) {
73 const { UI } = instance;
74
75 // Close the tab panel (if it's open) for refreshment.
76 UI.closeElements([this.tabPanel.dataElement]);
77
78 // Get the list of registered panels in the main webviewer.
79 this.viewerPanels = UI.getPanels();
80
81 // Find the Tab Panel to modify. The form sub-panel will be added to this Tab panel.
82 this.tabPanel.handle = this.viewerPanels.find((panel) => panel.dataElement === this.tabPanel.dataElement);
83
84 // Register the custom form sub-panel.
85 this.RegisterFormPanel(instance);
86
87 const languageLabel = document.createElement('h3');
88 languageLabel.textContent = "Select Language:";
89 this.formPanel.render.appendChild(languageLabel);
90
91 const languageSelect = document.createElement('select');
92 languageSelect.id = 'languageSelect';
93 for (const [langCode, langName] of Object.entries(this.languages)) {
94 const option = document.createElement('option');
95 option.value = langCode;
96 option.textContent = langName;
97 languageSelect.appendChild(option);
98 }
99 languageSelect.value = this.selectedLanguage;
100 languageSelect.onchange = () => {
101 this.selectedLanguage = languageSelect.value;
102 if (this.loadedFromPicker) // If the document is loaded from file picker once in the past, do not load using list again.
103 return;
104 this.loadingFromList = true;
105 instance.UI.loadDocument(this.demoFilesPath + this.ocrdemofiles[this.selectedLanguage]);
106 };
107 this.formPanel.render.appendChild(languageSelect);
108
109 const loadLabel = document.createElement('label');
110 loadLabel.textContent = this.loadLabelText;
111 this.formPanel.render.appendChild(loadLabel);
112
113 this.fileLabel = document.createElement('h4');
114 this.fileLabel.textContent = `Current file: ${instance.Core.documentViewer.getDocument().getFilename()}`;
115 this.formPanel.render.appendChild(this.fileLabel);
116
117 // OCR button
118 const recognizeButton = document.createElement('button');
119 recognizeButton.textContent = 'Recognize Text';
120 recognizeButton.id = 'recognizeTextButton';
121 this.setButtonStyle(recognizeButton);
122
123 recognizeButton.onclick = async () => {
124 this.ocrText.textContent = "Performing OCR, please wait...";
125 this.setLoading(instance, true);
126 this.formPanel.render.style.cursor = "wait";
127 await performOCR(instance); // Perform text recognition.
128 this.formPanel.render.style.cursor = "default";
129 }
130 this.formPanel.render.appendChild(recognizeButton);
131
132 let jsonDiv = document.createElement('div');
133 jsonDiv.id = 'json';
134 const jsonTitle = document.createElement('h3');
135 jsonTitle.textContent = "Exportable text";
136 jsonDiv.appendChild(jsonTitle);
137 jsonDiv.appendChild(document.createElement('p'));
138
139 this.jsonData = null;
140 this.ocrText = document.createElement('textarea');
141 this.ocrText.style.width = "350px";
142 this.ocrText.style.height = "350px";
143 this.ocrText.style.whiteSpace = "nowrap";
144 this.ocrText.readOnly = true;
145 this.ocrText.style.width = "350px";
146 this.ocrText.style.height = "350px";
147 this.ocrText.style.whiteSpace = "nowrap";
148 this.ocrText.readOnly = true;
149 this.ocrText.textContent = "Try applying OCR to this document\nby pressing the button above!";
150 jsonDiv.appendChild(this.ocrText);
151
152 this.formPanel.render.appendChild(jsonDiv);
153
154 // Add the new custom form sub-panel to list of sub-panels under the Tab Panel.
155 this.formPanel.handle = { render: this.formPanel.dataElement };
156 this.tabPanel.handle.panelsList = [this.formPanel.handle, ...this.tabPanel.handle.panelsList];
157
158 UI.openElements([this.tabPanel.dataElement]);
159 UI.setPanelWidth(this.tabPanel.dataElement, 400);
160 };
161
162 // Register the custom form sub-panel.
163 static RegisterFormPanel(instance) {
164 this.formPanel.render = this.createFormPanelElements(instance);
165 instance.UI.addPanel({
166 dataElement: this.formPanel.dataElement,
167 location: 'left',
168 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>',
169 title: 'Form',
170 render: () => this.formPanel.render,
171 });
172 };
173
174 // Create the form panel elements.
175 static createFormPanelElements() {
176 let panelDiv = document.createElement('div');
177 panelDiv.id = 'form';
178 let paragraph = document.createTextNode('A demo of Apryse SDK OCR. Take a PDF and find text. (Note: This demo only processes the current page).');
179 panelDiv.appendChild(paragraph);
180
181 let dividerDiv = document.createElement('div');
182 dividerDiv.style.borderTop = '1px solid #ccc';
183 dividerDiv.style.margin = '10px 0';
184 panelDiv.appendChild(dividerDiv);
185
186 panelDiv.appendChild(document.createElement('p'));
187 return panelDiv;
188 }
189}
190
191
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales