Electron Passport Scanner Desktop App
Electron is a framework for building desktop applications using JavaScript, HTML, and CSS. As web technologies have emerged as the best choice for building user interfaces, more and more developers are adopting Electron to build their applications.
In this article, we are going to create an Electron desktop app to scan passports with Dynamsoft Label Recognizer to showcase the technologies.
Screenshot:
We can see the MRZ (machine-readable zone) is recognized and the owner’s info like name and nationality is extracted.
Prerequisites
Get your trial key.
Request a Trial License
This generates an online license that collects data regarding your usage of the SDK. If you prefer an offline license key, please contact us.
New Project
-
Create a new project.
npm init
-
Install Electron.
npm install electron --save-dev
-
Create an
index.html
file:<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" /> <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'" /> <title>Passport Scanner</title> </head> <body> <h1>Passport Scanner</h1> </body> </html>
-
Create a
main.js
file as the entry of the project to start the Electron application.const { app, BrowserWindow } = require('electron/main') const createWindow = () => { const win = new BrowserWindow({ width: 800, height: 600 }) win.loadFile('index.html') } app.whenReady().then(() => { createWindow() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } }) }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } })
-
Run
npx electron .
to start the program.
Add Dependencies
Install the node package of Dynamsoft to add the ability to recognize the MRZ text from the passport image.
npm install dynamsoft-capture-vision-for-node dynamsoft-capture-vision-for-node-charactermodel
Open the Camera
Next, open the connected camera using getUserMedia
.
-
Add elements in the HTML file.
<div> <label> Camera: <select id="select-camera"></select> </label> <button id="button-start">Start Camera</button> </div> <div class="camera-container"> <video id="camera" autoplay playsinline></video> </div>
-
Ask for camera permission.
async function askForPermissions(){ var stream; try { var constraints = {video: true, audio: false}; //ask for camera permission stream = await navigator.mediaDevices.getUserMedia(constraints); } catch (error) { console.log(error); } closeStream(stream); } function closeStream(stream){ try{ if (stream){ stream.getTracks().forEach(track => track.stop()); } } catch (e){ alert(e.message); } }
-
List camera devices.
async function listDevices(){ devices = await getCameraDevices() for (let index = 0; index < devices.length; index++) { const device = devices[index]; camSelect.appendChild(new Option(device.label ?? "Camera "+index,device.deviceId)); } } async function getCameraDevices(){ await askForPermissions(); var allDevices = await navigator.mediaDevices.enumerateDevices(); var cameraDevices = []; for (var i=0;i<allDevices.length;i++){ var device = allDevices[i]; if (device.kind == 'videoinput'){ cameraDevices.push(device); } } return cameraDevices; }
-
Start the selected camera.
function startCamera(){ var video = document.getElementById("camera"); var selectedCamera = camSelect.selectedOptions[0].value; var constraints = { audio:false, video:true } if (selectedCamera) { constraints = { video: {deviceId: selectedCamera}, audio: false } } navigator.mediaDevices.getUserMedia(constraints).then(function(camera) { video.srcObject = camera; }).catch(function(error) { alert('Unable to capture your camera. Please check console logs.'); console.error(error); }); }
Capture a Frame as DataURL
Use Canvas to capture a frame as DataURL, which will later be used for MRZ recognition.
HTML:
<div class="result-container">
<canvas id="captured"></canvas>
</div>
JavaScript:
function capture(){
var video = document.getElementById("camera");
var canvas = document.getElementById("captured");
var context = canvas.getContext("2d");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
context.drawImage(video, 0, 0, canvas.width, canvas.height);
var dataurl = canvas.toDataURL("image/jpeg");
}
Recognize the MRZ
Inter-process communication (IPC) is a key part of building feature-rich desktop applications in Electron. In Electron, processes communicate by passing messages through developer-defined “channels” with the ipcMain and ipcRenderer modules.
The node edition of the Dynamsoft SDK works in the main process. We have to pass the DataURL from the renderer to the main and then pass the recognized result from the main to the renderer.
Let’s add functions to main.js
first.
-
Initialize the license of the Dynamsoft SDK.
function initLicense(){ LicenseManager.initLicense('LICENSE-KEY'); }
-
Update the runtime settings for MRZ recognition.
function initSettings(){ let mrzTemplate = `{ "CaptureVisionTemplates": [ { "Name": "ReadPassportAndId", "OutputOriginalImage": 1, "ImageROIProcessingNameArray": ["roi-passport-and-id"], "SemanticProcessingNameArray": ["sp-passport-and-id"], "Timeout": 2000 }, { "Name": "ReadPassport", "OutputOriginalImage": 1, "ImageROIProcessingNameArray": ["roi-passport"], "SemanticProcessingNameArray": ["sp-passport"], "Timeout": 2000 }, { "Name": "ReadId", "OutputOriginalImage": 1, "ImageROIProcessingNameArray": ["roi-id"], "SemanticProcessingNameArray": ["sp-id"], "Timeout": 2000 } ], "TargetROIDefOptions": [ { "Name": "roi-passport-and-id", "TaskSettingNameArray": ["task-passport-and-id"] }, { "Name": "roi-passport", "TaskSettingNameArray": ["task-passport"] }, { "Name": "roi-id", "TaskSettingNameArray": ["task-id"] } ], "TextLineSpecificationOptions": [ { "Name": "tls_mrz_passport", "BaseTextLineSpecificationName": "tls_base", "StringLengthRange": [44, 44], "OutputResults": 1, "ExpectedGroupsCount": 1, "ConcatResults": 1, "ConcatSeparator": "\\n", "SubGroups": [ { "StringRegExPattern": "(P[A-Z<][A-Z<]{3}[A-Z<]{39}){(44)}", "StringLengthRange": [44, 44], "BaseTextLineSpecificationName": "tls_base" }, { "StringRegExPattern": "([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[0-9<]{4}[0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{14}[0-9<][0-9]){(44)}", "StringLengthRange": [44, 44], "BaseTextLineSpecificationName": "tls_base" } ] }, { "Name": "tls_mrz_id_td2", "BaseTextLineSpecificationName": "tls_base", "StringLengthRange": [36, 36], "OutputResults": 1, "ExpectedGroupsCount": 1, "ConcatResults": 1, "ConcatSeparator": "\\n", "SubGroups": [ { "StringRegExPattern": "([ACI][A-Z<][A-Z<]{3}[A-Z<]{31}){(36)}", "StringLengthRange": [36, 36], "BaseTextLineSpecificationName": "tls_base" }, { "StringRegExPattern": "([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[0-9<]{4}[0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{8}){(36)}", "StringLengthRange": [36, 36], "BaseTextLineSpecificationName": "tls_base" } ] }, { "Name": "tls_mrz_id_td1", "BaseTextLineSpecificationName": "tls_base", "StringLengthRange": [30, 30], "OutputResults": 1, "ExpectedGroupsCount": 1, "ConcatResults": 1, "ConcatSeparator": "\\n", "SubGroups": [ { "StringRegExPattern": "([ACI][A-Z<][A-Z<]{3}[A-Z0-9<]{9}[0-9<][A-Z0-9<]{15}){(30)}", "StringLengthRange": [30, 30], "BaseTextLineSpecificationName": "tls_base" }, { "StringRegExPattern": "([0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[0-9<]{4}[0-9][A-Z<]{3}[A-Z0-9<]{11}[0-9]){(30)}", "StringLengthRange": [30, 30], "BaseTextLineSpecificationName": "tls_base" }, { "StringRegExPattern": "([A-Z<]{30}){(30)}", "StringLengthRange": [30, 30], "BaseTextLineSpecificationName": "tls_base" } ] }, { "Name": "tls_base", "CharacterModelName": "MRZ", "CharHeightRange": [5, 1000, 1], "BinarizationModes": [ { "BlockSizeX": 30, "BlockSizeY": 30, "Mode": "BM_LOCAL_BLOCK", "EnableFillBinaryVacancy": 0, "ThresholdCompensation": 15 } ], "ConfusableCharactersCorrection": { "ConfusableCharacters": [ ["0", "O"], ["1", "I"], ["5", "S"] ], "FontNameArray": ["OCR_B"] } } ], "LabelRecognizerTaskSettingOptions": [ { "Name": "task-passport", "ConfusableCharactersPath": "ConfusableChars.data", "TextLineSpecificationNameArray": ["tls_mrz_passport"], "SectionImageParameterArray": [ { "Section": "ST_REGION_PREDETECTION", "ImageParameterName": "ip-mrz" }, { "Section": "ST_TEXT_LINE_LOCALIZATION", "ImageParameterName": "ip-mrz" }, { "Section": "ST_TEXT_LINE_RECOGNITION", "ImageParameterName": "ip-mrz" } ] }, { "Name": "task-id", "ConfusableCharactersPath": "ConfusableChars.data", "TextLineSpecificationNameArray": ["tls_mrz_id_td1", "tls_mrz_id_td2"], "SectionImageParameterArray": [ { "Section": "ST_REGION_PREDETECTION", "ImageParameterName": "ip-mrz" }, { "Section": "ST_TEXT_LINE_LOCALIZATION", "ImageParameterName": "ip-mrz" }, { "Section": "ST_TEXT_LINE_RECOGNITION", "ImageParameterName": "ip-mrz" } ] }, { "Name": "task-passport-and-id", "ConfusableCharactersPath": "ConfusableChars.data", "TextLineSpecificationNameArray": ["tls_mrz_passport", "tls_mrz_id_td1", "tls_mrz_id_td2"], "SectionImageParameterArray": [ { "Section": "ST_REGION_PREDETECTION", "ImageParameterName": "ip-mrz" }, { "Section": "ST_TEXT_LINE_LOCALIZATION", "ImageParameterName": "ip-mrz" }, { "Section": "ST_TEXT_LINE_RECOGNITION", "ImageParameterName": "ip-mrz" } ] } ], "CharacterModelOptions": [ { "Name": "MRZ" } ], "ImageParameterOptions": [ { "Name": "ip-mrz", "TextureDetectionModes": [ { "Mode": "TDM_GENERAL_WIDTH_CONCENTRATION", "Sensitivity": 8 } ], "BinarizationModes": [ { "EnableFillBinaryVacancy": 0, "ThresholdCompensation": 21, "Mode": "BM_LOCAL_BLOCK" } ], "TextDetectionMode": { "Mode": "TTDM_LINE", "CharHeightRange": [5, 1000, 1], "Direction": "HORIZONTAL", "Sensitivity": 7 } } ], "SemanticProcessingOptions": [ { "Name": "sp-passport-and-id", "ReferenceObjectFilter": { "ReferenceTargetROIDefNameArray": ["roi-passport-and-id"] }, "TaskSettingNameArray": ["dcp-passport-and-id"] }, { "Name": "sp-passport", "ReferenceObjectFilter": { "ReferenceTargetROIDefNameArray": ["roi-passport"] }, "TaskSettingNameArray": ["dcp-passport"] }, { "Name": "sp-id", "ReferenceObjectFilter": { "ReferenceTargetROIDefNameArray": ["roi-id"] }, "TaskSettingNameArray": ["dcp-id"] } ], "CodeParserTaskSettingOptions": [ { "Name": "dcp-passport", "CodeSpecifications": ["MRTD_TD3_PASSPORT"] }, { "Name": "dcp-id", "CodeSpecifications": ["MRTD_TD1_ID", "MRTD_TD2_ID"] }, { "Name": "dcp-passport-and-id", "CodeSpecifications": ["MRTD_TD3_PASSPORT", "MRTD_TD1_ID", "MRTD_TD2_ID"] } ] }`; CaptureVisionRouter.initSettings(mrzTemplate); }
-
Add a function to recognize the MRZ from the image encoded as DataURL.
async function capture(dataurl){ let response = await fetch(dataurl); let bytes = await response.bytes(); let result = await CaptureVisionRouter.captureAsync(bytes, "ReadPassport"); let jsonStr = ""; if (result.parsedResultItems.length > 0) { let parsedResultItem = result.parsedResultItems[0]; jsonStr = JSON.stringify(parsedResultItem.parsed); } return jsonStr; }
-
Receive the DataURL message from the renderer and send the parsed result back to it.
const createWindow = () => { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { devTools: true, preload: path.join(__dirname, 'preload.js') } }) ipcMain.on('capture', async (event, dataurl) => { const webContents = event.sender const result = await capture(dataurl); webContents.send('update-result', result); }) win.loadFile('index.html') }
In preload.js
, define the functions.
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('Dynamsoft', {
onCaptured: (callback) => ipcRenderer.on('update-result', (_event, value) => callback(value)),
capture: (dataurl) => ipcRenderer.send('capture', dataurl)
})
In index.js
(the renderer process), use the following function to send the DataURL message:
window.Dynamsoft.capture(dataurl);
Then, receive the parsed result with the following function:
window.Dynamsoft.onCaptured((value) => {
let fields = {};
let parsed = JSON.parse(value);
})
All right, we’ve covered the key parts of the demo.
Source Code
The source code of the project is available here: https://github.com/tony-xlh/electron-passport-scanner