DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

How to Build a Chrome Extension for MRZ Generation from Scratch

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

MRZ Generator

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
Enter fullscreen mode Exit fullscreen mode

TD1 (ID Card) - 3 lines of 30 characters

I<UTOD231458907<<<<<<<<<<<<<<<
7408122F1204159UTO<<<<<<<<<<<6
ERIKSSON<<ANNA<MARIA<<<<<<<<<<
Enter fullscreen mode Exit fullscreen mode

TD2 (Travel Document) - 2 lines of 36 characters

I<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<
D231458907UTO7408122F1204159<<<<<<<6
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create a new directory and set up the basic files:

mkdir mrz-generator-extension
cd mrz-generator-extension
Enter fullscreen mode Exit fullscreen mode

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'"
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Character Values: Each character maps to a number (0-9 stay the same, A-Z map to 10-35, < maps to 0)
  2. Weight Pattern: [7, 3, 1] repeating
  3. Calculation: Sum of (character value × weight) for each position
  4. 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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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));
    });
}
Enter fullscreen mode Exit fullscreen mode

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}`;
}
Enter fullscreen mode Exit fullscreen mode

Installing the Extension Locally

  1. Open Chrome and navigate to chrome://extensions/.
  2. Enable Developer mode (toggle in top-right corner).
  3. Click Load unpacked.
  4. Select your extension folder.
  5. The extension should now appear in your toolbar.

Generating MRZ Images

  1. Click the extension icon.
  2. Click "Random Data" to populate fields, generate MRZ string, and render the document image.
  3. Download the document image.

Chrome Extension for MRZ Generation

MRZ Recognition with Online MRZ Scanner

Upload the generated image to Dynamsoft Online MRZ Scanner for validation.

Chrome Extension for MRZ Generation

Publishing to Chrome Web Store

  1. Package your extension into a ZIP file.
  2. Go to Chrome Web Store Developer Dashboard.
  3. Click "New Item".
  4. Upload your ZIP file.
  5. Fill in Store listing (screenshots, description, categories), Privacy, and Distribution.
  6. 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)