Steps to Develop an Angular Passport MRZ Reader & Scanner
The Dynamsoft Label Recognizer JavaScript SDK offers a robust OCR engine capable of extracting MRZ (Machine Readable Zone) characters from passports, visas, and other travel documents. It is a key part of Dynamsoft’s Capture Vision Suite, which also includes the Barcode Reader, Camera Enhancer, and Document Normalizer. This article will guide you through integrating the SDK into an Angular web application, enabling MRZ scanning from both image files and live camera streams.
This article is Part 4 in a 5-Part Series.
- Part 1 - How to Digitize Paper Documents in Angular Web Applications
- Part 2 - How to Build an Angular Barcode & QR Code Detection App: A Step-by-Step Guide
- Part 3 - How to Detect and Rectify Documents in Angular Web Applications
- Part 4 - Steps to Develop an Angular Passport MRZ Reader & Scanner
- Part 5 - Building an Angular Document Viewer for Image Loading, Annotation, and PDF Export
Angular MRZ Reader & Scanner
Prerequisites
- Node.js
-
Angular CLI
npm install -g @angular/cli ng --version
Step 1: Install Dynamsoft Label Recognizer and Dependencies
-
Install the SDK: Use npm to download and install the necessary packages
dynamsoft-label-recognizer
,dynamsoft-code-parser
,dynamsoft-capture-vision-dnn
, anddynamsoft-label-recognizer-data
.npm i dynamsoft-label-recognizer dynamsoft-code-parser dynamsoft-capture-vision-dnn dynamsoft-label-recognizer-data
Explanation
- The
dynamsoft-label-recognizer-data
package contains pre-trained models for MRZ detection. - The
dynamsoft-capture-vision-dnn
package includes a neural network engine used by thedynamsoft-label-recognizer
package. - The
dynamsoft-code-parser
package is responsible for parsing and extracting MRZ data.
- The
-
Configure Asset Path: Update
angular.json
to include the paths to the new packages."assets": [ "src/favicon.ico", "src/assets", ... { "glob": "**/*", "input": "./node_modules/dynamsoft-label-recognizer/dist", "output": "assets/dynamsoft-label-recognizer" }, { "glob": "**/*", "input": "./node_modules/dynamsoft-code-parser/dist", "output": "assets/dynamsoft-code-parser" }, { "glob": "**/*", "input": "./node_modules/dynamsoft-capture-vision-dnn/dist", "output": "assets/dynamsoft-capture-vision-dnn" }, { "glob": "**/*", "input": "./node_modules/dynamsoft-label-recognizer-data/dist", "output": "assets/dynamsoft-label-recognizer-data" } ],
-
Update Resource Paths in TypeScript Code: Define the full resource paths in the
CoreModule.engineResourcePaths
object within theproduct-list.component.ts
file.export function getFullUrl(endpoint: string): string { let baseUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}`; return `${baseUrl}${endpoint}`; } CoreModule.engineResourcePaths = { std: getFullUrl('assets/dynamsoft-capture-vision-std/'), dip: getFullUrl('assets/dynamsoft-image-processing/'), core: getFullUrl('assets/dynamsoft-core/'), license: getFullUrl('assets/dynamsoft-license/'), cvr: getFullUrl('assets/dynamsoft-capture-vision-router/'), dbr: getFullUrl('assets/dynamsoft-barcode-reader/'), dce: getFullUrl('assets/dynamsoft-camera-enhancer/'), ddn: getFullUrl('assets/dynamsoft-document-normalizer/'), dlr: getFullUrl('assets/dynamsoft-label-recognizer/'), dcp: getFullUrl('assets/dynamsoft-code-parser/'), dnn: getFullUrl('assets/dynamsoft-capture-vision-dnn/'), dlrData: getFullUrl('assets/dynamsoft-label-recognizer-data/'), };
Step 2: Add Angular MRZ Reader and MRZ Scanner Components
-
Generate Components: Use the Angular CLI to create two new components: MRZ Reader and MRZ Scanner.
ng generate component mrz-reader ng generate component mrz-scanner
-
Configure Routing: Update the
app-routing.module.ts
file to include routes for the new components.import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { ProductListComponent } from './product-list/product-list.component'; import { BarcodeReaderComponent } from './barcode-reader/barcode-reader.component'; import { BarcodeScannerComponent } from './barcode-scanner/barcode-scanner.component'; import { FileDetectionComponent } from './file-detection/file-detection.component'; import { CameraDetectionComponent } from './camera-detection/camera-detection.component'; import { MrzReaderComponent } from './mrz-reader/mrz-reader.component'; import { MrzScannerComponent } from './mrz-scanner/mrz-scanner.component'; const routes: Routes = [ { path: '', component: ProductListComponent }, { path: 'barcode-reader', component: BarcodeReaderComponent }, { path: 'barcode-scanner', component: BarcodeScannerComponent }, { path: 'file-detection', component: FileDetectionComponent }, { path: 'camera-detection', component: CameraDetectionComponent }, { path: 'mrz-reader', component: MrzReaderComponent }, { path: 'mrz-scanner', component: MrzScannerComponent } ];
-
Update Product List Template: Add navigation links to the new components in the
product-list.component.html
file.<h3> <div> <ng-template [ngIf]="product.id === 'reader'"> <a [title]="product.name + ' details'" [routerLink]="['/barcode-reader']">> </a> </ng-template> </div> <div> <ng-template [ngIf]="product.id === 'scanner'"> <a [title]="product.name + ' details'" [routerLink]="['/barcode-scanner']">> </a> </ng-template> </div> <div> <ng-template [ngIf]="product.id === 'file-detection'"> <a [title]="product.name + ' details'" [routerLink]="['/file-detection']">> </a> </ng-template> </div> <div> <ng-template [ngIf]="product.id === 'camera-detection'"> <a [title]="product.name + ' details'" [routerLink]="['/camera-detection']">> </a> </ng-template> </div> <div> <ng-template [ngIf]="product.id === 'mrz-reader'"> <a [title]="product.name + ' details'" [routerLink]="['/mrz-reader']">> </a> </ng-template> </div> <div> <ng-template [ngIf]="product.id === 'mrz-scanner'"> <a [title]="product.name + ' details'" [routerLink]="['/mrz-scanner']">> </a> </ng-template> </div> </h3>
Step 3: Configure the MRZ Recognition Template
The Dynamsoft Label Recognizer allows you to customize templates for various scenarios. For detecting MRZ characters in passports and other travel documents, we will create a template named ReadMRZ
and save it to assets/template.json
.
{
"CaptureVisionTemplates": [
{
"Name": "ReadMRZ",
"OutputOriginalImage": 0,
"ImageROIProcessingNameArray": [
"roi-mrz"
],
"Timeout": 2000
}
],
"TargetROIDefOptions": [
{
"Name": "roi-mrz",
"TaskSettingNameArray": [
"task-mrz"
]
}
],
"TextLineSpecificationOptions": [
{
"Name": "tls-mrz-passport",
"BaseTextLineSpecificationName": "tls-base",
"StringLengthRange": [
44,
44
],
"OutputResults": 1,
"ExpectedGroupsCount": 1,
"ConcatResults": 1,
"ConcatSeparator": "\n",
"SubGroups": [
{
"StringRegExPattern": "(P[A-Z<][A-Z<]{3}[A-Z<]{39}){(44)}",
"StringLengthRange": [
44,
44
],
"BaseTextLineSpecificationName": "tls-base"
},
{
"StringRegExPattern": "([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{14}[0-9<][0-9]){(44)}",
"StringLengthRange": [
44,
44
],
"BaseTextLineSpecificationName": "tls-base"
}
]
},
{
"Name": "tls-mrz-visa-td3",
"BaseTextLineSpecificationName": "tls-base",
"StringLengthRange": [
44,
44
],
"OutputResults": 1,
"ExpectedGroupsCount": 1,
"ConcatResults": 1,
"ConcatSeparator": "\n",
"SubGroups": [
{
"StringRegExPattern": "(V[A-Z<][A-Z<]{3}[A-Z<]{39}){(44)}",
"StringLengthRange": [
44,
44
],
"BaseTextLineSpecificationName": "tls-base"
},
{
"StringRegExPattern": "([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{14}[A-Z0-9<]{2}){(44)}",
"StringLengthRange": [
44,
44
],
"BaseTextLineSpecificationName": "tls-base"
}
]
},
{
"Name": "tls-mrz-visa-td2",
"BaseTextLineSpecificationName": "tls-base",
"StringLengthRange": [
36,
36
],
"OutputResults": 1,
"ExpectedGroupsCount": 1,
"ConcatResults": 1,
"ConcatSeparator": "\n",
"SubGroups": [
{
"StringRegExPattern": "(V[A-Z<][A-Z<]{3}[A-Z<]{31}){(36)}",
"StringLengthRange": [
36,
36
],
"BaseTextLineSpecificationName": "tls-base"
},
{
"StringRegExPattern": "([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{8}){(36)}",
"StringLengthRange": [
36,
36
],
"BaseTextLineSpecificationName": "tls-base"
}
]
},
{
"Name": "tls-mrz-id-td2",
"BaseTextLineSpecificationName": "tls-base",
"StringLengthRange": [
36,
36
],
"OutputResults": 1,
"ExpectedGroupsCount": 1,
"ConcatResults": 1,
"ConcatSeparator": "\n",
"SubGroups": [
{
"StringRegExPattern": "([ACI][A-Z<][A-Z<]{3}[A-Z<]{31}){(36)}",
"StringLengthRange": [
36,
36
],
"BaseTextLineSpecificationName": "tls-base"
},
{
"StringRegExPattern": "([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{8}){(36)}",
"StringLengthRange": [
36,
36
],
"BaseTextLineSpecificationName": "tls-base"
}
]
},
{
"Name": "tls-mrz-id-td1",
"BaseTextLineSpecificationName": "tls-base",
"StringLengthRange": [
30,
30
],
"OutputResults": 1,
"ExpectedGroupsCount": 1,
"ConcatResults": 1,
"ConcatSeparator": "\n",
"SubGroups": [
{
"StringRegExPattern": "([ACI][A-Z<][A-Z<]{3}[A-Z0-9<]{9}[0-9][A-Z0-9<]{15}){(30)}",
"StringLengthRange": [
30,
30
],
"BaseTextLineSpecificationName": "tls-base"
},
{
"StringRegExPattern": "([0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z<]{3}[A-Z0-9<]{11}[0-9]){(30)}",
"StringLengthRange": [
30,
30
],
"BaseTextLineSpecificationName": "tls-base"
},
{
"StringRegExPattern": "([A-Z<]{30}){(30)}",
"StringLengthRange": [
30,
30
],
"BaseTextLineSpecificationName": "tls-base"
}
]
},
{
"Name": "tls-base",
"CharacterModelName": "MRZ",
"CharHeightRange": [
5,
1000,
1
],
"BinarizationModes": [
{
"BlockSizeX": 30,
"BlockSizeY": 30,
"Mode": "BM_LOCAL_BLOCK",
"EnableFillBinaryVacancy": 0,
"ThresholdCompensation": 15
}
],
"ConfusableCharactersCorrection": {
"ConfusableCharacters": [
[
"0",
"O"
],
[
"1",
"I"
],
[
"5",
"S"
]
],
"FontNameArray": [
"OCR_B"
]
}
}
],
"LabelRecognizerTaskSettingOptions": [
{
"Name": "task-mrz",
"ConfusableCharactersPath": "ConfusableChars.data",
"TextLineSpecificationNameArray": [
"tls-mrz-passport",
"tls-mrz-visa-td3",
"tls-mrz-id-td1",
"tls-mrz-id-td2",
"tls-mrz-visa-td2"
],
"SectionImageParameterArray": [
{
"Section": "ST_REGION_PREDETECTION",
"ImageParameterName": "ip-mrz"
},
{
"Section": "ST_TEXT_LINE_LOCALIZATION",
"ImageParameterName": "ip-mrz"
},
{
"Section": "ST_TEXT_LINE_RECOGNITION",
"ImageParameterName": "ip-mrz"
}
]
}
],
"CharacterModelOptions": [
{
"DirectoryPath": "",
"Name": "MRZ"
}
],
"ImageParameterOptions": [
{
"Name": "ip-mrz",
"TextureDetectionModes": [
{
"Mode": "TDM_GENERAL_WIDTH_CONCENTRATION",
"Sensitivity": 8
}
],
"BinarizationModes": [
{
"EnableFillBinaryVacancy": 0,
"ThresholdCompensation": 21,
"Mode": "BM_LOCAL_BLOCK"
}
],
"TextDetectionMode": {
"Mode": "TTDM_LINE",
"CharHeightRange": [
5,
1000,
1
],
"Direction": "HORIZONTAL",
"Sensitivity": 7
}
}
]
}
Step 4: Recognize and Extract MRZ Characters from Image Files
Basic Steps
- Upload an image file.
- Recognize the MRZ characters with the Dynamsoft Capture Vision SDK.
- Extract the MRZ data from the recognized text using the Dynamsoft Code Parser SDK.
- Display the results in a text area.
UI Design
Add the following HTML code to the mrz-reader.component.html
file:
<div id="loading-indicator" class="loading-indicator" *ngIf="!isLoaded">
<div class="spinner"></div>
</div>
<div>
<input type="file" title="file" id="file" accept="image/*" (change)="onChange($event)" />
</div>
<div class="container" id="file_container">
<div class="row">
<div id="imageview" class="imageview">
<img id="image" alt="" />
<canvas id="overlay" class="overlay"></canvas>
</div>
</div>
</div>
<div>
<textarea id="detection_result"></textarea>
</div>
Explanation
- The file input element allows users to upload an image file.
- The image element displays the uploaded image.
- The overlay canvas draws the text area boundaries.
- The text area displays the MRZ data extracted from the image.
Component Implementation
-
Initialize the SDK: Create an instance of
CaptureVisionRouter
and load the MRZ template.ngOnInit(): void { this.overlayManager.initOverlay(document.getElementById('overlay') as HTMLCanvasElement); (async () => { try { await CodeParserModule.loadSpec("MRTD_TD1_ID"); await CodeParserModule.loadSpec("MRTD_TD2_FRENCH_ID"); await CodeParserModule.loadSpec("MRTD_TD2_ID"); await CodeParserModule.loadSpec("MRTD_TD2_VISA"); await CodeParserModule.loadSpec("MRTD_TD3_PASSPORT"); await CodeParserModule.loadSpec("MRTD_TD3_VISA"); await LabelRecognizerModule.loadRecognitionData("MRZ"); this.cvr = await CaptureVisionRouter.createInstance(); this.parser = await CodeParser.createInstance(); let ret = await this.cvr.initSettings('assets/template.json'); console.log(ret); } catch (ex) { alert(ex); } this.isLoaded = true; })(); }
-
Load an image file:
onChange(event: Event) { let parser = this.parser; const element = event.currentTarget as HTMLInputElement; let textarea = document.getElementById('detection_result') as HTMLTextAreaElement; let fileList: FileList | null = element.files; if (fileList) { let file = fileList.item(0) as any; if (file) { let fr = new FileReader(); fr.onload = (event: any) => { let image = document.getElementById('image') as HTMLImageElement; if (image) { image.src = event.target.result; const img = new Image(); textarea.value = ''; img.onload = (event: any) => { this.overlayManager.updateOverlay(img.width, img.height); // TODO }; img.src = event.target.result; } }; fr.readAsDataURL(file); } } }
-
Recognize MRZ: Call the
capture
method with theReadMRZ
template to recognize MRZ from the image file.if (this.cvr) { this.cvr.capture(file, "ReadMRZ").then(async (result: CapturedResult) => { let txts: any = []; try { let items = result.items; if (items.length > 0) { for (var i = 0; i < items.length; ++i) { if (items[i].type !== EnumCapturedResultItemType.CRIT_TEXT_LINE) { continue; // check if captured result item is a barcode } // TODO break; } } } catch (e) { alert(e); } if (txts.length === 0) { textarea.value = 'No MRZ detected.'; } }); }
-
Display the Results: Cast the
CapturedResult
toTextLineResultItem
and get the MRZ characters using theCodeParser
instance.let item = items[i] as TextLineResultItem; let localization = item.location; this.overlayManager.drawOverlay( localization, '' ); txts.push(item.text); textarea.value = txts.join('') + '\n'; let parseResults = await parser?.parse(item.text); textarea.value += JSON.stringify(handleMrzParseResult(parseResults!)) + '\n'; export function handleMrzParseResult(result: ParsedResultItem): any { const parseResultInfo: any = {}; 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; }
Step 5: Recognize and Extract MRZ Characters from Camera Stream
Basic Steps
- Register an event listener with
CaptureVisionRouter
to receive detected text results. - Bind a
CameraEnhancer
instance to theCaptureVisionRouter
instance. - Start the camera stream and MRZ recognition.
- Parse and extract the MRZ data in the callback function.
- Display the results in a text area.
UI Design
Add the following HTML code to the mrz-scanner.component.html
file:
<div id="loading-indicator" class="loading-indicator" *ngIf="!isLoaded">
<div class="spinner"></div>
</div>
<div class="select">
<label for="videoSource">Video source: </label>
<select id="videoSource" (change)="openCamera()"></select>
</div>
<div class="container">
<div id="videoview">
<div class="dce-video-container" id="videoContainer"></div>
<canvas id="overlay"></canvas>
</div>
</div>
<div>
<textarea id="result"></textarea>
</div>
Explanation
- The select element allows users to choose a video source.
- The video container displays the camera stream.
- The overlay canvas draws the text area boundaries in real-time.
- The text area displays the MRZ data extracted from the camera stream.
Component Implementation
-
Initialize the SDK and Register Callback: Create an instance of
CaptureVisionRouter
, load the MRZ template, and register a callback function to receive detected document edges. Bind a camera view to theCaptureVisionRouter
instance.ngOnInit(): void { this.videoSelect = document.querySelector('select#videoSource') as HTMLSelectElement; this.overlayManager.initOverlay(document.getElementById('overlay') as HTMLCanvasElement); (async () => { await CodeParserModule.loadSpec("MRTD_TD1_ID"); await CodeParserModule.loadSpec("MRTD_TD2_FRENCH_ID"); await CodeParserModule.loadSpec("MRTD_TD2_ID"); await CodeParserModule.loadSpec("MRTD_TD2_VISA"); await CodeParserModule.loadSpec("MRTD_TD3_PASSPORT"); await CodeParserModule.loadSpec("MRTD_TD3_VISA"); await LabelRecognizerModule.loadRecognitionData("MRZ"); this.cvr = await CaptureVisionRouter.createInstance(); this.parser = await CodeParser.createInstance(); let ret = await this.cvr.initSettings('assets/template.json'); console.log(ret); await this.initBarcodeScanner(); })(); } async initBarcodeScanner(): Promise<void> { const cameraView: CameraView = await CameraView.createInstance(); this.cameraEnhancer = await CameraEnhancer.createInstance(cameraView); let uiElement = document.getElementById('videoContainer'); if (uiElement) { uiElement.append(cameraView.getUIElement()); cameraView.getUIElement().shadowRoot?.querySelector('.dce-sel-camera')?.setAttribute('style', 'display: none'); cameraView.getUIElement().shadowRoot?.querySelector('.dce-sel-resolution')?.setAttribute('style', 'display: none'); let cameras = await this.cameraEnhancer.getAllCameras(); this.listCameras(cameras); if (this.isDestroyed) { throw Error(componentDestroyedErrorMsg); } this.cvr?.setInput(this.cameraEnhancer); this.cvr?.addResultReceiver({ onCapturedResultReceived: async (result: CapturedResult) => { // TODO }, }); this.cameraEnhancer.on('played', () => { this.updateResolution(); }); await this.openCamera(); if (this.isDestroyed) { throw Error(componentDestroyedErrorMsg); } await this.cvr?.startCapturing('ReadMRZ'); if (this.isDestroyed) { throw Error(componentDestroyedErrorMsg); } this.isLoaded = true; } }
-
Process the MRZ Recognition Results: Implement the callback function to handle MRZ recognition results.
onCapturedResultReceived: async (result: CapturedResult) => { this.overlayManager.clearOverlay(); let txts: any = []; let resultElement = document.getElementById('result'); try { let localization; let items = result.items if (items.length > 0) { for (var i = 0; i < items.length; ++i) { if (items[i].type !== EnumCapturedResultItemType.CRIT_TEXT_LINE) { continue; } let item = items[i] as TextLineResultItem; txts.push(item.text); localization = item.location; this.overlayManager.drawOverlay( localization, '' ); let parseResults = await this.parser?.parse(item.text); if (resultElement) { resultElement.innerHTML = JSON.stringify(handleMrzParseResult(parseResults!)) + '\n'; } break; } } } catch (e) { alert(e); } },
Source Code
https://github.com/yushulx/angular-barcode-mrz-document-scanner