Creating A Cross-platform Barcode Reader App With Electron And Dynamsoft Barcode Reader
Electron, one of the most popular cross-platform desktop application frameworks, has been used in numerous famous apps. With web technology, developers could rapidly build a product for Windows, macOS, and Linux. In this article, we would use Electron to build a barcode decoding and scanning app with Dynamsoft Barcode Reader.
Installation
To create our app, we need the following software/packages/tools.
- Dynamsoft Barcode Reader
- Electron
- npm
- Node.js
- Node-gyp
- electron-rebuild
- GCC/Clang/other C++ compilers
Node.js is the foundation of Electron and node package management. You must install Node.js in your host before continuing the development work.
Dynamsoft Barcode Reader is an industry-leading barcode decoding SDK. It supports various formats of barcode and could be run on all the mainstream platforms.
We are going to import a C++ addon for our project. The node-gyp is the configure tool for compiling C++ programs as Node.js addons.
Create The Project
Initialize The Project
We use the official quick start sample to begin our work.
Firstly, let’s clone the sample project to our host.
git clone https://github.com/electron/electron-quick-start.git
Then, install the dependencies and start the project to test if we can run Electron projects properly.
cd electron-quick-start
npm install
npm start
If there is no error, you would see a popup window printing “Hello World!”.
Electron app run successfully.
In your terminal, entering the following command to install the required dependencies.
npm install --save-dev electron-rebuild
npm install -g node-gyp
Enabling Node.js Integration
By default, Node.js integration is disabled. We change it to true in order to use require
while importing the module. In main.js, we specify this value in the option of BrowserWindow
.
const mainWindow = new BrowserWindow({
width: 1280,
height: 1024,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true // Change to true to use Node API
}
})
Import Node.js C++ Addons
Dynamsoft Barcode Reader provides both JavaScript and C/C++ barcode SDKs.To get better performance for the Electron app, we could write a Node.js barcode addon in C++. We can get the addon source code from Github.
We create a folder named libs
under the project root directory, then cloning the node-barcode repository.
mkdir libs
cd libs
git clone https://github.com/Dynamsoft/nodejs-barcode
Once the cloning process has finished, we change the directory and build the library for our project using node-gyp.
cd nodejs-barcode
../../node_modules/.bin/electron-rebuild
The command electron-rebuild
would detect the build environment and fetch corresponding headers to finish the build.
Rebuild the module with electron-rebuild
Once the build is done, several directories which include the dynamically linked library are created.
The headers and compiled binaries would be placed among three folders
Since the library has correctly referred to the binary library, we just need to import the index.js
file in our project.
Back to the project root and open the main.js file with an editor. The main.js is the starting point of the entire application. We import the barcode addon to do a quick test.
const dbr = require('dbr')
No errors generated after requiring the nodejs-barcode library
Decode Barcode From File
Before starting to code, let’s have a look at the APIs.
APIs of nodejs-barcode library
decodeBufferAsync
: reading and decoding from original RGBA data.decodeFileAsync
: reading and decoding by reading an image file.decodeFileStreamAsync
: reading and decoding by reading a file stream.decodeBase64Async
: the image data is passed as base64 stringdecodeYUYVAsync
: the image data is YUV format other than RGBA
Create Decoding Services
We create two script files: background-services.js
and foreground-services.js
.
The foreground-services.js is consumed by the web part, whereas the background-services.js invokes the C++ addon APIs and provides decoding services.
In the background-services.js, we import Node.js barcode addon.
const { ipcMain } = require('electron')
const dbr = require('./libs/nodejs-barcode/index')
const barcodeTypes = dbr.barcodeTypes
function decodeFileAsync(evt, filepath) {
}
function decodeBase64Async(evt, base64Str) {
}
function decodeBufferAsync(evt, imgData, width, height) {
}
function register() {
ipcMain.on('decodeFileAsync', decodeFileAsync)
ipcMain.on('decodeBase64Async', decodeBase64Async)
ipcMain.on('decodeBufferAsync', decodeBufferAsync)
}
module.exports = {
register
}
Since Node.js is using Event-Model, we make inter-process communication using event listeners. To register a listener on the main process, we use ipcMain.on
to add the event name and corresponding handler. We put the registering process in the function register
to control when to register these events.
In the foreground-services.js
, we are now able to consume these services.
const { ipcRenderer } = require('electron')
const DEBUG = false
ipcRenderer.setMaxListeners(30)
const resultBuffer = {
lastUpdate: null,
results: []
}
const frameBuffer = {
lastUpdate: null,
imgData: Uint8ClampedArray.from([]),
width: 0,
height: 0,
channel: 0,
decoding: false
}
function setFrameBuffer(img, width, height, channel) {
console.log('frame buffer to update')
frameBuffer.imgData = img
frameBuffer.width = width
frameBuffer.height = height
frameBuffer.channel = channel
frameBuffer.lastUpdate = Date.now()
}
function startVideoDecode() {
frameBuffer.decoding = true
videoDecode()
}
function stopVideoDecode() {
frameBuffer.decoding = false
}
function videoDecode() {
ipcRenderer.send('videoDecode', frameBuffer.imgData, frameBuffer.width, frameBuffer.height)
}
ipcRenderer.on('videoDecode-next', (evt, msg) => {
updateResultBuffer(msg)
if (frameBuffer.decoding)
videoDecode()
})
function decodeFileAsync(filepath) {
if (DEBUG)
console.log('sending decodeFileAsync from renderer process with args: ' + filepath)
ipcRenderer.send('decodeFileAsync', filepath)
}
function decodeBase64Async(base64Str) {
if (DEBUG)
console.log('sending decodeBase64Async from renderer process')
ipcRenderer.send('decodeBase64Async', base64Str)
}
function decodeBufferAsync(imgData, width, height) {
if (DEBUG)
console.log('sending decodeBufferAsync from renderer process')
ipcRenderer.send('decodeBufferAsync', imgData, width, height )
}
function updateResultBuffer(msg) {
resultBuffer.lastUpdate = Date.now()
resultBuffer.results = msg
}
ipcRenderer.on('decodeBufferAsync-done', (evt, msg) => {
updateResultBuffer(msg)
})
ipcRenderer.on('decodeFileAsync-done', (evt, msg) => {
updateResultBuffer(msg)
})
ipcRenderer.on('decodeBase64Async-done', (evt, msg) => {
updateResultBuffer(msg)
})
module.exports = {
decodeFileAsync,
decodeBase64Async,
decodeBufferAsync,
setFrameBuffer,
startVideoDecode,
stopVideoDecode,
resultBuffer
}
Consuming Services
We have prepared the services. Now we turn our attention to the “frontend”.
To select a file in the web application, the HTML input element is the one usually used.
<input id="file-selector" type="file">
To send the file path to the decoding service after the file is selected, we can register the onchange
event. Don’t forget to register **onclick**
at the same time, otherwise it would not decode the same image for a second time.
document.getElementById('file-selector').onchange = handleFileChange
document.getElementById('file-selector').onclick = evt => {
evt.target.value = ''
}
async function handleFileChange(evt) {
const file = evt.target.files[0]
const results = await services.decodeFileAsync(file.path)
updateResults(results)
}
async function updateResults(results) {
// Remove existing results
const container = document.getElementById('result-box')
container.innerHTML = ''
const nodes = []
results.forEach(result => {
nodes.push(`<div class="result-card"> \
<p>Format: ${result.format}</p> \
<p>Text: ${result.value}</p> \
</div>`
)
Showing the decoding results
Real-Time Decoding With Camera
Access Camera
Obviously, our first step is to access the camera. Similar to activating the camera in the browser, we use navigator.mediaDevices.getUserMedia
to apply for camera access.
function initCamera() {
const video = document.querySelector('video') || document.createElement("video")
const navigator = window.navigator
const stream = navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user', width: 1280, height: 720 }
})
stream.then(stream => {
video.srcObject = stream
})
}
The getUserMedia
returns a Promise which would be resolved with a video stream. We add the subsequent statement to specify the srcObject
as the resolved stream for the video element.
In the index.html
, we add a button and specify the onclick
event handler with initCamera
.
<button id="video-capture-btn" class="btn-secondary">Capture</button>
document.getElementById('video-capture-btn').onclick = initCamera
Retrieve Image Data
We use canvas to get image data.
function getFrame(videoElement) {
const cvs = new OffscreenCanvas(640, 360)
const ctx = cvs.getContext('2d')
ctx.drawImage(videoElement, 0, 0, cvs.width, cvs.height)
const imgData = ctx.getImageData(0, 0, cvs.width, cvs.height)
decodeFromFrame(imgData)
}
The canvas could accept a video element and draw the graph with the video frame. Simply specifying the video element and the area to draw, we could get the frame data afterward.
Each time we retrieve a video frame, we dispatch it to the service for decoding.
async function decodeFromFrame(frame) {
const res = await services.decodeBufferAsync(frame.data, frame.width, frame.height)
updateResults(res.data)
}
Finally, the decoding process needs an event to fire. In initCamera
, we register the onplay
handler with an interval action which periodically executes the getFrame
function to read and decode the image data.
Dispatch Image Data
Once we have the image data, we can send it to the decoding services. The ipcMain
and ipcRenderer
are the inter-processes communication objects for Main Process and Renderer Process respectively. By registering event listeners, each process could listen for specific messages. We register the following events and their corresponding finish message protocols.
Sender | Receiver | Event Name | Message Protocol |
---|---|---|---|
Renderer | Main | decodeFileAsync | File path |
Main | Renderer | decodeFileAsync-done | Results |
Renderer | Main | decodeBufferAsync | Image Data, width, height |
Main | Renderer | decodeBufferAsync-done | Results |
Renderer | Main | decodeBase64Async | Base64 String |
Main | Renderer | decodeBase64Async-done | Results |
Main | Renderer | decodeVideo | Image Data, width, height |
Renderer | Main | decodeVideo-next | Results |
In background-services, we register the following listeners,
ipcMain.on('decodeFileAsync', decodeFileAsync)
ipcMain.on('decodeBase64Async', decodeBase64Async)
ipcMain.on('decodeBufferAsync', decodeBufferAsync)
ipcMain.on('videoDecode', videoDecode)
and their handlers.
function videoDecode(evt, imgData, width, height) {
if (DEBUG)
console.log(`${new Date().toLocaleString()}/real-time decoding for video stream: ${imgData.length/height}, ${width}`)
dbr.decodeBufferAsync(imgData, width, height, width*4, barcodeTypes, (err, msg) => {
if (err)
console.log(err)
let results = [];
for (index in msg) {
let result = Object()
let res = msg[index];
result.format = res['format']
result.value = res['value']
results.push(result)
}
evt.reply('videoDecode-next', results)
if (DEBUG)
console.log('ipcMain: replied with ' + JSON.stringify(results))
})
}
function decodeFileAsync(evt, filepath) {
if (DEBUG)
console.log('ipcMain: decodeFileAsync invoked: ' + filepath)
dbr.decodeFileAsync(filepath, barcodeTypes, (err, msg) => {
if (err)
console.log(err)
let results = [];
for (index in msg) {
let result = Object()
let res = msg[index];
result.format = res['format']
result.value = res['value']
results.push(result)
}
evt.reply('decodeFileAsync-done', results)
if (DEBUG)
console.log('ipcMain: replied with ' + JSON.stringify(results))
})
}
function decodeBase64Async(evt, base64Str) {
if (DEBUG)
console.log('ipcMain: decodeBase64Async is invoked')
dbr.decodeBase64Async(base64Str, barcodeTypes, (err, msg) => {
if (err)
console.error(err)
let results = [];
for (index in msg) {
let result = Object()
let res = msg[index];
result.format = res['format']
result.value = res['value']
results.push(result)
}
evt.reply('decodeBase64Async-done', results)
if (DEBUG)
console.log('ipcMain: replied with ' + JSON.stringify(results))
})
}
function decodeBufferAsync(evt, imgData, width, height) {
if (DEBUG)
console.log('ipcMain: decodeBufferAsync is invoked')
console.log(imgData)
dbr.decodeBufferAsync(imgData, width, height, width*4, barcodeTypes, (err, msg) => {
if (err)
console.error(err)
let results = [];
for (index in msg) {
let result = Object()
let res = msg[index];
result.format = res['format']
result.value = res['value']
results.push(result)
}
evt.reply('decodeBufferAsync-done', results)
if (DEBUG)
console.log('ipcMain: replied with ' + JSON.stringify(results))
})
}
Read The Barcode
We have implemented the features we want. Now it is time to run and test our project. Go to the project root path, type npm start
in the terminal to launch the Electron barcode reader.
Note that it is compulsory to compile the nodejs-barcode library each time if the version of Electron or platform changed.
The first page after launching the app
Decoding a camera stream and showing the result
Source Code
Github: Dynamsoft/dbr-electron-nodejs