How to Build an Online MRZ Generator with Python, Pyodide and HTML5
Xiao Ling
Posted on October 16, 2023
When developing or selecting an MRZ (Machine Readable Zone) recognition SDK, the primary challenge lies in finding an appropriate dataset for testing. Acquiring genuine MRZ images is challenging, and due to privacy concerns, they aren't publicly accessible. Therefore, crafting MRZ images becomes a practical solution. Fortunately, there's an open-source Python MRZ generator project, available for download from pypi, eliminating the need to start from scratch. This article aims to illustrate how to integrate and run Python scripts within web applications. First, We will showcase how to employ the Python MRZ SDK and Flet to construct a cross-platform MRZ generator. Subsequently, we will reuse the Python script with Pyodide, HTML5, and the Dynamsoft JavaScript MRZ SDK, creating an advanced online MRZ tool that can handle both MRZ creation and MRZ detection.
Building a Cross-Platform MRZ Generator with Python and Flet
Flet UI is powered by Flutter. It allows developers to build desktop, mobile, and web applications using Python.
New Flet Project
-
Install
flet
andmrz
packages usingpip
:
pip install flet pip install mrz
-
Create a Flet project:
flet create mrz-generator
-
Run the project for desktop:
cd mrz-generator flet run
MRZ Generator UI Design
The UI of the MRZ generator is designed as follows:
-
The dropdown list is used for selecting MRZ type:
dropdown = ft.Dropdown(on_change=dropdown_changed, width=200, options=[ ft.dropdown.Option('Passport(TD3)'), ft.dropdown.Option('ID Card(TD1)'), ft.dropdown.Option('ID Card(TD2)'), ft.dropdown.Option('Visa(A)'), ft.dropdown.Option('Visa(B)'), ],) dropdown.value = 'Passport(TD3)'
-
The input fields are used for entering passport, ID card, and visa information:
document_type = ft.Text('Document type') country_code = ft.Text('Country') document_number = ft.Text('Document number') birth_date = ft.Text('Birth date') sex = ft.Text('Sex') expiry_date = ft.Text('Expiry date') nationality = ft.Text('Nationality') surname = ft.Text('Surname') given_names = ft.Text('Given names') optional_data1 = ft.Text('Optional data 1') optional_data2 = ft.Text('Optional data 2') document_type_txt = ft.TextField( value='P', text_align=ft.TextAlign.LEFT, width=200, height=50) country_code_txt = ft.TextField( value='', text_align=ft.TextAlign.LEFT, width=200, height=50) document_number_txt = ft.TextField( value='', text_align=ft.TextAlign.LEFT, width=200, height=50) birth_date_txt = ft.TextField( value='', text_align=ft.TextAlign.LEFT, width=200, height=50) sex_txt = ft.TextField( value='', text_align=ft.TextAlign.LEFT, width=200, height=50) expiry_date_txt = ft.TextField( value='', text_align=ft.TextAlign.LEFT, width=200, height=50) nationality_txt = ft.TextField( value='', text_align=ft.TextAlign.LEFT, width=200, height=50) surname_txt = ft.TextField( value='', text_align=ft.TextAlign.LEFT, width=200, height=50) given_names_txt = ft.TextField( value='', text_align=ft.TextAlign.LEFT, width=200, height=50) optional_data1_txt = ft.TextField( value='', text_align=ft.TextAlign.LEFT, width=200, height=50) optional_data2_txt = ft.TextField( value='', text_align=ft.TextAlign.LEFT, width=200, height=50) container_loaded = ft.ResponsiveRow([ ft.Column(col=2, controls=[document_type, document_type_txt, country_code, country_code_txt, document_number, document_number_txt,]), ft.Column(col=2, controls=[birth_date, birth_date_txt, sex, sex_txt, expiry_date, expiry_date_txt,]), ft.Column(col=2, controls=[nationality, nationality_txt, surname, surname_txt, given_names, given_names_txt,]), ft.Column(col=2, controls=[optional_data1, optional_data1_txt, optional_data2, optional_data2_txt]) ])
-
The random button is used for generating information automatically:
def generate_random_data(): data = utils.random_mrz_data() surname_txt.value = data['Surname'] given_names_txt.value = data['Given Name'] nationality_txt.value = data['Nationality'] country_code_txt.value = nationality_txt.value sex_txt.value = data['Sex'] document_number_txt.value = data['Document Number'] birth_date_txt.value = data['Birth Date'] expiry_date_txt.value = data['Expiry Date'] def generate_random(e): generate_random_data() page.update() button_random = ft.ElevatedButton( text='Random', on_click=generate_random)
The
random_mrz_data()
function randomly generates surname, given names, nationality, country code, sex, document number, birth date and expiry date.
import random import datetime VALID_COUNTRY_CODES = ['USA', 'CAN', 'GBR', 'AUS', 'FRA', 'CHN', 'IND', 'BRA', 'JPN', 'ZAF', 'RUS', 'MEX', 'ITA', 'ESP', 'NLD', 'SWE', 'ARG', 'BEL', 'CHE'] def random_date(start_year=1900, end_year=datetime.datetime.now().year): year = random.randint(start_year, end_year) month = random.randint(1, 12) if month in [1, 3, 5, 7, 8, 10, 12]: day = random.randint(1, 31) elif month in [4, 6, 9, 11]: day = random.randint(1, 30) else: # February if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0): # leap year day = random.randint(1, 29) else: day = random.randint(1, 28) return datetime.date(year, month, day) def random_string(length=10, allowed_chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ'): return ''.join(random.choice(allowed_chars) for i in range(length)) def random_mrz_data(): surname = random_string(random.randint(3, 7)) given_name = random_string(random.randint(3, 7)) nationality = random.choice(VALID_COUNTRY_CODES) sex = random.choice(['M', 'F']) document_number = random_string(9, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') birth_date = random_date() expiry_date = random_date(start_year=datetime.datetime.now( ).year, end_year=datetime.datetime.now().year + 10) return { 'Surname': surname, 'Given Name': given_name, 'Nationality': nationality, 'Sex': sex, 'Document Number': document_number, 'Birth Date': birth_date.strftime('%y%m%d'), 'Expiry Date': expiry_date.strftime('%y%m%d') }
-
The generate button is used for generating MRZ text:
def generate_mrz(e): if dropdown.value == 'ID Card(TD1)': try: mrz_field.value = TD1CodeGenerator( document_type_txt.value, country_code_txt.value, document_number_txt.value, birth_date_txt.value, sex_txt.value, expiry_date_txt.value, nationality_txt.value, surname_txt.value, given_names_txt.value, optional_data1_txt.value, optional_data2_txt.value) except Exception as e: page.snack_bar = ft.SnackBar( content=ft.Text(str(e)), action='OK', ) page.snack_bar.open = True elif dropdown.value == 'ID Card(TD2)': try: mrz_field.value = TD2CodeGenerator( document_type_txt.value, country_code_txt.value, surname_txt.value, given_names_txt.value, document_number_txt.value, nationality_txt.value, birth_date_txt.value, sex_txt.value, expiry_date_txt.value, optional_data1_txt.value) except Exception as e: page.snack_bar = ft.SnackBar( content=ft.Text(str(e)), action='OK', ) page.snack_bar.open = True elif dropdown.value == 'Passport(TD3)': try: mrz_field.value = TD3CodeGenerator( document_type_txt.value, country_code_txt.value, surname_txt.value, given_names_txt.value, document_number_txt.value, nationality_txt.value, birth_date_txt.value, sex_txt.value, expiry_date_txt.value, optional_data1_txt.value) except Exception as e: page.snack_bar = ft.SnackBar( content=ft.Text(str(e)), action='OK', ) page.snack_bar.open = True elif dropdown.value == 'Visa(A)': try: mrz_field.value = MRVACodeGenerator( document_type_txt.value, country_code_txt.value, surname_txt.value, given_names_txt.value, document_number_txt.value, nationality_txt.value, birth_date_txt.value, sex_txt.value, expiry_date_txt.value, optional_data1_txt.value) except Exception as e: page.snack_bar = ft.SnackBar( content=ft.Text(str(e)), action='OK', ) page.snack_bar.open = True elif dropdown.value == 'Visa(B)': try: mrz_field.value = MRVBCodeGenerator( document_type_txt.value, country_code_txt.value, surname_txt.value, given_names_txt.value, document_number_txt.value, nationality_txt.value, birth_date_txt.value, sex_txt.value, expiry_date_txt.value, optional_data1_txt.value) except Exception as e: page.snack_bar = ft.SnackBar( content=ft.Text(str(e)), action='OK', ) page.snack_bar.open = True page.update() button_generate = ft.ElevatedButton( text='Generate', on_click=generate_mrz)
Deploying the MRZ Generator to GitHub Pages
After completing the MRZ generator, we can build it into static web pages by running:
flet publish main.py
The standalone web app is located in the dist
folder. We can deploy it to GitHub Pages.
Here is the YAML file for GitHub Actions:
name: deploy mrz generator
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flet mrz
- name: Build package
run: flet publish main.py
- name: Change base-tag in index.html from / to mrz-generator
run: sed -i 's/<base href="\/">/<base href="\/mrz-generator\/">/g' dist/index.html
- name: Commit dist to GitHub Pages
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
FOLDER: dist
The mrz-generator
is the name of the repository. The sed
command is used to change the base tag in index.html
from /
to /mrz-generator/
.
Flet is a convenient choice for building cross-platform applications. However, it is still in the development phase and lacks several features. For instance, it does not support image drawing on a canvas for crafting MRZ images. Given that our objective is to create an online MRZ generator, we can integrate the Pyodide engine utilized by Flet into an HTML5 project to execute Python scripts. This integration of Python and JavaScript code can significantly enhance the capabilities of our MRZ generator.
Building an Online MRZ Generator with Python, Pyodide, and HTML5
Include the pyodide.js
file in the HTML page and initialize the Pyodide engine as follows:
<script src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>
<script>
async function main() {
pyodide = await loadPyodide();
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await micropip.install('mrz');
}
main();
</script>
The mrz
package is installed via micropip
.
Construct the UI in HTML5. In addition to generating MRZ text, the web project can also create MRZ images by drawing lines, text, and background images on the canvas.
<div class="container">
<div class="row">
<div>
<select onchange="selectChanged()" id="dropdown">
<option value="Passport(TD3)">Passport(TD3)</option>
<option value="ID Card(TD1)">ID Card(TD1)</option>
<option value="ID Card(TD2)">ID Card(TD2)</option>
<option value="Visa(A)">Visa(A)</option>
<option value="Visa(B)">Visa(B)</option>
</select>
</div>
</div>
<div class="row">
<div>
<label for="docType">Document type</label>
<input type="text" id="document_type_txt" placeholder="P">
</div>
<div>
<label for="birthDate">Birth date</label>
<input type="text" id="birth_date_txt" placeholder="210118">
</div>
<div>
<label for="nationality">Nationality</label>
<input type="text" id="nationality_txt" placeholder="GBR">
</div>
</div>
<div class="row">
<div>
<label for="country">Country</label>
<input type="text" id="country_code_txt" placeholder="GBR">
</div>
<div>
<label for="sex">Sex</label>
<input type="text" id="sex_txt" placeholder="F">
</div>
<div>
<label for="surname">Surname</label>
<input type="text" id="surname_txt" placeholder="SXNGND">
</div>
</div>
<div class="row">
<div>
<label for="docNumber">Document number</label>
<input type="text" id="document_number_txt" placeholder="K1RELFC7">
</div>
<div>
<label for="expiryDate">Expiry date</label>
<input type="text" id="expiry_date_txt" placeholder="240710">
</div>
<div>
<label for="givenNames">Given names</label>
<input type="text" id="given_names_txt" placeholder="MGGPJ">
</div>
</div>
<div class="row">
<div>
<label for="optionalData1">Optional data 1</label>
<input type="text" id="optional_data1_txt" placeholder="ZE184226B">
</div>
<div>
<label for="optionalData2">Optional data 2</label>
<input type="text" id="optional_data2_txt">
</div>
</div>
<div class="row">
<div>
<button id="randomBtn" onclick="randomize()">Random</button>
</div>
<div>
<button id="generateBtn" onclick="generate()">Generate</button>
</div>
<div>
<button onclick="recognize()">Recognize MRZ</button>
</div>
</div>
<div class="row">
<div>
<textarea rows="3" cols="50" readonly id="outputMRZ"></textarea>
</div>
</div>
<div class="row">
<div class="image-container">
<canvas id="overlay"></canvas>
</div>
</div>
<div id="mrz-result" class="right-sticky-content"></div>
</div>
To expedite development, we can utilize ChatGPT to efficiently port the Python code logic to JavaScript.
-
Randomize information for passport, ID card, and visa:
const VALID_COUNTRY_CODES = ['USA', 'CAN', 'GBR', 'AUS', 'FRA', 'CHN', 'IND', 'BRA', 'JPN', 'ZAF', 'RUS', 'MEX', 'ITA', 'ESP', 'NLD', 'SWE', 'ARG', 'BEL', 'CHE']; function randomIntFromInterval(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); } function randomDate(startYear = 1900, endYear = new Date().getFullYear()) { let year = randomIntFromInterval(startYear, endYear); let month = randomIntFromInterval(1, 12); let day; if ([1, 3, 5, 7, 8, 10, 12].includes(month)) { day = randomIntFromInterval(1, 31); } else if ([4, 6, 9, 11].includes(month)) { day = randomIntFromInterval(1, 30); } else { // February if ((year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0)) { // leap year day = randomIntFromInterval(1, 29); } else { day = randomIntFromInterval(1, 28); } } let date = new Date(year, month - 1, day); return date; } function formatDate(date) { return date.toISOString().slice(2, 10).replace(/-/g, ""); } function randomString(length = 10, allowedChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') { let result = ''; for (let i = 0; i < length; i++) { result += allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)); } return result; } function randomMRZData() { let surname = randomString(randomIntFromInterval(3, 7)); let givenName = randomString(randomIntFromInterval(3, 7)); let nationality = VALID_COUNTRY_CODES[Math.floor(Math.random() * VALID_COUNTRY_CODES.length)]; let sex = Math.random() < 0.5 ? 'M' : 'F'; let documentNumber = randomString(9, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'); let birthDate = randomDate(); let expiryDate = randomDate(new Date().getFullYear(), new Date().getFullYear() + 10); return { 'Surname': surname, 'Given Name': givenName, 'Nationality': nationality, 'Sex': sex, 'Document Number': documentNumber, 'Birth Date': formatDate(birthDate), 'Expiry Date': formatDate(expiryDate) }; }
-
Generate the MRZ text:
function generate() { detectedLines = []; document.getElementById('mrz-result').innerText = ''; if (!pyodide) return; pyodide.globals.set('dropdown', dropdown.value); pyodide.globals.set('document_type_txt', document_type_txt.value); pyodide.globals.set('country_code_txt', country_code_txt.value); pyodide.globals.set('birth_date_txt', birth_date_txt.value); pyodide.globals.set('document_number_txt', document_number_txt.value); pyodide.globals.set('sex_txt', sex_txt.value); pyodide.globals.set('expiry_date_txt', expiry_date_txt.value); pyodide.globals.set('nationality_txt', nationality_txt.value); pyodide.globals.set('surname_txt', surname_txt.value); pyodide.globals.set('given_names_txt', given_names_txt.value); pyodide.globals.set('optional_data1_txt', optional_data1_txt.value); pyodide.globals.set('optional_data2_txt', optional_data2_txt.value); pyodide.runPython(` from mrz.generator.td1 import TD1CodeGenerator from mrz.generator.td2 import TD2CodeGenerator from mrz.generator.td3 import TD3CodeGenerator from mrz.generator.mrva import MRVACodeGenerator from mrz.generator.mrvb import MRVBCodeGenerator if dropdown == 'ID Card(TD1)': try: txt = str(TD1CodeGenerator( document_type_txt, country_code_txt, document_number_txt, birth_date_txt, sex_txt, expiry_date_txt, nationality_txt, surname_txt, given_names_txt, optional_data1_txt, optional_data2_txt)) except Exception as e: txt = e elif dropdown == 'ID Card(TD2)': try: txt = str(TD2CodeGenerator( document_type_txt, country_code_txt, surname_txt, given_names_txt, document_number_txt, nationality_txt, birth_date_txt, sex_txt, expiry_date_txt, optional_data1_txt)) except Exception as e: txt = e elif dropdown == 'Passport(TD3)': try: txt = str(TD3CodeGenerator( document_type_txt, country_code_txt, surname_txt, given_names_txt, document_number_txt, nationality_txt, birth_date_txt, sex_txt, expiry_date_txt, optional_data1_txt)) except Exception as e: txt = e elif dropdown == 'Visa(A)': try: txt = str(MRVACodeGenerator( document_type_txt, country_code_txt, surname_txt, given_names_txt, document_number_txt, nationality_txt, birth_date_txt, sex_txt, expiry_date_txt, optional_data1_txt)) except Exception as e: txt = e elif dropdown == 'Visa(B)': try: txt = str(MRVBCodeGenerator( document_type_txt, country_code_txt, surname_txt, given_names_txt, document_number_txt, nationality_txt, birth_date_txt, sex_txt, expiry_date_txt, optional_data1_txt)) except Exception as e: txt = e `); dataFromPython = pyodide.globals.get('txt'); document.getElementById("outputMRZ").value = dataFromPython; }
Lastly, we can render document information and MRZ text onto the canvas:
function drawImage() {
let canvas = document.getElementById("overlay");
let ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
var img = new Image();
img.src = 'images/bg.jpg';
img.onload = function () {
canvas.width = img.width;
canvas.height = img.height;
ctx.fillStyle = '#FFFFFF'; // e.g., a shade of orange
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, img.width, img.height);
lines = dataFromPython.split('\n');
ctx.fillStyle = "black";
// Title
let x = 60;
let y = 80;
ctx.font = '40px "Arial", monospace';
if (dropdown.value === 'ID Card(TD1)' || dropdown.value === 'ID Card(TD2)') {
ctx.fillText('ID Card', x, y);
}
else if (dropdown.value === 'Passport(TD3)') {
ctx.fillText('Passport', x, y);
}
else {
ctx.fillText('Visa', x, y);
}
// Info area
let delta = 21;
let space = 10;
x = 400;
y = 140;
ctx.font = '16px "Arial", monospace';
ctx.fillText('Type', x, y);
y += delta;
ctx.font = 'bold 18px "Arial", monospace';
ctx.fillText(document_type_txt.value, x, y);
y += delta + space;
ctx.font = '16px "Arial", monospace';
ctx.fillText('Surname', x, y);
y += delta;
ctx.font = 'bold 18px "Arial", monospace';
ctx.fillText(surname_txt.value, x, y);
y += delta + space;
ctx.font = '16px "Arial", monospace';
ctx.fillText('Given names', x, y);
y += delta;
ctx.font = 'bold 18px "Arial", monospace';
ctx.fillText(given_names_txt.value, x, y);
y += delta + space;
ctx.font = '16px "Arial", monospace';
ctx.fillText('Date of birth', x, y);
y += delta;
ctx.font = 'bold 18px "Arial", monospace';
ctx.fillText(`${birth_date_txt.value.slice(0, 2)}/${birth_date_txt.value.slice(2, 4)}/${birth_date_txt.value.slice(4, 6)}`, x, y);
y += delta + space;
ctx.font = '16px "Arial", monospace';
ctx.fillText('Sex', x, y);
y += delta;
ctx.font = 'bold 18px "Arial", monospace';
ctx.fillText(sex_txt.value, x, y);
y += delta + space;
ctx.font = '16px "Arial", monospace';
ctx.fillText('Date of expiry', x, y);
y += delta;
ctx.font = 'bold 18px "Arial", monospace';
ctx.fillText(`${expiry_date_txt.value.slice(0, 2)}/${expiry_date_txt.value.slice(2, 4)}/${expiry_date_txt.value.slice(4, 6)}`, x, y);
y += delta + space;
ctx.font = '16px "Arial", monospace';
ctx.fillText('Issuing country', x, y);
y += delta;
ctx.font = 'bold 18px "Arial", monospace';
ctx.fillText(country_code_txt.value, x, y);
x = 500
y = 140
ctx.font = '16px "Arial", monospace';
if (dropdown.value === 'ID Card(TD1)' || dropdown.value === 'ID Card(TD2)') {
ctx.fillText('Document number', x, y);
}
else if (dropdown.value === 'Passport(TD3)') {
ctx.fillText('Passport number', x, y);
}
else {
ctx.fillText('Visa number', x, y);
}
y += delta;
ctx.font = 'bold 18px "Arial", monospace';
ctx.fillText(document_number_txt.value, x, y);
// MRZ area
ctx.font = '16px "OCR-B", monospace';
x = 60;
y = canvas.height - 80;
let letterSpacing = 3;
let index = 0;
for (text of lines) {
let currentX = x;
let checkLine = '';
if (detectedLines.length > 0) {
checkLine = detectedLines[index];
}
for (let i = 0; i < text.length; i++) {
ctx.fillText(text[i], currentX, y);
if (checkLine !== '' && checkLine[i] !== text[i]) {
ctx.fillRect(currentX, y + 5, ctx.measureText(text[i]).width, 2);
}
currentX += ctx.measureText(text[i]).width + letterSpacing;
}
y += 30;
index += 1;
}
}
}
Evaluating JavaScript MRZ SDK with MRZ Generator
After completing the MRZ generator, we can evaluate any JavaScript MRZ SDK with it. For example, we can use the Dynamsoft JavaScript MRZ SDK to recognize MRZ code from the generated MRZ images. A valid license key is required for the JavaScript MRZ detection SDK.
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-label-recognizer@2.2.31/dist/dlr.js"></script>
<script>
async function main() {
...
Dynamsoft.DLR.LabelRecognizer.initLicense("LICENSE-KEY");
recognizer = await Dynamsoft.DLR.LabelRecognizer.createInstance({
runtimeSettings: "MRZ"
});
}
main();
function recognize() {
if (recognizer) {
let div = document.getElementById('mrz-result');
div.textContent = 'Recognizing...';
recognizer.recognize(document.getElementById("overlay")).then(function (results) {
let hasResult = false;
for (let result of results) {
if (result.lineResults.length !== 2 && result.lineResults.length !== 3) {
continue;
}
let output = '';
for (let line of result.lineResults) {
detectedLines.push(line.text);
output += line.text + '\n';
}
div.innerText = output;
hasResult = true;
}
if (!hasResult) {
div.innerText = 'Not found';
}
else {
drawImage();
}
});
}
}
</script>
Try Online MRZ Generator
- MRZ generator only: https://yushulx.me/mrz-generator/
- MRZ generator and MRZ detector: https://yushulx.me/mrz-generator-pyodide/
Source Code
Posted on October 16, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.