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, with the addition of showing the resulting file in WebViewer. If a viewer is not needed, please check out our Server SDK OCR Sample Code
Note: Only the first page is processed in this demo.
This demo allows you to:
To add OCR Module capability with Server SDK, and view with WebViewer:
Step 1: Get started in Server SDK language or framework of your choice
Step 2: Follow get-started in your preferred web stack for WebViewer
Step 3: Download OCR Module
Step 4: Add the sample code provided in this guide
To use this feature in production, your license key will need theĀ OCR Package. Trial keys already include all packages.
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0 - GPT-4 Model - September 24, 2024
3// File: ocr-module/client/index.js
4
5// **Important**
6// You must get a license key from Apryse to run the WebViewer Server SDK.
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 viewerElement = document.getElementById('viewer');
11
12const languages = {
13 eng: 'English',
14 fra: 'French',
15 spa: 'Spanish',
16 ita: 'Italian',
17 deu: 'German',
18 rus: 'Russian',
19};
20
21const demoFilesPath = "https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/";
22const ocrdemofiles = {
23 eng: 'scanned_sublease_agreement.pdf',
24 fra: 'scanned_sublease_agreement_french.pdf',
25 spa: 'scanned_sublease_agreement_spanish.pdf',
26 ita: 'scanned_sublease_agreement_italian.pdf',
27 deu: 'scanned_sublease_agreement_german.pdf',
28 rus: 'scanned_sublease_agreement_russian.pdf',
29};
30
31let loadingFromList = true; // initially true because we load the first document from the list
32let loadedFromPicker = false; // to differentiate if the document is loaded from the list or from file picker
33let jsonData = null;
34let loadLabelText = ' Select language to load sample';
35let ocrText = null; // element to show OCR result text
36let selectedLanguage = 'eng'; // Default language
37let fileLabel = null;
38
39// The list of registered panels in the main webviewer
40let viewerPanels = null;
41
42// The tab panel, representing the webviewer left panel
43const tabPanel = {
44 handle: null,
45 dataElement: 'tabPanel'
46};
47
48// The custom form sub-panel to be registered
49const formPanel = {
50 handle: null,
51 dataElement: 'formPanel',
52 render: null,
53};
54
55// Function to set WebViewer 'loading' state
56function setLoading(inst, isLoading) {
57 if (isLoading) {
58 inst.UI.openElements(['loadingModal']);
59 } else {
60 inst.UI.closeElements(['loadingModal']);
61 }
62}
63
64function setButtonStyle(button) {
65 button.style.margin = '10px';
66 button.style.padding = '5px 10px';
67 button.style.border = '1px solid #ccc';
68 button.style.borderRadius = '4px';
69 button.style.cursor = 'pointer';
70 button.style.fontSize = '14px';
71 button.style.fontWeight = 'bold';
72 button.style.transition = 'all 0.2s ease';
73 button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
74 button.style.color = 'white';
75 button.style.backgroundColor = '#007bff';
76}
77
78// Customize the main webviewer left panel after the load completion
79const customizeUI = (instance) => {
80 const { UI } = instance;
81
82 // close the tab panel (if it's open) for refreshment.
83 UI.closeElements([tabPanel.dataElement]);
84
85 // Get the list of registered panels in the main webviewer
86 viewerPanels = UI.getPanels();
87
88 // Find the Tab Panel to modify. The form sub-panel will be added to this Tab panel.
89 tabPanel.handle = viewerPanels.find((panel) => panel.dataElement === tabPanel.dataElement);
90
91 // Register the custom form sub-panel
92 RegisterFormPanel(instance);
93
94 const languageLabel = document.createElement('h3');
95 languageLabel.textContent = "Select Language:";
96 formPanel.render.appendChild(languageLabel);
97
98 const languageSelect = document.createElement('select');
99 languageSelect.id = 'languageSelect';
100 for (const [langCode, langName] of Object.entries(languages)) {
101 const option = document.createElement('option');
102 option.value = langCode;
103 option.textContent = langName;
104 languageSelect.appendChild(option);
105 }
106 languageSelect.value = selectedLanguage;
107 languageSelect.onchange = () => {
108 selectedLanguage = languageSelect.value;
109 if(loadedFromPicker) // if the document is loaded from file picker once in the past, do not load using list again
110 return;
111 loadingFromList = true;
112 instance.UI.loadDocument(demoFilesPath + ocrdemofiles[selectedLanguage]);
113 };
114 formPanel.render.appendChild(languageSelect);
115
116 const loadLabel = document.createElement('label');
117 loadLabel.textContent = loadLabelText;
118 formPanel.render.appendChild(loadLabel);
119
120 fileLabel = document.createElement('h4');
121 fileLabel.textContent = `Current file: ${instance.Core.documentViewer.getDocument().getFilename()}`;
122 formPanel.render.appendChild(fileLabel);
123
124 // OCR button
125 const recognizeButton = document.createElement('button');
126 recognizeButton.textContent = 'Recognize Text';
127 recognizeButton.id = 'recognizeTextButton';
128 setButtonStyle(recognizeButton);
129
130 recognizeButton.onclick = async () => {
131 ocrText.textContent = "Performing OCR, please wait...";
132 setLoading(instance, true);
133 formPanel.render.style.cursor = "wait";
134 await performOCR(instance); // Perform text recognition
135 formPanel.render.style.cursor = "default";
136 }
137 formPanel.render.appendChild(recognizeButton);
138
139 let jsonDiv = document.createElement('div');
140 jsonDiv.id = 'json';
141 const jsonTitle = document.createElement('h3');
142 jsonTitle.textContent = "Exportable text";
143 jsonDiv.appendChild(jsonTitle);
144 jsonDiv.appendChild(document.createElement('p'));
145
146 jsonData = null;
147 ocrText = document.createElement('textarea');
148 ocrText.style.width = "350px";
149 ocrText.style.height = "350px";
150 ocrText.style.whiteSpace = "nowrap";
151 ocrText.readOnly = true;
152 ocrText.style.width = "350px";
153 ocrText.style.height = "350px";
154 ocrText.style.whiteSpace = "nowrap";
155 ocrText.readOnly = true;
156 ocrText.textContent = "Try applying OCR to this document\nby pressing the button above!";
157 jsonDiv.appendChild(ocrText);
158 const openDialogButton = document.createElement('button');
159 setButtonStyle(openDialogButton);
160
161 openDialogButton.textContent = "Open in Dialog";
162 openDialogButton.onclick = () => {
163 if (jsonData) {
164 openJsonDataDialog(ocrText.textContent);
165 }
166 };
167 jsonDiv.appendChild(openDialogButton);
168 formPanel.render.appendChild(jsonDiv);
169
170 // Add the new custom form sub-panel to list of sub-panels under the Tab Panel
171 formPanel.handle = { render: formPanel.dataElement };
172 tabPanel.handle.panelsList = [formPanel.handle, ...tabPanel.handle.panelsList];
173
174 UI.openElements([tabPanel.dataElement]);
175 UI.setPanelWidth(tabPanel.dataElement, 400);
176};
177
178// Register the custom form sub-panel
179const RegisterFormPanel = (instance) => {
180 formPanel.render = createFormPanelElements(instance);
181 instance.UI.addPanel({
182 dataElement: formPanel.dataElement,
183 location: 'left',
184 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>',
185 title: 'Form',
186 render: () => formPanel.render,
187 });
188};
189
190// Create the form panel elements.
191const createFormPanelElements = (instance) => {
192 let panelDiv = document.createElement('div');
193 panelDiv.id = 'form';
194 let paragraph = document.createTextNode('A demo of Apryse SDK OCR. Take a PDF and find text. (Note: This demo only processes the current page).');
195 panelDiv.appendChild(paragraph);
196
197 let dividerDiv = document.createElement('div');
198 dividerDiv.style.borderTop = '1px solid #ccc';
199 dividerDiv.style.margin = '10px 0';
200 panelDiv.appendChild(dividerDiv);
201
202 panelDiv.appendChild(document.createElement('p'));
203 return panelDiv;
204};
205
206// Open JSON data in a viewer with button
207// This viewer will be viewed on top of the PDF document viewer.
208const openJsonDataDialog = (jsonText) => {
209 const dialog = document.createElement('dialog');
210 dialog.style.width = "80%";
211 dialog.style.height = "80%";
212 dialog.style.padding = "10px";
213 dialog.style.border = "1px solid #ccc";
214 dialog.style.borderRadius = "8px";
215 dialog.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.1)";
216 dialog.style.display = "flex";
217 dialog.style.flexDirection = "column";
218 const content = document.createElement('pre');
219 content.style.flex = "1";
220 content.style.overflow = "auto";
221 content.textContent = jsonText;
222 dialog.appendChild(content);
223 const closeButton = document.createElement('button');
224 closeButton.textContent = "Close";
225 setButtonStyle(closeButton);
226 closeButton.onclick = () => {dialog.close(); dialog.remove(); }; //Not only close but also remove from DOM
227 dialog.appendChild(closeButton);
228 formPanel.render.appendChild(dialog);
229 dialog.showModal();
230}
231
232// Perform OCR by sending the current PDF page to the server
233const performOCR = async (instance) => {
234 // Reset JSON data
235 jsonData = null;
236 let resultText = '';
237
238 // Preparation of the PDF blob to be sent to the server
239 const doc = instance.Core.documentViewer.getDocument();
240 const currentPage = instance.Core.documentViewer.getCurrentPage();
241 const xfdfString = await instance.Core.annotationManager.exportAnnotations(); // obtaining annotations in the loaded document
242 const data = await doc.getFileData({ xfdfString });
243 const arr = new Uint8Array(data);
244 const blob = new Blob([arr], { type: 'application/pdf' });
245 const formData = new FormData();
246 formData.append(doc.filename, blob, doc.filename);
247 // Send the PDF blob to the server for processing
248 new Promise(function (resolve, reject) {
249 console.log('š Sending PDF to server for processing...');
250 fetch(`http://localhost:5050/server/handler.js?filename=${doc.filename}¤tPage=${currentPage}&lang=${selectedLanguage}`, {
251 method: 'POST',
252 body: formData,
253 }).then(function (response) {
254
255 if (response.status === 200) {
256 response.text().then(function (json) {
257 jsonData = json;
258 const ocrResult = JSON.parse(jsonData);
259
260 for (const page of ocrResult.Page) {
261 for (const para of page.Para) {
262 for (const line of para.Line) {
263 for (const word of line.Word) {
264 resultText += word.text + ' ';
265 }
266 resultText += '\n';
267 }
268 }
269 }
270 resolve();
271 })
272 } else {
273 const errorText = `ā Server responded with status: ${response.status}`;
274 resultText = errorText + resultText;
275 console.error(resultText);
276 reject(new Error(`Server error: ${response.status}`));
277 }
278 }).catch(function (error) {
279 let errorText = 'ā Failed to connect to server: ' + error;
280 errorText += '\nš Attempted URL: http://localhost:5050/server/handler.js';
281 errorText += '\nš This likely means the OCR server is not running on port 5050';
282 console.error(errorText);
283 resultText = errorText + resultText;
284 reject(error);
285 });
286 }).catch(function (error) {
287 const errorText = 'ā Error in PDF upload promise: ' + error;
288 console.error(errorText);
289 resultText = errorText + resultText;
290 }).finally(function (){
291 ocrText.textContent = resultText;
292 setLoading(instance, false);
293 });
294}
295
296WebViewer({
297 path: '/lib',
298 initialDoc: demoFilesPath + ocrdemofiles['eng'],
299 enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
300 enableMeasurement: false,
301 loadAsPDF: true,
302 licenseKey: licenseKey,
303}, viewerElement).then(instance => {
304
305 // Once the PDF document is loaded, send it to the server.
306 // The sent PDF document will be processed by the server,
307 // by extracting form fields JSON data when the user clicks the "Detect Form Fields" button.
308 instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
309 if(!loadingFromList) {
310 loadedFromPicker = true;
311 loadLabelText = ' Use file picker to load your own file';
312 }
313 loadingFromList = false; // if next time we load from list, this will be set to true again
314 // Customize the main webviewer left panel after loading completes
315 customizeUI(instance);
316 });
317 console.log('ā
WebViewer loaded successfully.');
318}).catch((error) => {
319 console.error('ā Failed to initialize WebViewer:', error);
320});
321
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});
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales