vision-camera-mrz-scanner
Version:
VisionCamera Frame Processor Plugin to detect and read MRZ data from passports using MLKit Text Recognition.
476 lines (447 loc) • 18.3 kB
JavaScript
// logging import and setup
import { ListItemData } from '../constants/listItemData';
const countryIsoJson = require('../constants/CountryIsoCodes.json');
/**
* It takes a string, and returns a number
* @param {string} text - The text to be encoded.
* @returns The checkSum function returns the check sum of the input string.
*/
export const checkSum = text => {
let value = 0;
let isNumber = /([0-9])/;
for (let i = 0; i < text.length; i++) {
if (isNumber.test(text.charAt(i))) {
if (i % 3 === 0) {
value += 7 * parseInt(text.charAt(i), 10);
} else if (i % 3 === 1) {
value += 3 * parseInt(text.charAt(i), 10);
} else {
value += parseInt(text.charAt(i), 10);
}
} else if (text.charCodeAt(i) === 60) {
// if the character is a '<'
value += 0;
} else {
if (i % 3 === 0) {
value += 7 * text.charCodeAt(i) - 55;
} else if (i % 3 === 1) {
value += 3 * text.charCodeAt(i) - 55;
} else {
value += text.charCodeAt(i) - 55;
}
}
}
value %= 10;
return value;
};
export const parseMRZ = initialLines => {
let lines = [];
const firstInitialLastLine = initialLines[initialLines.length - 1];
const secondInitialLastLine = initialLines[initialLines.length - 2];
// if lines.length >= 2, extract and parse two-line MRZ
if (initialLines && initialLines.length >= 2 && firstInitialLastLine && secondInitialLastLine) {
// return undefined if a double left angle bracket character is found in either last line, or second to last line.
if (firstInitialLastLine.indexOf('«') !== -1 || secondInitialLastLine.indexOf('«') !== -1) {
return undefined;
}
// remove all empty spaces in each line, capitalize all letters, change all '$' to 'S'
initialLines.forEach(line => {
while (line.indexOf(' ') !== -1) {
line = line.replace(' ', '');
}
line = line.toUpperCase();
while (line.indexOf('$') !== -1) {
line = line.replace('$', 'S');
}
// MLKIT sometimes add a new line character when it finds a new line instead of separating the lines into different elements.
while (line.indexOf('\n') !== -1) {
lines.push(line.substring(0, line.indexOf('\n')));
line = line.substring(line.indexOf('\n') + 1);
}
lines.push(line);
});
// parse 2 line MRZ if the current line, and the previous line both have 43, 44, or 45 characters
for (let i = 1; i < lines.length; i++) {
const currentLine = lines[i];
const lastLine = lines[i - 1];
if (currentLine && lastLine) {
if (currentLine.length > 42 && currentLine.length < 46 && lastLine.length > 42 && lastLine.length < 46 || currentLine.length > 35 && currentLine.length < 37 && lastLine.length > 35 && lastLine.length < 37) {
return parse2LineMRZ(lastLine, currentLine);
// return parse([lastLine, currentLine]).fields;
}
}
}
} // end (lines.length >= 2)
if (lines.length >= 3) {
// At this point, empty spaces will already be removed and all letters will be capitalized.
// return undefined if a double left angle bracket character is found in third to last line.
const thirdToLastLine = lines[lines.length - 3];
if (thirdToLastLine && thirdToLastLine.indexOf('«') !== -1) {
return undefined;
}
for (let i = 2; i < lines.length; i++) {
const currentLine = lines[i];
const lastLine = lines[i - 1];
const secondToLastLine = lines[i - 2];
if (currentLine && lastLine && secondToLastLine) {
if (currentLine.length > 28 && currentLine.length < 32 && lastLine.length > 28 && lastLine.length < 32 && secondToLastLine.length > 28 && secondToLastLine.length < 32) {
return parse3LineMRZ(secondToLastLine, lastLine, currentLine);
// return parse([secondToLastLine, lastLine, currentLine]);
}
}
}
} // end (lines.length >= 3)
return undefined;
};
/**
* It takes two strings, parses them, and returns an object with the parsed data
* @param {string} firstRow - string, secondRow: string
* @param {string} secondRow - string =
* "P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<<<<<\nL898902C<3UTO6908061F9406236ZE184226B<<<<<14"
* @returns an object with the following properties:
* docMRZ: `\n`,
* docType: docType,
* issuingCountry: issuingCountry,
* givenNames: givenNames.join(' ').trim(),
* lastNames: lastNames.join(' ').trim(),
* idNumber: idNumber,
* nationality
*/
const parse2LineMRZ = (firstRow, secondRow) => {
let docType = extractDocType(firstRow);
let namesFromLine = extractNamesFromLine(5, firstRow);
let givenNames = namesFromLine.givenNames;
let lastNames = namesFromLine.lastNames;
// Extract idNumber
let idNumber = extractIdNumber(secondRow, 0, 9);
if (!idNumber) {
return undefined;
}
// extract issuing country from document holder
let issuingCountry = extractCountry(firstRow, 2, 5);
// extract Nationality of the document holder
let nationality = extractCountry(secondRow, 10, 13);
// extract dateOfBirth
let dob = extractDateOfBirthFromLine(13, secondRow);
// Extract gender
// let gender = extractGender(secondRow.charAt(20), issuingCountry, docType);
let gender = extractGender(secondRow.charAt(20));
// let gender = secondRow.charAt(20);
// Extract expiration date then assign the YYYY-MM-DD string format to docExpirationDate
let docExpirationDate = extractDateOfExpirationFromLine(21, secondRow);
return {
docMRZ: `${firstRow}\n${secondRow}`,
docType: docType,
issuingCountry: issuingCountry,
givenNames: givenNames.join(' ').trim(),
lastNames: lastNames.join(' ').trim(),
idNumber: idNumber,
nationality: nationality,
dob: dob,
gender: gender,
docExpirationDate: docExpirationDate,
additionalInformation: undefined // TODO remove? (The logic for extracting additional information was deleted since we're not using it)
};
};
/**
* It takes in three strings, and returns an object with the following properties: docMRZ, docType,
* issuingCountry, givenNames, lastNames, idNumber, nationality, dob, gender, docExpirationDate, and
* additionalInformation
* @param {string} firstRow - string,
* @param {string} secondRow - string,
* @param {string} thirdRow - string,
* @returns An object with the following properties:
* docMRZ: string
* docType: string
* issuingCountry: string
* givenNames: string
* lastNames: string
* idNumber: string
* nationality: string
* dob: string
* gender: string
* docExpirationDate: string
* additionalInformation: string
*/
const parse3LineMRZ = (firstRow, secondRow, thirdRow) => {
let docType = extractDocType(firstRow);
let namesFromLine = extractNamesFromLine(0, thirdRow);
let givenNames = namesFromLine.givenNames;
let lastNames = namesFromLine.lastNames;
// Extract idNumber
let idNumber = extractIdNumber(firstRow, 5, 14);
if (!idNumber) {
return undefined;
}
// Extract issuingCountry
let issuingCountry = extractCountry(firstRow, 2, 5);
// Extract nationality
let nationality = extractCountry(secondRow, 15, 18);
// Extract date of birth
let dob = extractDateOfBirthFromLine(0, secondRow);
// Extract gender
let gender = extractGender(secondRow.charAt(7));
// Extract expiration date then store that date as a string in docExpirationDate
let docExpirationDate = extractDateOfExpirationFromLine(8, secondRow);
// extract additional information from first and second row
let additionalInformation = '';
additionalInformation = firstRow.substring(15, 30) + secondRow.substring(18, 29);
while (additionalInformation.indexOf('<') !== -1) {
additionalInformation = additionalInformation.replace('<', ' ');
}
additionalInformation = additionalInformation.trim();
return {
docMRZ: `${firstRow}\n${secondRow}\n${thirdRow}`,
docType: docType,
issuingCountry: issuingCountry,
givenNames: givenNames.join(' ').trim(),
lastNames: lastNames.join(' ').trim(),
idNumber: idNumber,
nationality: nationality,
dob: dob,
gender: gender,
docExpirationDate: docExpirationDate,
additionalInformation: additionalInformation
};
};
/**
* It takes a string and returns a string
* @param {string} line - string - the line of text that we're parsing
* @returns the docType.
*/
const extractDocType = line => {
const docTypeLen = line.charAt(1) === '<' ? 1 : 2;
const docType = line.substring(0, docTypeLen);
return getDocTypeFromCode(docType);
};
/**
* It takes a document code and returns the document type
* @param {string} [docCode] - string
* @returns The value of the first item in the array that has a codes array that contains the docCode.
*/
export const getDocTypeFromCode = docCode => {
var _ListItemData$DOCUMEN;
return (_ListItemData$DOCUMEN = ListItemData.DOCUMENT_TYPE.find(doc => doc.codes.find(code => code === docCode))) === null || _ListItemData$DOCUMEN === void 0 ? void 0 : _ListItemData$DOCUMEN.value;
};
/**
* It takes a string, extracts a substring from it, replaces all 'O' with '0', calculates the checksum
* of the substring, compares it with the checksum on the document, removes all '<' from the substring,
* and returns the substring.
* @param {string} line - the line of text that contains the ID number
* @param {number} startingIndex - the index of the first character of the ID number
* @param {number} endingIndex - the index of the last character of the id number
* @returns The ID number.
*/
const extractIdNumber = (line, startingIndex, endingIndex) => {
let idNumber = line.substring(startingIndex, endingIndex);
// replace all 'O' with '0'
while (idNumber.indexOf('O') !== -1) {
idNumber = idNumber.replace('O', '0');
}
// calculate the checksum using idNumber then compare it with the checksum on the document
let idNumberCheckSum = checkSum(idNumber);
if (idNumberCheckSum === parseInt(line.charAt(endingIndex), 10)) {}
// remove all '<' from idNumber
while (idNumber.indexOf('<') !== -1) {
idNumber = idNumber.replace('<', '');
}
return idNumber;
};
/**
* A CountryIsoCode is an object with a string property called isoCountryNumber, a string property
* called isoCountryCode, and a string property called countryName.
* @property {string} isoCountryNumber - The ISO country number.
* @property {string} isoCountryCode - The ISO 3166-1 alpha-2 code for the country.
* @property {string} countryName - The name of the country.
*/
/**
* It takes a string, and two numbers, and returns a string.
* @param {string} line - string - the line of text that we're parsing
* @param {number} startingIndex - the index of the first character of the country code
* @param {number} endingIndex - number = line.length,
* @returns A function that takes 3 parameters and returns a string.
*/
const extractCountry = (line, startingIndex, endingIndex) => {
var _countryIsoJson$find;
let country = line.substring(startingIndex, endingIndex);
// ensure 6's, 0's, and 2's are swapped out with G's, O's, and Z'z respectively
country = replaceNumbersWithCorrespondingLetters(country);
// if country is germany, return DEU
if (country === 'D<<') {
return 'DEU';
}
// if country is in countryIsoJson, return it, if not return undefined
return (_countryIsoJson$find = countryIsoJson.find(item => item.isoCountryCode === country)) === null || _countryIsoJson$find === void 0 ? void 0 : _countryIsoJson$find.isoCountryCode;
};
/**
* It takes a starting index and a line of text, and returns a date of expiration if it can find one.
* @param {number} startingIndex - number,
* @param {string} line - string - the line of text that contains the date of expiration
* @returns A date string in the format of YYYY-MM-DD
*/
const extractDateOfExpirationFromLine = (startingIndex, line) => {
// replace all 'O' with '0'
while (line.substring(startingIndex, startingIndex + 6).indexOf('O') !== -1) {
line = line.replace('O', '0');
}
let twoDigitYearOfExpiration = line.substring(startingIndex, startingIndex + 2);
let twoDigitMonthOfExpiration = line.substring(startingIndex + 2, startingIndex + 4);
let twoDigitDayOfExpiration = line.substring(startingIndex + 4, startingIndex + 6);
let fullYearOfExpDate = 2000 + parseInt(twoDigitYearOfExpiration, 10);
let currentYear = new Date().getFullYear();
if (fullYearOfExpDate - currentYear > 10) {
fullYearOfExpDate -= 100;
}
// ensure expiration date is valid. if not, return undefined
let docExpirationDate = `${fullYearOfExpDate}-${twoDigitMonthOfExpiration}-${twoDigitDayOfExpiration}`;
if (new Date(docExpirationDate).toString() === 'Invalid Date') {
return undefined;
}
// Confirm checkSum for date of expiration
let expDateCheckSum = checkSum(twoDigitYearOfExpiration + twoDigitMonthOfExpiration + twoDigitDayOfExpiration);
if (expDateCheckSum === parseInt(line.charAt(startingIndex + 6), 10)) {
return docExpirationDate;
}
return undefined;
};
/**
* It takes a starting index and a line of text, and returns a date of birth if the checksum is valid,
* otherwise it returns undefined
* @param {number} startingIndex - The index of the first character of the date of birth in the line
* @param {string} line - the line of text that contains the date of birth
* @returns A function that takes two parameters, startingIndex and line.
*/
const extractDateOfBirthFromLine = (startingIndex, line) => {
// replace all 'O' with '0'
while (line.substring(startingIndex, startingIndex + 6).indexOf('O') !== -1) {
line = line.replace('O', '0');
}
let twoDigitYearOfBirth = line.substring(startingIndex, startingIndex + 2);
let twoDigitMonthOfBirth = line.substring(startingIndex + 2, startingIndex + 4);
let twoDigitDayOfBirth = line.substring(startingIndex + 4, startingIndex + 6);
let fullYearOfBirth = 2000 + parseInt(twoDigitYearOfBirth, 10);
let currentYear = new Date().getFullYear();
if (currentYear - fullYearOfBirth < 0) {
fullYearOfBirth -= 100;
}
// Confirm checkSum for date of birth
let dobCheckSum = checkSum(twoDigitYearOfBirth + twoDigitMonthOfBirth + twoDigitDayOfBirth);
let dob = `${fullYearOfBirth}-${twoDigitMonthOfBirth}-${twoDigitDayOfBirth}`;
// ensure date of birth is a valid date. if not, return undefined
if (new Date(dob).toString() === 'Invalid Date') {
return undefined;
}
if (dobCheckSum === parseInt(line.charAt(startingIndex + 6), 10)) {
return dob;
}
return undefined;
};
/**
* It takes a string and extracts the given names and last names from it.
* @param {number} startingIndex - number - the index of the line where the names start
* @param {string} line - the line of text that contains the names
* @returns An object with two properties: givenNames and lastNames.
*/
const extractNamesFromLine = (startingIndex, line) => {
let angleBracketCount = 0;
let lastNamesExtracted = false;
let lastNames = [];
let lastName = '';
let givenNames = [];
let givenName = '';
for (let i = startingIndex; i < line.length; i++) {
if (line.charAt(i) !== '<' && !lastNamesExtracted) {
angleBracketCount = 0;
lastName += line.charAt(i);
if (i === line.length - 1) {
lastNames.push(lastName);
}
}
// append to givenName
else if (line.charAt(i) !== '<' && lastNamesExtracted) {
angleBracketCount = 0;
givenName += line.charAt(i);
if (i === line.length - 1) {
givenNames.push(givenName);
}
}
// append to lastNames[]
else if (line.charAt(i) === '<' && angleBracketCount === 0 && !lastNamesExtracted) {
lastNames.push(lastName);
lastName = '';
angleBracketCount++;
}
// append to givenNames[]
else if (line.charAt(i) === '<' && angleBracketCount === 0 && lastNamesExtracted) {
givenNames.push(givenName);
givenName = '';
angleBracketCount++;
}
// switch from lastName extraction to givenName extraction
else if (line.charAt(i) === '<' && angleBracketCount === 1 && !lastNamesExtracted) {
lastNames.push(lastName);
lastNamesExtracted = true;
angleBracketCount = 0;
}
// end extraction
else if (line.charAt(i) === '<' && angleBracketCount === 1 && lastNamesExtracted) {
givenNames.push(givenName);
break;
}
}
// remove empty strings from givenNames and lastNames
givenNames = givenNames.filter(fName => fName.length > 0);
lastNames = lastNames.filter(lName => lName.length > 0);
// ensure 6's, 0's, and 2's are swapped out with G's, O's, and Z'z respectively
for (let i = 0; i < givenNames.length; i++) {
const initialGivenName = givenNames[i];
if (initialGivenName) {
givenNames[i] = replaceNumbersWithCorrespondingLetters(initialGivenName);
}
}
for (let i = 0; i < lastNames.length; i++) {
const initialLastName = lastNames[i];
if (initialLastName) {
lastNames[i] = replaceNumbersWithCorrespondingLetters(initialLastName);
}
}
return {
givenNames,
lastNames
};
};
/**
* It replaces all the numbers in a string with their corresponding letters
* @param {string} word - string - the word that you want to replace the numbers with letters
* @returns The word with the numbers replaced with the corresponding letters.
*/
const replaceNumbersWithCorrespondingLetters = word => {
while (word.indexOf('0') !== -1) {
word = word.replace('0', 'O');
}
while (word.indexOf('6') !== -1) {
word = word.replace('6', 'G');
}
while (word.indexOf('2') !== -1) {
word = word.replace('2', 'Z');
}
while (word.indexOf('1') !== -1) {
word = word.replace('1', 'I');
}
return word;
};
/**
* If the letter is '<', return 'U', if the letter is 'H', return 'M', otherwise return the letter
* @param {string} letter - string - the letter to extract the gender from
* @returns the letter if it is not '<' or 'H'.
*/
const extractGender = letter => {
if (letter === '<') {
return 'U';
}
if (letter === 'H') {
return 'M';
}
return letter;
};
//# sourceMappingURL=mrzParser.js.map