How to Build an MRZ Scanner in Vue

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 Vue with Dynamsoft Label Recognizer.

Demo video:

Online demo

New Project

Create a new Vue project with Vite:

npm create vite@latest MRZScanner -- --template vue-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 components/MRZScanner.vue with the following content:

    <script setup lang="ts">
    import { ref, type Ref} from "vue";
    const elRef: Ref<HTMLDivElement | null> = ref(null);
    </script>
    
    <template>
      <div ref="elRef">
        <svg class="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 class="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 class="dce-video-container"></div>
        <div class="dce-scanarea">
            <div class="dce-scanlight"></div>
        </div>
        <div v-if="hideSelect != true" class="sel-container">
            <select class="dce-sel-camera"></select>
            <select class="dce-sel-resolution"></select>
            <select class="dlr-sel-minletter"></select>
        </div>
        <slot></slot>
      </div>
    </template>
    
    <style scoped>
    @keyframes dce-rotate{from{transform:rotate(0turn);}to{transform:rotate(1turn);}}
    @keyframes dce-scanlight{from{top:0;}to{top:97%;}}
    .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;}
    .sel-container .dlr-sel-minletter{display:block;margin-top:5px;}
    </style>
    

    A container is used for holding the elements of the scanner.

  2. Initialize Dynamsoft Camera Enhancer when the component is mounted and dispose of it when it is unmounted.

    const defaultDCEEngineResourcePath = "https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@3.3.4/dist/";
    let dce:CameraEnhancer|null;
    onMounted(async()=>{
      try{
        CameraEnhancer.engineResourcePath = defaultDCEEngineResourcePath;
        dce = await CameraEnhancer.createInstance();
        await dce.setUIElement(elRef.value as HTMLDivElement);
        dce.setVideoFit("cover");
      } catch(ex:any) {
        console.log(ex);
      }
    })
    
    onUnmounted(() => {
      if (dce) {
        dce.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. 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/";
    let dce:CameraEnhancer|null;
    let dlr:LabelRecognizer|null;
    onMounted(async()=>{
      LabelRecognizer.onResourcesLoadStarted = () => { 
        emit("onResourcesLoadStarted");
      }
      LabelRecognizer.onResourcesLoadProgress = (resourcesPath?:string, progress?:{ loaded:number, total:number })=>{
        if (resourcesPath && progress) {
          emit("onResourcesLoadProgress",resourcesPath,progress);
        }
      };
      LabelRecognizer.onResourcesLoaded = () => { 
        emit("onResourcesLoaded");
      }
    
      try{
        if (LabelRecognizer.isWasmLoaded() === false) {
          CameraEnhancer.engineResourcePath = defaultDCEEngineResourcePath;
          LabelRecognizer.engineResourcePath = defaultDLRengineResourcePath;
          LabelRecognizer.license = props.license ?? "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="; //one-day trial
        }
        dce = await CameraEnhancer.createInstance();
        dlr = await LabelRecognizer.createInstance();
        await dce.setUIElement(elRef.value as HTMLDivElement);
        await dlr.setImageSource(dce, {resultsHighlightBaseShapes: DrawingItem});
        await dlr.updateRuntimeSettingsFromString("video-mrz");
        dce.setVideoFit("cover");
        // Callback to MRZ recognizing result
        dlr.onMRZRead = async (_txt, results) => {
          emit("scanned",results);
        }
        emit("initialized", dce, dlr);
      } 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);
      }
    })
    
    onUnmounted(async() => {
      if (dlr) {
        dlr.destroyContext();
      }
      if (dce) {
        dce.dispose(true);
      }
    })
    

    Props and emits are defined like the following:

    const props = defineProps({
      license: String
    })
    const emit = defineEmits<{
      (e: 'scanned', results: DLRLineResult[]): void
      (e: 'initialized', dce:CameraEnhancer,dlr:LabelRecognizer): void
      (e: 'onResourcesLoadStarted'): void
      (e: 'onResourcesLoadProgress',resourcesPath: string, progress: { loaded:number, total:number }): void
      (e: 'onResourcesLoaded'): void
    }>();
    

    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.

  4. Add a scanning prop to control the scanning status.

    Props:

    const props = defineProps({
      license: String,
      scanning: Boolean
    })
    

    In the onMounted function, if the scanning prop is true, start scanning MRZ.

    onMounted(async()=>{
      if (props.scanning) {
        await dlr.startScanning(true);
      }
    });
    

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

    watch(() => props.scanning, (newVal) => {
      if (dlr && dce) {
        if (newVal === true) {
          if ((dlr as any)._bPauseScan) {
            dlr.resumeScanning();
          }else{
            dlr.startScanning(true);
          }
        }else{
          dlr.stopScanning(true);
        }
      }
    });
    

Use the MRZ Scanner Component

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

    <script setup lang="ts">
    const scanning = ref(false);
    const showScanner = ref(false);
    const startScanner = () => {
      scanning.value = true;
      showScanner.value = true;
    }
    
    const stopScanner = () => {
      scanning.value = false;
      showScanner.value = false;
    }
    </script>
    <template>
      <div v-if="!showScanner">
        <h2>MRZ Scanner</h2>
        <button @click="startScanner">Start Scanning MRZ</button>
      </div>
      <div v-if="showScanner">
        <MRZScanner 
          :scanning="scanning"
          @scanned="(results)=>console.log(results)"
        >
          <button class="close-button" @click="stopScanner">Close</button>
        </MRZScanner>
      </div>
    </template>
    <style scoped>
    .close-button {
      position: absolute;
      top: 0;
      right: 0;
    }
    </style>
    
  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.

    <script setup lang="ts">
    import { ref } from 'vue';
    import MRZScanner from './components/MRZScanner.vue'
    import { DLRLineResult } from 'dynamsoft-label-recognizer';
    
    const scanning = ref(false);
    const showScanner = ref(false);
    const MRZLineResults = ref<DLRLineResult[]>([]);
    const confirmed = ref(false);
    const showConfirmation = ref(false);
    
    const startScanner = () => {
      confirmed.value = false;
      showConfirmation.value = false;
      scanning.value = true;
      showScanner.value = true;
    }
    
    const stopScanner = () => {
      showConfirmation.value = false;
      scanning.value = false;
      showScanner.value = false;
    }
    
    const onScanned = (results:DLRLineResult[]) => {
      MRZLineResults.value = results;
      showConfirmation.value = true;
    }
    
    const MRZString = () => {
      let str = "";
      for (let index = 0; index < MRZLineResults.value.length; index++) {
        const lineResult = MRZLineResults.value[index];
        str = str + lineResult.text;
        if (index != MRZLineResults.value.length - 1) {
          str = str + "\n";
        }
      }
      return str;
    }
    
    const correct = () => {
      confirmed.value = true;
      scanning.value = false;
      showScanner.value = false;
    }
    
    const rescan = () => {
      showConfirmation.value = false;
      scanning.value = true;
    }
    
    </script>
    
    <template>
      <div v-if="!showScanner">
        <h2>MRZ Scanner</h2>
        <button @click="startScanner">Start Scanning MRZ</button>
        <pre v-if="confirmed">{{ MRZString() }}</pre>
      </div>
      <div v-if="showScanner">
        <MRZScanner 
          :scanning="scanning"
          @scanned="(results)=>onScanned(results)"
        >
          <button class="close-button" @click="stopScanner">Close</button>
          <div v-if="showConfirmation" class="confirmation modal">
            <pre>{{MRZString()}}</pre>
            <button @click="correct">Correct</button>
            <button @click="rescan">Rescan</button>
          </div>
        </MRZScanner>
      </div>
    </template>
    
    <style scoped>
    .close-button {
      position: absolute;
      top: 0;
      right: 0;
    }
    
    .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;
    }
    </style>
    

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

Package as a Library

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

  1. Install dependencies.

    npm install -D @types/node vite-plugin-dts
    
  2. Create an index.ts file to export the component.

    import MRZScanner from './components/MRZScanner.vue'
    const install = (app:any) => {
      app.component('mrz-scanner', MRZScanner);
    }
    
    export default MRZScanner;
    
    const plugin = {
      install
    };
    
    export { plugin };
    
  3. Update vite.config.ts as the following:

    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import { resolve } from 'path';
    import dts from 'vite-plugin-dts';
    
    // https://vitejs.dev/guide/build.html#library-mode
    export default defineConfig({
      build: {
        lib: {
          entry: resolve(__dirname, 'src/index.ts'),
          name: 'mrz-scanner',
          fileName: 'mrz-scanner',
        },
        rollupOptions: {
          // make sure to externalize deps that shouldn't be bundled
          // into your library
          external: ['vue'],
          output: {
            // Provide global variables to use in the UMD build
            // for externalized deps
            globals: {
              vue: 'Vue',
            },
          },
        },
      },
      plugins: [vue(),dts()],
    });
    
  4. Update package.json to add the following:

    {
      "name": "vue-mrz-scanner",
      "version": "0.0.0",
      "private": false,
      "type": "module",
      "files": [
        "dist"
      ],
      "main": "./dist/mrz-scanner.umd.cjs",
      "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.cjs"
        }
      }
    }
    

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

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

Source Code

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

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