Machine Readable Zone (MRZ) codes are standardized text fields found on passports, ID cards, and visas that enable automated document verification. If you're developing document or identity–related scanning tools, you often need realistic test data. In this tutorial, we'll build a Chrome extension that generates mock MRZ codes for various document types using JavaScript and the Canvas API.
Disclaimer: This tool generates synthetic data for testing and development only. Do not use it to forge or simulate real identification documents.
Demo Video: Chrome Extension for MRZ Generation
Chrome Extension Installation
What We're Building
A Chrome extension that can:
- Generate ICAO 9303-compliant MRZ codes for:
- Passports (TD3)
- ID Cards (TD1)
- Travel Documents (TD2)
- Visas (MRVA, MRVB)
- Render document images with MRZ text in OCR-B font
- Copy or download generated images
- Copy MRZ codes to clipboard
- Work completely offline with no data collection
Understanding MRZ Structure
MRZ formats vary by document type:
TD3 (Passport) - 2 lines of 44 characters
P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<<
L898902C36UTO7408122F1204159ZE184226B<<<<<10
TD1 (ID Card) - 3 lines of 30 characters
I<UTOD231458907<<<<<<<<<<<<<<<
7408122F1204159UTO<<<<<<<<<<<6
ERIKSSON<<ANNA<MARIA<<<<<<<<<<
TD2 (Travel Document) - 2 lines of 36 characters
I<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<
D231458907UTO7408122F1204159<<<<<<<6
Key Components:
- Document type code (P = Passport, I = ID Card, V = Visa)
- Issuing country code (3 letters)
- Name (surname, given names separated by
<<) - Document number
- Date of birth (YYMMDD)
- Sex (M/F/X)
- Expiry date (YYMMDD)
- Check digits for validation
Project Setup
Let's create the basic project structure:
mrz-generator-extension/
├── manifest.json
├── popup.html
├── popup.css
├── popup.js
├── mrz-generator.js
├── README.md
├── PRIVACY-POLICY.md
├── fonts/
│ └── ocrbfont.ttf
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
Create a new directory and set up the basic files:
mkdir mrz-generator-extension
cd mrz-generator-extension
Creating the Extension Manifest
The manifest.json file is the blueprint of your Chrome extension. For Manifest V3, create:
{
"manifest_version": 3,
"name": "MRZ Generator",
"version": "1.0.0",
"description": "Generate mock Machine Readable Zone (MRZ) documents for testing purposes",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": {
"default_popup": "popup.html",
"default_title": "MRZ Generator"
},
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}
Key Points:
-
manifest_version: 3- Uses the latest Manifest V3 specification - No permissions required - Everything runs locally
-
action.default_popup- Defines the popup interface - CSP policy ensures secure script execution
Building the MRZ Generator
Create mrz-generator.js with the core MRZ generation logic. Let's start with the TD3 (Passport) generator:
Check Digit Calculation
The MRZ uses weighted check digits for validation:
class CodeGenerator {
checkDigit(input) {
const weights = [7, 3, 1];
const charValues = {
'<': 0, '0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
'5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14,
'F': 15, 'G': 16, 'H': 17, 'I': 18, 'J': 19,
'K': 20, 'L': 21, 'M': 22, 'N': 23, 'O': 24,
'P': 25, 'Q': 26, 'R': 27, 'S': 28, 'T': 29,
'U': 30, 'V': 31, 'W': 32, 'X': 33, 'Y': 34, 'Z': 35
};
let sum = 0;
for (let i = 0; i < input.length; i++) {
const char = input[i].toUpperCase();
const value = charValues[char] || 0;
sum += value * weights[i % 3];
}
return sum % 10;
}
// Pad string with '<' filler characters
pad(str, length) {
return (str + '<'.repeat(length)).substring(0, length);
}
// Format name with surname and given names
formatName(surname, givenNames) {
const formattedSurname = surname.toUpperCase().replace(/ /g, '<');
const formattedGivenNames = givenNames.toUpperCase().replace(/ /g, '<');
return formattedSurname + '<<' + formattedGivenNames;
}
}
TD3 (Passport) Generator
class TD3CodeGenerator extends CodeGenerator {
constructor(documentType, countryCode, surname, givenNames,
documentNumber, nationality, birthDate, sex,
expiryDate, optionalData) {
super();
this.documentType = documentType;
this.countryCode = countryCode;
this.surname = surname;
this.givenNames = givenNames;
this.documentNumber = documentNumber;
this.nationality = nationality;
this.birthDate = birthDate;
this.sex = sex;
this.expiryDate = expiryDate;
this.optionalData = optionalData || '';
}
generate() {
// Line 1: Document type, country, and name (44 characters)
const line1 =
this.documentType +
'<' +
this.pad(this.countryCode, 3) +
this.pad(this.formatName(this.surname, this.givenNames), 39);
// Line 2: Document number, dates, and check digits (44 characters)
const docNumPadded = this.pad(this.documentNumber, 9);
const docNumCheck = this.checkDigit(docNumPadded);
const birthCheck = this.checkDigit(this.birthDate);
const expiryCheck = this.checkDigit(this.expiryDate);
const optionalPadded = this.pad(this.optionalData, 14);
const optionalCheck = this.checkDigit(optionalPadded);
// Composite check digit validates entire line 2
const compositeData =
docNumPadded + docNumCheck +
this.birthDate + birthCheck +
this.expiryDate + expiryCheck +
optionalPadded + optionalCheck;
const compositeCheck = this.checkDigit(compositeData);
const line2 =
docNumPadded + docNumCheck +
this.pad(this.nationality, 3) +
this.birthDate + birthCheck +
this.sex +
this.expiryDate + expiryCheck +
optionalPadded + optionalCheck +
compositeCheck;
return line1 + '\n' + line2;
}
toString() {
return this.generate();
}
}
Important Notes:
- Each data field has specific length requirements
- Check digits validate document number, birth date, expiry date, and optional data
- The composite check digit validates the entire second line
- All text must be uppercase with spaces replaced by
<
Implementing Check Digit Calculation
The check digit algorithm is crucial for ICAO compliance. Here's how it works:
-
Character Values: Each character maps to a number (0-9 stay the same, A-Z map to 10-35,
<maps to 0) - Weight Pattern: [7, 3, 1] repeating
- Calculation: Sum of (character value × weight) for each position
- Result: Sum modulo 10
Example:
Input: "L898902C3"
Values: [21, 8, 9, 8, 9, 0, 2, 12, 3]
Weights: [7, 3, 1, 7, 3, 1, 7, 3, 1]
Products: [147, 24, 9, 56, 27, 0, 14, 36, 3]
Sum: 316
Check: 316 % 10 = 6
Designing the User Interface
Create popup.html with a clean, professional form:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MRZ Generator</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<div class="header">
<h1>🔒 MRZ Generator</h1>
<p>Generate Machine Readable Zone codes</p>
</div>
<div class="form-section">
<div class="form-group">
<label for="document_type">Document Type:</label>
<select id="document_type" class="form-control">
<option value="TD3">TD3 - Passport</option>
<option value="TD2">TD2 - Travel Document</option>
<option value="TD1">TD1 - ID Card</option>
<option value="MRVA">MRVA - Visa Type A</option>
<option value="MRVB">MRVB - Visa Type B</option>
</select>
</div>
<div class="form-group">
<label for="country">Issuing Country:</label>
<input type="text" id="country" class="form-control"
maxlength="3" placeholder="USA">
</div>
<div class="form-group">
<label for="surname">Surname:</label>
<input type="text" id="surname" class="form-control"
placeholder="SMITH">
</div>
<div class="form-group">
<label for="given_names">Given Names:</label>
<input type="text" id="given_names" class="form-control"
placeholder="JOHN">
</div>
<!-- More form fields... -->
<div class="button-group">
<button id="randomBtn" class="btn btn-secondary">
🎲 Random Data
</button>
<button id="generateBtn" class="btn btn-primary">
⚡ Generate MRZ
</button>
</div>
</div>
<div class="output-section">
<canvas id="overlay" width="900" height="640"></canvas>
<button id="downloadBtn" class="btn btn-success">
💾 Download Image
</button>
</div>
</div>
<script src="mrz-generator.js"></script>
<script src="popup.js"></script>
</body>
</html>
Styling with CSS
Create popup.css for a professional look:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
width: 900px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #667eea;
font-size: 32px;
margin-bottom: 10px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.form-control {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #667eea;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
#overlay {
width: 100%;
border-radius: 8px;
margin: 20px 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
Rendering Document Images
Use Canvas API to create realistic document images. Create the drawImage() function in popup.js:
function drawImage() {
const canvas = document.getElementById('overlay');
const ctx = canvas.getContext('2d');
// Set canvas dimensions
const isPassport = dropdown.value === 'TD3';
canvas.width = 900;
canvas.height = isPassport ? 640 : 580;
// Create gradient background
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, '#e8f4f8');
gradient.addColorStop(0.5, '#d4e9f2');
gradient.addColorStop(1, '#c1dfe9');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw decorative pattern
ctx.save();
ctx.globalAlpha = 0.05;
for (let i = 0; i < 50; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const size = Math.random() * 60 + 20;
ctx.fillStyle = '#4a90a4';
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
// Draw main document with rounded corners
const docMargin = 40;
const docWidth = canvas.width - docMargin * 2;
const docHeight = canvas.height - docMargin * 2;
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
ctx.shadowBlur = 20;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.roundRect(docMargin, docMargin, docWidth, docHeight, 15);
ctx.fill();
// Draw header with gradient
const headerHeight = 60;
const headerGradient = ctx.createLinearGradient(
docMargin, docMargin,
docMargin + docWidth, docMargin + headerHeight
);
headerGradient.addColorStop(0, '#667eea');
headerGradient.addColorStop(1, '#764ba2');
ctx.fillStyle = headerGradient;
ctx.beginPath();
ctx.roundRect(docMargin, docMargin, docWidth, headerHeight, [15, 15, 0, 0]);
ctx.fill();
// Draw title
ctx.fillStyle = 'white';
ctx.font = 'bold 24px Arial';
ctx.textAlign = 'left';
const titleText = isPassport ? 'PASSPORT' : 'IDENTIFICATION CARD';
ctx.fillText(titleText, docMargin + 85, docMargin + 35);
// Draw photo placeholder
const photoX = docMargin + 50;
const photoY = docMargin + headerHeight + 25;
const photoWidth = 140;
const photoHeight = 180;
const photoGradient = ctx.createLinearGradient(
photoX, photoY,
photoX + photoWidth, photoY + photoHeight
);
photoGradient.addColorStop(0, '#e2e8f0');
photoGradient.addColorStop(1, '#cbd5e1');
ctx.fillStyle = photoGradient;
ctx.fillRect(photoX, photoY, photoWidth, photoHeight);
// Draw person icon
ctx.fillStyle = '#94a3b8';
ctx.font = '80px Arial';
ctx.textAlign = 'center';
ctx.fillText('👤', photoX + photoWidth / 2, photoY + photoHeight / 2 + 20);
// Draw personal information fields
const infoX = photoX + photoWidth + 40;
let currentY = photoY;
drawField(ctx, 'SURNAME', surname_txt.value, infoX, currentY);
currentY += 35;
drawField(ctx, 'GIVEN NAMES', given_names_txt.value, infoX, currentY);
currentY += 35;
drawField(ctx, 'NATIONALITY', nationality_txt.value, infoX, currentY);
currentY += 35;
drawField(ctx, 'DATE OF BIRTH', formatDisplayDate(birth_date_txt.value), infoX, currentY);
// Draw MRZ section at bottom
drawMRZSection(ctx, docMargin, canvas.height - 120, docWidth);
}
function drawField(ctx, label, value, x, y) {
ctx.font = '11px Arial';
ctx.fillStyle = '#64748b';
ctx.textAlign = 'left';
ctx.fillText(label, x, y);
ctx.font = 'bold 18px Arial';
ctx.fillStyle = '#1e293b';
ctx.fillText(value, x, y + 20);
}
function drawMRZSection(ctx, x, y, width) {
// MRZ background
ctx.fillStyle = '#f8fafc';
ctx.fillRect(x, y, width, 80);
// Load OCR-B font for authentic MRZ display
ctx.font = '22px "OCR-B"';
ctx.fillStyle = '#000000';
ctx.textAlign = 'center';
// Draw MRZ lines
const lines = dataFromGenerator.split('\n');
lines.forEach((line, index) => {
ctx.fillText(line, x + width / 2, y + 25 + (index * 30));
});
}
Date Formatting Helper
Handle 2-digit year conversion properly:
function formatDisplayDate(yymmdd) {
if (!yymmdd || yymmdd.length !== 6) return '';
const yy = parseInt(yymmdd.slice(0, 2));
// Pivot year: 00-50 = 20xx, 51-99 = 19xx
const fullYear = yy > 50 ? `19${yymmdd.slice(0, 2)}` : `20${yymmdd.slice(0, 2)}`;
const month = yymmdd.slice(2, 4);
const day = yymmdd.slice(4, 6);
return `${day}.${month}.${fullYear}`;
}
Installing the Extension Locally
- Open Chrome and navigate to
chrome://extensions/. - Enable Developer mode (toggle in top-right corner).
- Click Load unpacked.
- Select your extension folder.
- The extension should now appear in your toolbar.
Generating MRZ Images
- Click the extension icon.
- Click "Random Data" to populate fields, generate MRZ string, and render the document image.
- Download the document image.
MRZ Recognition with Online MRZ Scanner
Upload the generated image to Dynamsoft Online MRZ Scanner for validation.
Publishing to Chrome Web Store
- Package your extension into a ZIP file.
- Go to Chrome Web Store Developer Dashboard.
- Click "New Item".
- Upload your ZIP file.
- Fill in Store listing (screenshots, description, categories), Privacy, and Distribution.
- Click Submit for Review (approval is typically 1–3 business days).
Source Code
https://github.com/yushulx/web-mrz-generator/tree/main/chrome-extension


Top comments (0)