How to Build a Passport Scanner Desktop App with Electron and MRZ Recognition
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.

We can see the MRZ (machine-readable zone) is recognized and the owner’s info like name and nationality is extracted.
This article is Part 5 in a 5-Part Series.
- Part 1 - How to Recognize MRZ from Passport and ID Card with Node.js
- Part 2 - Building a Barcode and QR Code Detection Module for Node-RED with JavaScript
- Part 3 - How to Build a Node.js Barcode Reader for Command-Line and Web Applications
- Part 4 - How to Read 1D/2D Barcodes from Multi-Page PDFs in Node.js
- Part 5 - How to Build a Passport Scanner Desktop App with Electron and MRZ Recognition
What you’ll build: A cross-platform Electron desktop app that opens a live camera stream, captures passport frames, and uses Dynamsoft Label Recognizer (Node.js) to parse MRZ fields such as name, nationality, and document number — all running locally without a server.
Key Takeaways
- Electron’s IPC architecture (ipcMain / ipcRenderer) lets you run the Dynamsoft Node.js SDK in the main process while keeping the camera UI in the renderer.
- Dynamsoft Label Recognizer recognizes MRTD TD1, TD2, and TD3 (passport) MRZ formats from a single
captureAsynccall. 3.6.4003ships a ready-to-useTemplates/MRZScanner.json; load it withCaptureVisionRouter.initSettings()instead of describing the MRZ pipeline by hand.- All processing happens locally on-device — no cloud API or network round-trip required.
- The approach works on Windows, macOS, and Linux wherever Electron and Node.js run.
Common Developer Questions
How do I build a passport scanner desktop app with Electron and JavaScript?
Use Electron for the desktop shell, open the camera in the renderer process, and send captured frames to the main process through IPC so the Node.js MRZ SDK can decode them. That architecture keeps the UI responsive while the heavier passport parsing work stays in the privileged process.
How do I recognize MRZ text from a live camera feed in an Electron app?
Start a webcam stream with getUserMedia, draw frames to a canvas or export snapshots, and hand those image buffers to Dynamsoft’s Node.js recognition API for MRZ parsing. The decoded fields can then be pushed back into the renderer and shown alongside the preview.
How do I pass image data from the Electron renderer process to the main process for OCR?
Serialize the captured image in the renderer, send it over an ipcRenderer channel, and handle it in ipcMain where the Node.js SDK is available. This is the standard Electron pattern when OCR libraries need filesystem or native-process access that the renderer should not own.
Prerequisites
Get your trial key to activate Dynamsoft Label Recognizer.
Step 1: Create a New Electron Project
-
Create a new project.
npm init -
Install Electron.
npm install electron --save-dev -
Create an
index.htmlfile:<!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.jsfile 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.
Step 2: Install Dynamsoft MRZ Dependencies
Install the Dynamsoft Capture Vision packages: the SDK itself plus dynamsoft-capture-vision-for-node-model, which carries the MRZ neural-network models. The SDK also ships the MRZScanner.json template that declares those models.
npm install dynamsoft-capture-vision-for-node@3.6.4003 dynamsoft-capture-vision-for-node-model@3.6.4000 -E
Step 3: Set Up the Live Camera Stream
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); }); }
Step 4: 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");
}
Step 5: Recognize the MRZ via IPC
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 and load the MRZ templates when the main process starts.
const path = require('node:path'); const { LicenseManager, CaptureVisionRouter } = require('dynamsoft-capture-vision-for-node'); function initDCV(){ LicenseManager.initLicense('LICENSE-KEY'); // The npm package ships MRZScanner.json together with the MRZ models it declares. const packageRoot = path.dirname( path.dirname(require.resolve('dynamsoft-capture-vision-for-node')) ); CaptureVisionRouter.initSettings(path.join(packageRoot, 'Templates', 'MRZScanner.json')); } initDCV();3.6.4003bundlesTemplates/MRZScanner.json, which definesReadPassport,ReadPassportAndId,ReadId,ReadVisa, andReadMRZtogether with the model declarations they need. Hand-written label-recognizer templates from older releases no longer load: the 3.6 engine rejects theirCharacterModelOptionsandImageParameterOptionskeys withSkipped unsupported keys(dcvErrorCode -10077). -
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.
Common Issues & Edge Cases
- Camera permission denied on macOS/Windows: Electron’s
BrowserWindowdoes not inherit system camera permissions automatically. Callnavigator.mediaDevices.getUserMediaand handle theNotAllowedError— prompt the user to grant camera access in OS settings. - MRZ not detected under poor lighting: Dynamsoft Label Recognizer uses local-block binarization. If recognition fails consistently, ensure the passport is well-lit and held flat; oblique angles or strong reflections on the laminate surface reduce accuracy.
Skipped unsupported keys(dcvErrorCode -10077): a hand-written label-recognizer template from an older DCV release is being loaded. The 3.6 engine no longer acceptsCharacterModelOptions,ImageParameterOptions, orTextDetectionMode; load the bundledTemplates/MRZScanner.jsoninstead.captureAsyncreturns empty results for non-passport documents: The template used in this demo targets TD3 (passport) format by default. Switch toReadIdorReadPassportAndIdtemplate name to handle national ID cards in TD1/TD2 format.- App exits with code
139on Linux after a scan:dynamsoft-capture-vision-for-node3.6.4000-3.6.4002segfaulted while the process was being torn down after an MRZ capture. The recognition result was correct and the app kept running, but quitting ended with a crash report. Fixed in3.6.4003— pin at least that version on Linux.
Frequently Asked Questions
How do I build a passport scanner desktop app with Electron?
Use Electron’s main/renderer IPC pattern: run the Dynamsoft Label Recognizer Node.js SDK in the main process, capture a canvas frame in the renderer, send it over ipcRenderer, and return the parsed MRZ JSON via webContents.send.
How accurate is Dynamsoft Label Recognizer for MRZ reading?
Dynamsoft Label Recognizer applies OCR-B font modeling and confusable-character correction (e.g., 0/O, 1/I, 5/S) specifically tuned for ICAO MRTD documents, yielding high accuracy on standard travel documents under typical office lighting.
Can I use this Electron MRZ scanner on Linux?
Yes. The dynamsoft-capture-vision-for-node package ships native binaries for Windows, macOS, and Linux (x64/arm64), and Electron runs on all three platforms. Use 3.6.4003 or newer: earlier 3.6.400x builds segfaulted while the process was exiting after an MRZ capture on Linux.
Source Code
The full source code for this Electron passport scanner is available on GitHub: Get the complete sample project source code on GitHub