How to Build an MRZ Scanner in React

MRZ stands for “machine-readable zone”. It is usually at the bottom of an identity page for machines to read its info like document type, name, nationality, date of birth, sex and expiration date, etc.

Dynamsoft Label Recognizer can read MRZ with sophisticated image processing algorithms and provides a JavaScript edition. In this article, we are going to talk about how to build an MRZ scanner in React with Dynamsoft Label Recognizer.

Demo video:

Online demo

New Project

Create a new React project with Vite:

npm create vite@latest MRZScanner -- --template react-ts

Add Dependencies

Install Dynamsoft Label Recognizer and Dynamsoft Camera Enhancer (for camera control):

npm install dynamsoft-label-recognizer dynamsoft-camera-enhancer

Create an MRZ Scanner Component

  1. Create a component file under component/MRZScanner.tsx with the following template:

    import React, { MutableRefObject, ReactNode, useEffect, useRef } from 'react';
    import './MRZScanner.css';
    export interface ScannerProps {
      children?: ReactNode;
      hideSelect?: Boolean;
    }
    const MRZScanner = (props:ScannerProps): React.ReactElement => {
      const container: MutableRefObject<HTMLDivElement|null> = useRef(null);
      return (
        <div ref={container}>
          <svg className="dce-bg-loading" viewBox="0 0 1792 1792"><path d="M1760 896q0 176-68.5 336t-184 275.5-275.5 184-336 68.5-336-68.5-275.5-184-184-275.5-68.5-336q0-213 97-398.5t265-305.5 374-151v228q-221 45-366.5 221t-145.5 406q0 130 51 248.5t136.5 204 204 136.5 248.5 51 248.5-51 204-136.5 136.5-204 51-248.5q0-230-145.5-406t-366.5-221v-228q206 31 374 151t265 305.5 97 398.5z" /></svg>
          <svg className="dce-bg-camera" viewBox="0 0 2048 1792"><path d="M1024 672q119 0 203.5 84.5t84.5 203.5-84.5 203.5-203.5 84.5-203.5-84.5-84.5-203.5 84.5-203.5 203.5-84.5zm704-416q106 0 181 75t75 181v896q0 106-75 181t-181 75h-1408q-106 0-181-75t-75-181v-896q0-106 75-181t181-75h224l51-136q19-49 69.5-84.5t103.5-35.5h512q53 0 103.5 35.5t69.5 84.5l51 136h224zm-704 1152q185 0 316.5-131.5t131.5-316.5-131.5-316.5-316.5-131.5-316.5 131.5-131.5 316.5 131.5 316.5 316.5 131.5z" /></svg>
          <div className="dce-video-container"></div>
             
          <div className="dce-scanarea">
            <div className="dce-scanlight"></div>
          </div>
          {!props.hideSelect &&
            <div className="sel-container">
              <select className="dce-sel-camera"></select>
              <select className="dce-sel-resolution"></select>
            </div>
          }
          {props.children}
        </div>
      );
    }
    
    export default MRZScanner;
    

    A container is used for holding the elements of the scanner. Its styles are included in MRZScanner.css:

    @keyframes dce-rotate{from{transform:rotate(0turn);}to{transform:rotate(1turn);}}
    @keyframes dce-scanlight{from{top:0;}to{top:97%;}}
    .container{width:90%;height:65vh;max-width: 1200px;background:#eee;position:relative;margin: 0 auto;}
    .dce-bg-loading{animation:1s linear infinite dce-rotate;width:40%;height:40%;position:absolute;margin:auto;left:0;top:0;right:0;bottom:0;fill:#aaa;}
    .dce-bg-camera{display:none;width:40%;height:40%;position:absolute;margin:auto;left:0;top:0;right:0;bottom:0;fill:#aaa;}
    .dce-video-container{position:absolute;left:0;top:0;width:100%;height:100%;}
    .dce-scanarea{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;}
    .dce-scanarea .dce-scanlight{display:none;position:absolute;width:100%;height:3%;border-radius:50%;box-shadow:0px 0px 2vw 1px #00e5ff;background:#fff;animation:3s infinite dce-scanlight;user-select:none;}
    .sel-container{position: absolute;left: 0;top: 0;}
    .sel-container .dce-sel-camera{display:block;}
    .sel-container .dce-sel-resolution{display:block;margin-top:5px;}
    
  2. Initialize Dynamsoft Camera Enhancer when the component is mounted and dispose of it when it is unmounted using useEffect.

    const defaultDCEEngineResourcePath = "https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@3.3.4/dist/";
    const MRZScanner = (props:ScannerProps): React.ReactElement => {
      const configSet = useRef(false);
      const dce = useRef<CameraEnhancer|null>(null);
      useEffect(() => {
        const init = async () => {
          try{
            if (configSet.current === false) {
              CameraEnhancer.engineResourcePath = defaultDCEEngineResourcePath;
              configSet.current = true;
            }
            dce.current = await CameraEnhancer.createInstance();
            await dce.current.setUIElement(container.current as HTMLDivElement);
            dce.current.setVideoFit("cover");
          } catch(ex:any) {
            console.error(ex);
          }
        }
        init();
        return () => {
          if (dce.current) {
            dce.current.dispose(true);
          }
        }
      }, []);
    }
    
  3. Initialize Dynamsoft Label Recognizer and bind it with Dynamsoft Camera Enhancer when the component is mounted and dispose of it when it is unmounted using useEffect. A license is required to use Dynamsoft Label Recognizer. You can apply for a license here.

    const defaultDLRengineResourcePath = "https://cdn.jsdelivr.net/npm/dynamsoft-label-recognizer@2.2.30/dist/";
    const MRZScanner = (props:ScannerProps): React.ReactElement => {
      const configSet = useRef(false);
      const dlr = useRef<LabelRecognizer|null>(null);
      useEffect(() => {
        const init = async () => {
          try{
            if (LabelRecognizer.isWasmLoaded() === false && configSet.current === false) {
              CameraEnhancer.engineResourcePath = defaultDCEEngineResourcePath;
              LabelRecognizer.engineResourcePath = defaultDLRengineResourcePath;
              LabelRecognizer.license = props.license ?? "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="; //one-day trial
              configSet.current = true;
            }
            LabelRecognizer.onResourcesLoadStarted = () => { 
              if (props.onResourcesLoadStarted) {
                props.onResourcesLoadStarted();
              }
            }
            LabelRecognizer.onResourcesLoadProgress = (resourcesPath, progress)=>{
              if (props.onResourcesLoadProgress) {
                if (resourcesPath && progress) {
                  props.onResourcesLoadProgress(resourcesPath,progress);
                }
              }
            };
            LabelRecognizer.onResourcesLoaded = async () => { 
              if (props.onResourcesLoaded) {
                props.onResourcesLoaded();
              }
            }
    
            dlr.current = await LabelRecognizer.createInstance();
            dce.current = await CameraEnhancer.createInstance();
            await dlr.current.setImageSource(dce.current, {resultsHighlightBaseShapes: DrawingItem});
            await dlr.current.updateRuntimeSettingsFromString("video-mrz");
            await dce.current.setUIElement(container.current as HTMLDivElement);
            dce.current.setVideoFit("cover");
            dlr.current.onMRZRead = async (_txt, results) => {
              props.onScanned(results);
            }
          } catch(ex:any) {
            let errMsg: string;
            if (ex.message.includes("network connection error")) {
              errMsg = "Failed to connect to Dynamsoft License Server: network connection error. Check your Internet connection or contact Dynamsoft Support (support@dynamsoft.com) to acquire an offline license.";
            } else {
              errMsg = ex.message||ex;
            }
            console.error(errMsg);
            alert(errMsg);
          }
        }
        init();
        return () => {
          if (dlr.current && dce.current) {
            dlr.current.destroyContext();
            dce.current.dispose(true);
          }
        }
      }, []);  
    }
    

    The scanner props are modified to add props related to Dynamsoft Label Recognizer. Dynamsoft Label Recognizer needs to download an OCR model about 2MB to the browser. The progress can be monitored using the events starting with onResources.

     export interface ScannerProps {
    +  license?: string;
       children?: ReactNode;
       hideSelect?: boolean;
    +  onInitialized?: (dce:CameraEnhancer,dlr:LabelRecognizer) => void;
    +  onScanned: (results:DLRLineResult[]) => void;
    +  onResourcesLoadStarted?: () => void;
    +  onResourcesLoadProgress?: (resourcesPath:string, progress:{loaded: number; total: number;}) => void;
    +  onResourcesLoaded?: () => void;
     }
    
  4. Add a scanning prop to control the scanning status.

    Props:

    export interface ScannerProps {
      //...
      scanning:boolean;
      //...
    }
    

    In the onmount useEffect, if the scanning prop is true, start scanning MRZ.

    useEffect(() => {
     const init = async () => {
       //...
       if (props.scanning) {
         dlr.current.startScanning(true);
       }
       //...
     }
     init();
    }, []);
    

    If the scanning prop is changed, update the scanning status:

    useEffect(() => {
      if (dlr.current) {
        if (props.scanning) {
          if ((dlr.current as any)._bPauseScan) {
            dlr.current.resumeScanning();
          }else{
            dlr.current.startScanning(true);
          }
        }else{
          dlr.current.pauseScanning();
        }
      }
    }, [props.scanning]);
    

Use the MRZ Scanner Component

  1. Modify App.tsx to add the MRZ scanner component. The component is mounted when the start scanning button is clicked.

    function App() {
      const [scanning, setScanning] = useState(false);
      const [showScanner, setShowScanner] = useState(false);
      const startScanner = () => {
        setScanning(true);
        setShowScanner(true);
      }
      const stopScanner = () => {
        setScanning(false);
        setShowScanner(false);
      }
      return (
        <div>
          {!showScanner &&
            <>
              <h2>MRZ Scanner</h2>
              <button onClick={()=>startScanner()}>Start Scanning MRZ</button>
            </>
          }
          {showScanner && 
            <MRZScanner
              scanning={scanning}
              onScanned={(results)=>(console.log(results))}
            >
              <button className="close-button" onClick={()=>stopScanner()}>Close</button>
            </MRZScanner>
          }
        </div>
      )
    }
    

    CSS of the close button:

    .close-button {
      position: absolute;
      top: 0;
      right: 0;
    }
    
  2. Display a confirmation modal for users to check whether the MRZ result is correct. When the modal is shown, pause scanning. If the result is confirmed, stop the scanner and display the MRZ result below the start scanning button. Otherwise, resume scanning.

    function App() {
      const [scanning, setScanning] = useState(false);
      const [MRZLineResults, setMRZLineResults] = useState<DLRLineResult[]>([]);
      const [showScanner, setShowScanner] = useState(false);
      const [showConfirmation, setShowConfirmation] = useState(false);
      const [confirmed, setConfirmed] = useState(false);
    
      const showConfirmationModal = (results:DLRLineResult[]) => {
        if (scanning === true) {
          setConfirmed(false);
          setScanning(false);
          setShowConfirmation(true);
          setMRZLineResults(results);
        }
      }
    
      const MRZString = () => {
        let str = "";
        for (let index = 0; index < MRZLineResults.length; index++) {
          const lineResult = MRZLineResults[index];
          str = str + lineResult.text;
          if (index != MRZLineResults.length - 1) {
            str = str + "\n";
          }
        }
        return str;
      }
    
      const correct = () => {
        setConfirmed(true);
        setShowConfirmation(false);
        setShowScanner(false);
      }
    
      const rescan = () => {
        setShowConfirmation(false);
        setScanning(true);
      }
    
      const startScanner = () => {
        setShowConfirmation(false);
        setScanning(true);
        setShowScanner(true);
      }
    
      const stopScanner = () => {
        setScanning(false);
        setShowScanner(false);
      }
      return (
        <div>
          {!showScanner &&
            <>
              <h2>MRZ Scanner</h2>
              <button onClick={()=>startScanner()}>Start Scanning MRZ</button>
              {confirmed && MRZLineResults.length>0 &&
                <pre>{MRZString()}</pre>
              }
            </>
          }
          {showScanner && 
            <MRZScanner
              scanning={scanning}
              onScanned={(results)=>(showConfirmationModal(results))}
            >
              {showConfirmation && 
                <div className="confirmation modal">
                  <pre>
                    {MRZString()}
                  </pre>
                  <button onClick={()=>correct()}>Correct</button>
                  <button onClick={()=>rescan()} >Rescan</button>
                </div>
              }
              <button className="close-button" onClick={()=>stopScanner()}>Close</button>
            </MRZScanner>
          }
        </div>
      )
    }
    

    CSS:

    .modal {
      position: absolute;
      left: 50%;
      top: 50px;
      transform: translateX(-50%);
      z-index: 99999;
      background: #fff;
      padding: 10px;
      border: thick double black;
      border-radius: 5px;
      font-family: sans-serif;
    }
    .confirmation {
      text-align: center;
    }
    
    @media screen and (max-device-width: 600px){
      .confirmation {
        width: 90%;
      }
    }
    
    .confirmation pre {
      white-space: break-spaces;
      word-break: break-all;
    }
    

All right, we’ve finished writing the MRZ scanner in React.

Package as a Library

We can package the MRZ scanner as a React component library and publish it to NPM.

  1. Install dependencies.

    npm install -D @types/node vite-plugin-dts
    
  2. Create vite.config.ts with the following content:

    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    import path from 'node:path';
    import dts from 'vite-plugin-dts';
    
    export default defineConfig({
        plugins: [
            react(),
            dts({
                insertTypesEntry: true,
            }),
        ],
        build: {
            lib: {
                entry: path.resolve(__dirname, 'src/index.ts'),
                name: 'mrz-scanner',
                fileName: `mrz-scanner`,
            },
            rollupOptions: {
                external: ['react', 'react-dom'],
                output: {
                    globals: {
                        react: 'React',
                        'react-dom': 'ReactDOM'
                    },
                },
            },
        },
    });
    
  3. Update package.json to add the following:

    {
      "name": "react-mrz-scanner",
      "version": "0.0.0",
      "private": false,
      "type": "module",
      "files": [
        "dist"
      ],
      "main": "./dist/mrz-scanner.umd.js",
      "module": "./dist/mrz-scanner.js",
      "types": "./dist/index.d.ts",
      "exports": {
        "import": {
          "types": "./dist/index.d.ts",
          "default": "./dist/mrz-scanner.js"
        },
        "require": {
          "types": "./dist/index.d.ts",
          "default": "./dist/mrz-scanner.umd.js"
        }
      }
    }
    

Run npm run build. Then, we can have the packaged files in the dist which is ready for publishing to NPM:

DIST
│  App.d.ts
│  index.d.ts
│  main.d.ts
│  mrz-scanner.js
│  mrz-scanner.umd.cjs
│  style.css
│
└─component
        MRZScanner.d.ts

Source Code

Check out the source code of the demo to have a try:

https://github.com/tony-xlh/react-mrz-scanner