How to Build a Web App to Scan Barcodes, Documents, and MRZ in HTML5 and JavaScript

Modern web applications increasingly require advanced image processing capabilities. Technologies like barcode detection, Machine Readable Zone (MRZ) recognition, and document rectification have become essential across industries from retail checkout systems to airport passport scanners and digital document management solutions. This comprehensive guide demonstrates how to implement all three capabilities in a single web application using the Dynamsoft Capture Vision JavaScript SDK.

Demo Video: Web Scanner for Barcode, QR Code, MRZ, and Document

Online Demo

https://yushulx.me/javascript-barcode-qr-code-scanner/examples/barcode_mrz_document/

Prerequisites

Setting Up the Application: License, Input Source, and Scanning Modes

License Activation

image

To optimize performance, the core WebAssembly (WASM) modules are loaded on-demand when the user clicks the Activate SDK button with a valid license key. This lazy-loading approach reduces initial page load time.

<div>
    <label>
        Get a License key from <a href="https://www.dynamsoft.com/customer/license/trialLicense/?product=dcv&package=cross-platform"
            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 cvr;
    let reader;
    let cameraEnhancer;
    let isSDKReady = false;
    let img = new Image();
    let globalPoints;
    let cameras;
    let resolution;
    let isDetecting = false;
    let isCaptured = false;
    let parser;

    async function activate() {
        toggleLoading(true);
        let divElement = document.getElementById("license_key");
        let licenseKey = divElement.value == "" ? divElement.placeholder : divElement.value;

        try {
            await Dynamsoft.License.LicenseManager.initLicense(
                licenseKey,
                true
            );

            Dynamsoft.Core.CoreModule.loadWasm(["DBR", "DLR", "DDN"]);

            parser = await Dynamsoft.DCP.CodeParser.createInstance();

            await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD1_ID");
            await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD2_FRENCH_ID");
            await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD2_ID");
            await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD2_VISA");
            await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD3_PASSPORT");
            await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD3_VISA");
            await Dynamsoft.CVR.CaptureVisionRouter.appendDLModelBuffer("MRZCharRecognition");
            await Dynamsoft.CVR.CaptureVisionRouter.appendDLModelBuffer("MRZTextLineRecognition");

            cvr = await Dynamsoft.CVR.CaptureVisionRouter.createInstance();

            cvr.addResultReceiver({
                onCapturedResultReceived: (result) => {

                    showCameraResult(result);
                },
            });

            isSDKReady = true;
        }
        catch (ex) {
            console.error(ex);
        }

        toggleLoading(false);
    }
</script>

Input Source Selection

The application supports two input sources: static image files and live camera feed. Users can switch between these modes using a dropdown menu, which dynamically shows or hides the corresponding UI containers.

<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>
    async function selectChanged() {
        if (dropdown.value === 'file') {
            if (cameraEnhancer != null) {
                closeCamera(cameraEnhancer);

                if (cvr != null) {
                    await cvr.stopCapturing();
                }

                scanButton.innerHTML = "Scan";
                isDetecting = false;
            }
            let divElement = document.getElementById("file_container");
            divElement.style.display = "block";

            divElement = document.getElementById("camera_container");
            divElement.style.display = "none";
        }
        else {
            if (cameraEnhancer == null) {
                await initCamera();
            }
            let divElement = document.getElementById("camera_container");
            divElement.style.display = "block";

            divElement = document.getElementById("file_container");
            divElement.style.display = "none";

            await cameraChanged();
        }
    }

</script>

Implementing Image File Detection

To enable detection from static images, follow these steps:

  1. Construct the UI (User Interface) for file input and result display. 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>
    
  2. Load the uploaded image onto an HTML canvas. The canvas serves dual purposes: displaying the image and overlaying detection results to visualize detected regions.

     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 = async function () {
             let width = img.width;
             let height = img.height;
        
             overlayCanvas.width = width;
             overlayCanvas.height = height;
        
             targetCanvas.width = width;
             targetCanvas.height = height;
              
             ...
         };
     }
    
  3. Use Dynamsoft Capture Vision APIs to detect barcodes, MRZ, or document boundaries based on the selected mode. Detection results are then rendered on the canvas as visual overlays.

     let selectedMode = document.querySelector('input[name="scanMode"]:checked').value;
        
     let context = overlayCanvas.getContext('2d');
     context.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
     try {
         if (selectedMode == "barcode") {
             await cvr.resetSettings();
             cvr.capture(img.src, "ReadBarcodes_Default").then((result) => {
                 showFileResult(selectedMode, context, result, img, Dynamsoft.Core.EnumCapturedResultItemType.CRIT_BARCODE);
             });
         }
         else if (selectedMode == "mrz") {
             await cvr.initSettings("./full.json");
             cvr.capture(img.src, "ReadMRZ").then((result) => {
                 showFileResult(selectedMode, context, result, img, Dynamsoft.Core.EnumCapturedResultItemType.CRIT_TEXT_LINE);
             });
         }
         else if (selectedMode == "document") {
             await cvr.resetSettings();
             cvr.capture(img.src, "DetectDocumentBoundaries_Default").then((result) => {
                 showFileResult(selectedMode, context, result, img, Dynamsoft.Core.EnumCapturedResultItemType.CRIT_DETECTED_QUAD);
             });
         }
     }
     catch (ex) {
         console.error(ex);
     }
    
     async function showFileResult(selectedMode, context, result, img, type) {
         let parseResults = '';
         let detection_result = document.getElementById('detection_result');
         detection_result.innerHTML = "";
         let txts = [];
         let items = result.items;
         if (items.length > 0) {
             for (var i = 0; i < items.length; ++i) {
        
                 if (items[i].type !== type) {
                     continue;
                 }
        
                 let item = items[i];
                 txts.push(item.text);
                 localization = item.location;
        
                 context.strokeStyle = '#ff0000';
                 context.lineWidth = 2;
        
                 let points = localization.points;
                 globalPoints = points;
                 context.beginPath();
                 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.closePath();
                 context.stroke();
        
                 if (selectedMode == "barcode") {
                     if (txts.length > 0) {
                         detection_result.innerHTML += txts.join('\n') + '\n\n';
                     }
                     else {
                         detection_result.innerHTML += "Recognition Failed\n";
                     }
                 }
                 else if (selectedMode == "mrz") {
                     if (txts.length > 0) {
                         detection_result.innerHTML += txts.join('\n') + '\n\n';
                         parseResults = await parser.parse(item.text);
                         detection_result.innerHTML += JSON.stringify(extractMrzInfo(parseResults));
                     }
                     else {
                         detection_result.innerHTML += "Recognition Failed\n";
                     }
        
                 }
                 else if (selectedMode == "document") {
                     openEditor(img.src);
                 }
        
        
             }
         }
         else {
             detection_result.innerHTML += "Nothing found\n";
         }
     }
    

Parsing MRZ Data

Once the MRZ text is recognized, parse it to extract structured information such as the holder’s name, passport number, date of birth, nationality, and expiration date. The CodeParser module handles the complex parsing logic automatically.

function extractMrzInfo(result) {
    const parseResultInfo = {};
    let type = result.getFieldValue("documentCode");
    parseResultInfo['Document Type'] = JSON.parse(result.jsonString).CodeType;
    let nation = result.getFieldValue("issuingState");
    parseResultInfo['Issuing State'] = nation;
    let surName = result.getFieldValue("primaryIdentifier");
    parseResultInfo['Surname'] = surName;
    let givenName = result.getFieldValue("secondaryIdentifier");
    parseResultInfo['Given Name'] = givenName;
    let passportNumber = type === "P" ? result.getFieldValue("passportNumber") : result.getFieldValue("documentNumber");
    parseResultInfo['Passport Number'] = passportNumber;
    let nationality = result.getFieldValue("nationality");
    parseResultInfo['Nationality'] = nationality;
    let gender = result.getFieldValue("sex");
    parseResultInfo["Gender"] = gender;
    let birthYear = result.getFieldValue("birthYear");
    let birthMonth = result.getFieldValue("birthMonth");
    let birthDay = result.getFieldValue("birthDay");
    if (parseInt(birthYear) > (new Date().getFullYear() % 100)) {
        birthYear = "19" + birthYear;
    } else {
        birthYear = "20" + birthYear;
    }
    parseResultInfo['Date of Birth (YYYY-MM-DD)'] = birthYear + "-" + birthMonth + "-" + birthDay;
    let expiryYear = result.getFieldValue("expiryYear");
    let expiryMonth = result.getFieldValue("expiryMonth");
    let expiryDay = result.getFieldValue("expiryDay");
    if (parseInt(expiryYear) >= 60) {
        expiryYear = "19" + expiryYear;
    } else {
        expiryYear = "20" + expiryYear;
    }
    parseResultInfo["Date of Expiry (YYYY-MM-DD)"] = expiryYear + "-" + expiryMonth + "-" + expiryDay;
    return parseResultInfo;
}

Creating a Document Edge Editor

Use a div element to create a 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 enables manual refinement of detected document boundaries when automatic detection isn’t perfect. The four corner points are stored in the globalPoints array. Users can click and drag any corner to adjust the boundary, with the quadrilateral redrawn in real-time to provide visual feedback.

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 clientX = e.clientX;
        let clientY = e.clientY;

        if (e.touches && e.touches.length > 0) {
            clientX = e.touches[0].clientX;
            clientY = e.touches[0].clientY;
        }

        let mouseX = clientX;
        let mouseY = clientY;

        if (scaleX < scaleY) {
            mouseX = clientX - rect.left;
            mouseY = clientY - rect.top - (canvas.clientHeight - canvas.height * scaleX) / 2;

            mouseX = mouseX / scaleX;
            mouseY = mouseY / scaleX;
        }
        else {
            mouseX = clientX - rect.left - (canvas.clientWidth - canvas.width * scaleY) / 2;
            mouseY = clientY - rect.top;

            mouseX = mouseX / scaleY;
            mouseY = mouseY / scaleY;
        }

        return { x: Math.round(mouseX), y: Math.round(mouseY) };
    }

    let delta = 20;
    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) {
            e.preventDefault();
            canvas.addEventListener("mousemove", dragPoint);
            canvas.addEventListener("mouseup", releasePoint);
            canvas.addEventListener("touchmove", dragPoint);
            canvas.addEventListener("touchend", releasePoint);
            function dragPoint(e) {
                e.preventDefault();
                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, 10, 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();
}

Implementing Real-Time Camera Scanning

  1. Create a container div element for the camera view, which will display the live video feed and detection results.

     <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>
    
  2. Select and activate a camera device from the available options.

     async function cameraChanged() {
         if (cameras != null && cameras.length > 0) {
             let index = cameraSource.selectedIndex;
             await openCamera(cameraEnhancer, cameras[index]);
         }
     }
    
     async function openCamera(cameraEnhancer, cameraInfo) {
         if (!Dynamsoft) return;
        
         try {
             await cameraEnhancer.selectCamera(cameraInfo);
             cameraEnhancer.on("played", function () {
                 resolution = cameraEnhancer.getResolution();
             });
             await cameraEnhancer.open();
         }
         catch (ex) {
             console.error(ex);
         }
     }
    
  3. Start continuous scanning on the camera feed. Detection results are delivered asynchronously through the onCapturedResultReceived callback, enabling real-time processing without blocking the UI.

     cvr.addResultReceiver({
         onCapturedResultReceived: (result) => {
    
             showCameraResult(result);
         },
     });
    
     async function startScanning() {
         if (!isSDKReady) {
             alert("Please activate the SDK first.");
             return;
         }
    
         if (isDetecting) {
             return; // Already scanning
         }
    
         let selectedMode = document.querySelector('input[name="scanMode"]:checked').value;
    
         if (selectedMode == "mrz") {
             let scanRegion = {
                 x: 10,
                 y: 30,
                 width: 80,
                 height: 40,
                 isMeasuredInPercentage: true
             };
             cameraEnhancer.setScanRegion(scanRegion);
         }
         else {
             cameraEnhancer.setScanRegion(null);
         }
    
         isDetecting = true;
         cvr.setInput(cameraEnhancer);
    
         if (selectedMode == "mrz") {
             await cvr.initSettings("./full.json");
             cvr.startCapturing("ReadMRZ");
         }
         else if (selectedMode == "barcode") {
             await cvr.resetSettings();
             cvr.startCapturing("ReadBarcodes_Default");
         }
         else if (selectedMode == "document") {
             await cvr.resetSettings();
             let params = await cvr.getSimplifiedSettings("DetectDocumentBoundaries_Default");
             params.outputOriginalImage = true;
             await cvr.updateSettings("DetectDocumentBoundaries_Default", params);
             cvr.startCapturing("DetectDocumentBoundaries_Default");
         }
     }
    
     async function stopScanning() {
         if (!isDetecting) {
             return; // Not scanning
         }
    
         isDetecting = false;
    
         if (cvr != null) {
             await cvr.stopCapturing();
         }
    
         if (cameraView) {
             cameraView.clearAllInnerDrawingItems();
         }
     }
    
  4. Display the detection results in real-time:

     async function showCameraResult(result) {
         let selectedMode = document.querySelector('input[name="scanMode"]:checked').value;
         let items = result.items;
         let scan_result = document.getElementById('scan_result');
         scan_result.innerHTML = "";
         let txts = [];
    
         clearOverlay(cameraEnhancer);
    
         let type;
         if (selectedMode == "barcode") {
             type = Dynamsoft.Core.EnumCapturedResultItemType.CRIT_BARCODE;
         }
         else if (selectedMode == "mrz") {
             type = Dynamsoft.Core.EnumCapturedResultItemType.CRIT_TEXT_LINE;
         }
         else if (selectedMode == "document") {
             type = Dynamsoft.Core.EnumCapturedResultItemType.CRIT_DETECTED_QUAD
         }
        
         if (items != null && items.length > 0) {
             for (var i = 0; i < items.length; ++i) {
                 let item = items[i];
                 if (items[i].type === type) {
                     txts.push(item.text);
                     globalPoints = item.location.points;
        
                     if (selectedMode == "barcode") {
                         if (txts.length > 0) {
                             scan_result.innerHTML += txts.join('\n') + '\n\n';
                         }
                         else {
                             scan_result.innerHTML += "Recognition Failed\n";
                         }
                     }
                     else if (selectedMode == "mrz") {
                         if (txts.length > 0) {
                             scan_result.innerHTML += txts.join('\n') + '\n\n';
                             parseResults = await parser.parse(item.text);
                             scan_result.innerHTML += JSON.stringify(extractMrzInfo(parseResults));
                         }
                         else {
                             scan_result.innerHTML += "Recognition Failed\n";
                         }
        
                     }
                 }
                 else if (items[i].type === Dynamsoft.Core.EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE) {
                     if (selectedMode == "document") {
                         if (isCaptured) {
                             isCaptured = false;
                             await stopScanning();
                             targetCanvas.width = resolution.width;
                             targetCanvas.height = resolution.height;
                             openEditor(item.imageData.toCanvas().toDataURL());
                         }
                     }
                 }
             }
        
         }
         else {
             scan_result.innerHTML += "Nothing found\n";
         }
     }
    

Testing the Web Scanner Application

Barcode Scanner

MRZ Scanner

Document Scanner

Source Code

https://github.com/yushulx/javascript-barcode-qr-code-scanner/tree/main/examples/barcode_mrz_document