In the fast-paced realm of web development, seamlessly integrating varied functionalities into web applications has become increasingly essential. Technologies such as barcode detection, Machine Readable Zones (MRZ) recognition, and document rectification are pivotal tools adopted in various domains, including retail, travel, and document management. In this article, we'll explore how to implement these functionalities within your web applications using Dynamsoft JavaScript APIs.
Online Demo
https://yushulx.me/javascript-barcode-qr-code-scanner/examples/9.x/barcode_mrz_document/
Prerequisites
- Free trial licenses: Get a free trial license for each product
- Dynamsoft Camera Enhancer: Helps to access the camera and capture images
- Dynamsoft JavaScript Barcode: Reads barcodes from images
- Dynamsoft Document Normalizer: Detects and rectifies documents
- Dynamsoft Label Recognizer: Recognizes MRZ from images
Integrating Dynamsoft JavaScript APIs
In the index.html
file, include the following JavaScript libraries:
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@3.3.9/dist/dce.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.6.32/dist/dbr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-normalizer@1.0.12/dist/ddn.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-label-recognizer@2.2.31/dist/dlr.js"></script>
Configuring License Key, Input Source and Scanning Options
The loading of core WebAssembly (WASM) modules will be initiated only upon the user clicking the Activate SDK
button and entering a valid license key.
<div>
<label>
Get a License key from <a href="https://www.dynamsoft.com/customer/license/trialLicense"
target="_blank">here</a>
</label>
<input type="text" id="license_key"
value="LICENSE-KEY"
placeholder="LICENSE-KEY">
<button onclick="activate()">Activate SDK</button>
</div>
<script>
let normalizer;
let reader;
let recognizer;
let cameraEnhancer;
let isSDKReady = false;
async function activate() {
toggleLoading(true);
let divElement = document.getElementById("license_key");
let licenseKey = divElement.value == "" ? divElement.placeholder : divElement.value;
try {
Dynamsoft.DBR.BarcodeScanner.license = licenseKey;
Dynamsoft.DDN.DocumentNormalizer.license = licenseKey;
Dynamsoft.DLR.LabelRecognizer.license = licenseKey;
await Dynamsoft.DBR.BarcodeReader.loadWasm();
await Dynamsoft.DLR.LabelRecognizer.loadWasm();
await Dynamsoft.DDN.DocumentNormalizer.loadWasm();
reader = await Dynamsoft.DBR.BarcodeReader.createInstance();
normalizer = await Dynamsoft.DDN.DocumentNormalizer.createInstance();
recognizer = await Dynamsoft.DLR.LabelRecognizer.createInstance();
await recognizer.updateRuntimeSettingsFromString("MRZ");
cameraEnhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance();
initCamera();
isSDKReady = true;
}
catch (ex) {
console.error(ex);
}
toggleLoading(false);
}
async function initCamera() {
try {
cameras = await getCameras(cameraEnhancer);
if (cameras != null && cameras.length > 0) {
for (let i = 0; i < cameras.length; i++) {
let option = document.createElement("option");
option.text = cameras[i].label;
cameraSource.add(option);
}
await setVideoElement(cameraEnhancer, "camera_view");
await openCamera(cameraEnhancer, cameras[0]);
}
else {
alert("No camera found.");
}
}
catch (ex) {
console.error(ex);
}
}
</script>
Based on the input source selected from a dropdown list, we can dynamically switch between different containers. For instance, selecting an image file as the input will display the file container, whereas choosing a camera input reveals the camera container.
<div class="row">
<div>
<select onchange="selectChanged()" id="dropdown">
<option value="file">File</option>
<option value="camera">Camera</option>
</select>
<input type="checkbox" id="barcode_checkbox" checked>Barcode
<input type="checkbox" id="mrz_checkbox">MRZ
<input type="checkbox" id="document_checkbox" onchange="checkChanged()">Document
</div>
</div>
<script>
function selectChanged() {
switchProduct(dropdown.value)
}
function switchProduct(name) {
if (name === 'file') {
let divElement = document.getElementById("file_container");
divElement.style.display = "block";
divElement = document.getElementById("camera_container");
divElement.style.display = "none";
}
else {
let divElement = document.getElementById("camera_container");
divElement.style.display = "block";
divElement = document.getElementById("file_container");
divElement.style.display = "none";
}
}
</script>
Detecting Barcodes, MRZ, and Documents from Images
The following steps outline the process for detecting barcodes, MRZ (Machine Readable Zone), and documents within an image file:
-
Construct the UI (User Interface) for file input and displaying results. This involves creating a section for users to upload an image file and another section to show the detection outcomes.
<div class="container" id="file_container"> <div> <input type="file" id="pick_file" accept="image/*" /> <button onclick="detect()">Detect</button> </div> <div class="row"> <div class="imageview"> <img id="image_file" src="default.png" /> <canvas id="overlay_canvas" class="overlay"></canvas> </div> </div> <div class="row"> <div> <textarea id="detection_result"></textarea> </div> </div> <div class="row"> <div> <img id="document-rectified-image" /> </div> </div> </div>
-
Load the uploaded image onto an HTML canvas. The canvas serves not only to display the image but also to overlay the detection results, providing a visual representation of what has been identified.
document.getElementById("pick_file").addEventListener("change", function () { let currentFile = this.files[0]; if (currentFile == null) { return; } var fr = new FileReader(); fr.onload = function () { loadImage2Canvas(fr.result); } fr.readAsDataURL(currentFile); }); function loadImage2Canvas(base64Image) { imageFile.src = base64Image; img.src = base64Image; img.onload = function () { let width = img.width; let height = img.height; overlayCanvas.width = width; overlayCanvas.height = height; targetCanvas.width = width; targetCanvas.height = height; detect(); }; }
-
Utilize Dynamsoft JavaScript APIs to detect the barcode, MRZ, and document within the image. Once detected, draw the results directly on the canvas to visually indicate the locations of the detected items.
Barcode
if (barcodeCheckbox.checked) { let barcodeResults = await reader.decode(img); if (barcodeResults.length > 0) { let txts = []; for (var i = 0; i < barcodeResults.length; ++i) { txts.push(barcodeResults[i].barcodeText); localization = barcodeResults[i].localizationResult; text = barcodeResults[i].barcodeText; // Draw overlay context.beginPath(); context.strokeStyle = '#ff0000'; context.lineWidth = 2; context.moveTo(localization.x1, localization.y1); context.lineTo(localization.x2, localization.y2); context.lineTo(localization.x3, localization.y3); context.lineTo(localization.x4, localization.y4); context.lineTo(localization.x1, localization.y1); context.stroke(); context.font = '18px Verdana'; context.fillStyle = '#ff0000'; let x = [localization.x1, localization.x2, localization.x3, localization.x4]; let y = [localization.y1, localization.y2, localization.y3, localization.y4]; x.sort(function (a, b) { return a - b; }); y.sort(function (a, b) { return b - a; }); let left = x[0]; let top = y[0]; context.fillText(text, left, top + 50); } detection_result.innerHTML += txts.join(', ') + '\n'; } }
MRZ
if (mrzCheckbox.checked) { let mrzResults = await recognizer.recognize(img); let txts = []; for (let result of mrzResults) { for (let line of result.lineResults) { let text = line.text; let points = line.location.points; // Draw overlay context.beginPath(); context.strokeStyle = '#0000ff'; context.lineWidth = 2; context.moveTo(points[0].x, points[0].y); context.lineTo(points[1].x, points[1].y); context.lineTo(points[2].x, points[2].y); context.lineTo(points[3].x, points[3].y); context.lineTo(points[0].x, points[0].y); context.stroke(); context.font = '18px Verdana'; context.fillStyle = '#ff0000'; let x = [points[0].x, points[1].x, points[0].x, points[0].x]; let y = [points[0].y, points[1].y, points[0].y, points[0].y]; x.sort(function (a, b) { return a - b; }); y.sort(function (a, b) { return b - a; }); let left = x[0]; let top = y[0]; context.fillText(text, left, top); txts.push(text); } } if (txts.length == 2) { detection_result.innerHTML += JSON.stringify(mrzParseTwoLine(txts[0], txts[1])) + '\n'; } else if (txts.length == 3) { detection_result.innerHTML += JSON.stringify(mrzParseThreeLine(txts[0], txts[1], txts[2])) + '\n'; } }
Document
if (documentCheckbox.checked) { let documentResults = await normalizer.detectQuad(img); if (documentResults.length > 0) { let quad = documentResults[0]; globalPoints = quad.location.points; // Start document editor openEditor(img.src) // Draw overlay context.strokeStyle = "#00ff00"; context.lineWidth = 2; for (let i = 0; i < globalPoints.length; i++) { context.beginPath(); context.arc(globalPoints[i].x, globalPoints[i].y, 5, 0, 2 * Math.PI); context.stroke(); } context.beginPath(); context.moveTo(globalPoints[0].x, globalPoints[0].y); context.lineTo(globalPoints[1].x, globalPoints[1].y); context.lineTo(globalPoints[2].x, globalPoints[2].y); context.lineTo(globalPoints[3].x, globalPoints[3].y); context.lineTo(globalPoints[0].x, globalPoints[0].y); context.stroke(); let x = [globalPoints[0].x, globalPoints[1].x, globalPoints[0].x, globalPoints[0].x]; let y = [globalPoints[0].y, globalPoints[1].y, globalPoints[0].y, globalPoints[0].y]; x.sort(function (a, b) { return a - b; }); y.sort(function (a, b) { return b - a; }); let left = x[0]; let top = y[0]; context.font = '18px Verdana'; context.fillStyle = '#00ff00'; context.fillText('Detected document', left, top); } }
Parsing MRZ
After successfully recognizing the MRZ, it is possible to parse the MRZ string to extract vital information, including the individual's name, passport number, date of birth, and expiration date.
function mrzParseLine(line, startIndex, endIndex, isDate = false) {
let extractedString = line.substring(startIndex, endIndex).replace(/</g, ' ').trim();
return isDate ? formatDateString(extractedString) : extractedString;
}
function formatDateString(dateString) {
let currentYear = new Date().getFullYear();
let century = parseInt(dateString.substr(0, 2)) > currentYear % 100 ? '19' : '20';
return century + dateString.slice(0, 2) + '-' + dateString.slice(2, 4) + '-' + dateString.slice(4);
}
function extractType(typeChar) {
if (!/[IPV]/.test(typeChar)) return false;
switch (typeChar) {
case 'P': return 'PASSPORT (TD-3)';
case 'V': return line1.length === 44 ? 'VISA (MRV-A)' : 'VISA (MRV-B)';
case 'I': return 'ID CARD (TD-2)';
default: return false;
}
}
function mrzParseTwoLine(line1, line2) {
let passportMRZ = {};
passportMRZ.type = extractType(line1.substring(0, 1));
if (!passportMRZ.type) return false;
passportMRZ.nationality = mrzParseLine(line1, 2, 5);
passportMRZ.surname = mrzParseLine(line1, 5, line1.indexOf("<<"));
passportMRZ.givenname = mrzParseLine(line1, line1.indexOf("<<") + 2);
passportMRZ.passportnumber = mrzParseLine(line2, 0, 9);
passportMRZ.issuecountry = mrzParseLine(line2, 10, 13);
passportMRZ.birth = mrzParseLine(line2, 13, 19, true);
passportMRZ.gender = line2[20];
passportMRZ.expiry = mrzParseLine(line2, 21, 27, true);
return passportMRZ;
}
function mrzParseThreeLine(line1, line2, line3) {
let passportMRZ = {};
passportMRZ.type = extractType(line1.substring(0, 1));
if (!passportMRZ.type) return false;
passportMRZ.nationality = mrzParseLine(line2, 15, 18);
passportMRZ.surname = mrzParseLine(line3, 0, line3.indexOf("<<"));
passportMRZ.givenname = mrzParseLine(line3, line3.indexOf("<<") + 2);
passportMRZ.passportnumber = mrzParseLine(line1, 5, 14);
passportMRZ.issuecountry = mrzParseLine(line1, 2, 5);
passportMRZ.birth = mrzParseLine(line2, 0, 6, true);
passportMRZ.gender = line2[7].replace('<', 'X');
passportMRZ.expiry = mrzParseLine(line2, 8, 14, true);
return passportMRZ;
}
Creating a Document Edge Editor
A div
element is utilized to construct the document edge editor.
<div class="container" id="document_editor">
<div>
<button onclick="edit()">Edit</button>
<button onclick="rectify()">Rectify</button>
<button onclick="save()">Save</button>
</div>
<div class="imageview" id="edit_view">
<img id="target_file" src="default.png" />
<canvas id="target_canvas" class="overlay"></canvas>
</div>
<div class="imageview" id="rectify_view">
<img id="rectified_image" src="default.png" />
</div>
</div>
The editor allows users to manually refine the detected document edges if the initial detection is imprecise. The coordinates of the document's four points are stored in the globalPoints
array. When the user clicks and drags a point, the coordinates are updated and the quad is redrawn.
function openEditor(image) {
let target_context = targetCanvas.getContext('2d');
targetCanvas.addEventListener("mousedown", (event) => updatePoint(event, target_context, targetCanvas));
targetCanvas.addEventListener("touchstart", (event) => updatePoint(event, target_context, targetCanvas));
drawQuad(target_context, targetCanvas);
targetFile.src = image;
}
function updatePoint(e, context, canvas) {
if (!globalPoints) {
return;
}
function getCoordinates(e) {
let rect = canvas.getBoundingClientRect();
let scaleX = canvas.clientWidth / canvas.width;
let scaleY = canvas.clientHeight / canvas.height;
let mouseX = e.clientX || e.touches[0].clientX;
let mouseY = e.clientX || e.touches[0].clientY;
if (scaleX < scaleY) {
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top - (canvas.clientHeight - canvas.height * scaleX) / 2;
mouseX = mouseX / scaleX;
mouseY = mouseY / scaleX;
}
else {
mouseX = e.clientX - rect.left - (canvas.clientWidth - canvas.width * scaleY) / 2;
mouseY = e.clientY - rect.top;
mouseX = mouseX / scaleY;
mouseY = mouseY / scaleY;
}
return { x: Math.round(mouseX), y: Math.round(mouseY) };
}
let delta = 10;
let coordinates = getCoordinates(e);
for (let i = 0; i < globalPoints.length; i++) {
if (Math.abs(globalPoints[i].x - coordinates.x) < delta && Math.abs(globalPoints[i].y - coordinates.y) < delta) {
canvas.addEventListener("mousemove", dragPoint);
canvas.addEventListener("mouseup", releasePoint);
canvas.addEventListener("touchmove", dragPoint);
canvas.addEventListener("touchend", releasePoint);
function dragPoint(e) {
coordinates = getCoordinates(e);
globalPoints[i].x = coordinates.x;
globalPoints[i].y = coordinates.y;
drawQuad(context, canvas);
}
function releasePoint() {
canvas.removeEventListener("mousemove", dragPoint);
canvas.removeEventListener("mouseup", releasePoint);
canvas.removeEventListener("touchmove", dragPoint);
canvas.removeEventListener("touchend", releasePoint);
}
break;
}
}
}
function drawQuad(context, canvas) {
context.clearRect(0, 0, canvas.width, canvas.height);
context.strokeStyle = "#00ff00";
context.lineWidth = 2;
for (let i = 0; i < globalPoints.length; i++) {
context.beginPath();
context.arc(globalPoints[i].x, globalPoints[i].y, 5, 0, 2 * Math.PI);
context.stroke();
}
context.beginPath();
context.moveTo(globalPoints[0].x, globalPoints[0].y);
context.lineTo(globalPoints[1].x, globalPoints[1].y);
context.lineTo(globalPoints[2].x, globalPoints[2].y);
context.lineTo(globalPoints[3].x, globalPoints[3].y);
context.lineTo(globalPoints[0].x, globalPoints[0].y);
context.stroke();
}
Scanning Barcodes, MRZ, and Documents from a Camera
-
Create a
div
element that will serve as the camera view, providing a visual output of the camera feed.
<div class="container" id="camera_container"> <div> <select onchange="cameraChanged()" id="camera_source"> </select> <button onclick="scan()" id="scan_button">Start</button> <button onclick="capture()">Capture a document</button> <div id="videoview"> <div id="camera_view"></div> </div> <div class="row"> <div> <textarea id="scan_result"></textarea> </div> </div> </div> </div>
-
Choose and activate a camera. Utilize the
isLooping
flag to ensure that the system waits for one camera to close before opening another.
async function cameraChanged() { isDetecting = false; while (isLooping) { await new Promise(resolve => setTimeout(resolve, 100)); } if (cameras != null && cameras.length > 0) { let index = cameraSource.selectedIndex; await openCamera(cameraEnhancer, cameras[index]); } }
-
Start the process of scanning for barcodes, MRZ (Machine Readable Zones), and documents directly from the camera feed.
function scan() { if (!isSDKReady) { alert("Please activate the SDK first."); return; } if (!isDetecting) { scanButton.innerHTML = "Stop"; isDetecting = true; startDetectionLoop(); } else { scanButton.innerHTML = "Scan"; isDetecting = false; } } async function startDetectionLoop() { isLooping = true; let scan_result = document.getElementById('scan_result'); scan_result.innerHTML = ""; while (isDetecting) { let frame = acquireCameraFrame(cameraEnhancer); let clearCount = 0; try { if (barcodeCheckbox.checked) { let barcodeResults = await reader.decode(frame); ... // Draw overlay } if (mrzCheckbox.checked) { let mrzResults = await recognizer.recognize(frame); ... // Draw overlay } if (documentCheckbox.checked) { let documentResults = await normalizer.detectQuad(frame); ... // Draw overlay } if (clearCount == 0) { clearOverlay(cameraEnhancer); await new Promise(resolve => setTimeout(resolve, 30)); } } catch (ex) { console.error(ex); } } clearOverlay(cameraEnhancer); isLooping = false; }
-
Employ the Dynamsoft Camera Enhancer's built-in rendering capabilities to overlay the scanning results on the camera feed, enhancing user interaction and feedback.
function drawLine(cameraEnhancer, x1, y1, x2, y2) { if (!Dynamsoft) return; try { let drawingLayers = cameraEnhancer.getDrawingLayers(); let drawingLayer; let drawingItems = new Array( new Dynamsoft.DCE.DrawingItem.DT_Line({ x: x1, y: y1 }, { x: x2, y: y2 }, 1) ) if (drawingLayers.length > 0) { drawingLayer = drawingLayers[0]; } else { drawingLayer = cameraEnhancer.createDrawingLayer(); } drawingLayer.addDrawingItems(drawingItems); } catch (ex) { console.error(ex); } } function drawText(cameraEnhancer, text, x, y) { if (!Dynamsoft) return; try { let drawingLayers = cameraEnhancer.getDrawingLayers(); let drawingLayer; let drawingItems = new Array( new Dynamsoft.DCE.DrawingItem.DT_Text(text, x, y, 1), ) if (drawingLayers.length > 0) { drawingLayer = drawingLayers[0]; } else { drawingLayer = cameraEnhancer.createDrawingLayer(); } drawingLayer.addDrawingItems(drawingItems); } catch (ex) { console.error(ex); } }
Barcode
if (barcodeResults.length > 0) { let txts = []; for (var i = 0; i < barcodeResults.length; ++i) { txts.push(barcodeResults[i].barcodeText); localization = barcodeResults[i].localizationResult; text = barcodeResults[i].barcodeText; // Draw overlay drawLine(cameraEnhancer, localization.x1, localization.y1, localization.x2, localization.y2); drawLine(cameraEnhancer, localization.x2, localization.y2, localization.x3, localization.y3); drawLine(cameraEnhancer, localization.x3, localization.y3, localization.x4, localization.y4); drawLine(cameraEnhancer, localization.x4, localization.y4, localization.x1, localization.y1); let x = [localization.x1, localization.x2, localization.x3, localization.x4]; let y = [localization.y1, localization.y2, localization.y3, localization.y4]; x.sort(function (a, b) { return a - b; }); y.sort(function (a, b) { return b - a; }); let left = x[0]; let top = y[0]; drawText(cameraEnhancer, text, left, top + 50); } scan_result.innerHTML += txts.join(', ') + '\n'; }
MRZ
if (mrzCheckbox.checked) { let mrzResults = await recognizer.recognize(frame); if (clearCount == 0) { clearOverlay(cameraEnhancer); clearCount += 1; } let txts = []; for (let result of mrzResults) { for (let line of result.lineResults) { let text = line.text; let points = line.location.points; // Draw overlay drawLine(cameraEnhancer, points[0].x, points[0].y, points[1].x, points[1].y); drawLine(cameraEnhancer, points[1].x, points[1].y, points[2].x, points[2].y); drawLine(cameraEnhancer, points[2].x, points[2].y, points[3].x, points[3].y); drawLine(cameraEnhancer, points[3].x, points[3].y, points[0].x, points[0].y); let x = [points[0].x, points[1].x, points[0].x, points[0].x]; let y = [points[0].y, points[1].y, points[0].y, points[0].y]; x.sort(function (a, b) { return a - b; }); y.sort(function (a, b) { return b - a; }); let left = x[0]; let top = y[0]; drawText(cameraEnhancer, text, left, top); txts.push(text); } } if (txts.length == 2) { scan_result.innerHTML += JSON.stringify(mrzParseTwoLine(txts[0], txts[1])) + '\n'; } else if (txts.length == 3) { scan_result.innerHTML += JSON.stringify(mrzParseThreeLine(txts[0], txts[1], txts[2])) + '\n'; } }
Document
if (documentCheckbox.checked) { let documentResults = await normalizer.detectQuad(frame); if (clearCount == 0) { clearOverlay(cameraEnhancer); clearCount += 1; } if (documentResults.length > 0) { let quad = documentResults[0]; let points = quad.location.points; if (isCaptured) { isCaptured = false; globalPoints = points; targetCanvas.width = resolution[0]; targetCanvas.height = resolution[1]; openEditor(frame.toDataURL()); } // Draw overlay drawLine(cameraEnhancer, points[0].x, points[0].y, points[1].x, points[1].y); drawLine(cameraEnhancer, points[1].x, points[1].y, points[2].x, points[2].y); drawLine(cameraEnhancer, points[2].x, points[2].y, points[3].x, points[3].y); drawLine(cameraEnhancer, points[3].x, points[3].y, points[0].x, points[0].y); let x = [points[0].x, points[1].x, points[0].x, points[0].x]; let y = [points[0].y, points[1].y, points[0].y, points[0].y]; x.sort(function (a, b) { return a - b; }); y.sort(function (a, b) { return b - a; }); let left = x[0]; let top = y[0]; drawText(cameraEnhancer, 'Detected document', left, top); } }
Testing the Multi-scanning Application in a Web Browser
Barcode
Machine-readable Zone
Document
Top comments (0)