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:

passport scanner 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

Demo: DLS2eyJvcmdhbml6YXeyJo34567AwMDAxLTEwM34XphdGlvbk812345AxIn0=
Reveal

This generates an online license that collects data regarding your usage of the SDK. If you prefer an offline license key, please contact us.​

Your Trial License

DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTEwMTIwMDkzNiIsIm9yZ2FuaXphdGlvbklEIjoiMjAwMDAxIn0=
Copy
Copied!

Request a Trial License - Successful

The license will be sent to your email immediately. If you don't see it in your inbox, please check your junk/spam folder as well.

New Project

  1. Create a new project.

    npm init
    
  2. Install Electron.

    npm install electron --save-dev
    
  3. 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>
    
  4. 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()
      }
    })
    
  5. 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.

  1. 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>
    
  2. 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);
      }
    }
    
  3. 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;
    }
    
  4. 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.

  1. Initialize the license of the Dynamsoft SDK.

    function initLicense(){
        LicenseManager.initLicense('LICENSE-KEY');
    }
    
  2. 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);
    }
    
  3. 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;
    }
    
  4. 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