import moment from 'moment'; type MRZ = { type: 'TD1' | 'TD2' | 'TD3'; zone: string[]; }; type Field = { field: string; format?: (value: string, obj?: Record) => string; /** * Post process value after format. * Useful for extract one field to multiple fields . * * @example * Convert fullname into firstname and lastname * * ```ts * (value) => [ * { field: 'first_name', value: value.split(' ').at(0) || '' }, * { field: 'last_name', value: value.split(' ').slice(1).join(' ') }, * ] * ``` */ process?: (value: string) => { field: string; value: string }[]; }; type FieldList = Record; const DEFAULT_FIELD = { documentType: { field: 'doc_type' }, documentNo: { field: 'doc_number' }, documentNoCheck: { field: 'doc_number_check' }, name: { field: 'full_name', format: (value, _) => value.replace(/0/, 'O'), process: (value) => [ { field: 'first_name', value: value.split(' ').at(0) || '' }, { field: 'last_name', value: value.split(' ').slice(1).join(' ') }, ], }, country: { field: 'country', format: (value, _) => value.replace(/0/, 'O'), }, nationality: { field: 'nationality', format: (value, _) => value.replace(/0/, 'O'), }, gender: { field: 'sex', format: (value, _) => (value === 'M' ? 'male' : 'female'), }, birthDate: { field: 'birth_date', format: (value, _) => moment(value, 'YYMMDD').format('YYYY-MM-DD'), }, birthDateCheck: { field: 'birth_date_check' }, expireDate: { field: 'expire_date', format: (value, _) => moment(value, 'YYMMDD').format('YYYY-MM-DD'), }, expireDateCheck: { field: 'expire_date_check' }, optionalData: { field: 'optional_data' }, optionalDataCheck: { field: 'optional_data_check' }, lineCheck: { field: 'line_check' }, } satisfies FieldList; const MRZ_TD_1 = [ new RegExp( [ `(?<${DEFAULT_FIELD.documentType.field}>[0-9A-Z<]{2})`, `(?<${DEFAULT_FIELD.country.field}>[0-9A-Z<]{3})`, `(?<${DEFAULT_FIELD.documentNo.field}>[0-9A-Z<]{9})`, `(?<${DEFAULT_FIELD.documentNoCheck.field}>[0-9A-Z<]{1})`, `(?<${DEFAULT_FIELD.optionalData.field}>[0-9A-Z<]{15})`, ].join(''), ), new RegExp( [ `(?<${DEFAULT_FIELD.birthDate.field}>[0-9A-Z<]{6})`, `(?<${DEFAULT_FIELD.birthDateCheck.field}>[0-9A-Z<]{1})`, `(?<${DEFAULT_FIELD.gender.field}>[mfMF<]{1})`, `(?<${DEFAULT_FIELD.expireDate.field}>[0-9A-Z<]{6})`, `(?<${DEFAULT_FIELD.expireDateCheck.field}>[0-9A-Z<]{1})`, `(?<${DEFAULT_FIELD.nationality.field}>[0-9A-Z<]{3})`, `(?<${DEFAULT_FIELD.optionalData.field}>[A-Z0-9<]{11})`, `(?<${DEFAULT_FIELD.lineCheck.field}>[0-9A-Z<]{1})`, ].join(''), ), new RegExp([`(?<${DEFAULT_FIELD.name.field}>[A-Z<]{30})`].join('')), ]; const MRZ_TD_2 = [ new RegExp( [ `(?<${DEFAULT_FIELD.documentType.field}>[0-9A-Z<]{2})`, `(?<${DEFAULT_FIELD.country.field}>[0-9A-Z<]{3})`, `(?<${DEFAULT_FIELD.name.field}>[A-Z<]{31})`, ].join(''), ), new RegExp( [ `(?<${DEFAULT_FIELD.documentNo.field}>[0-9A-Z<]{9})`, `(?<${DEFAULT_FIELD.documentNoCheck.field}>[0-9A-Z<]{1})`, `(?<${DEFAULT_FIELD.nationality.field}>[0-9A-Z<]{3})`, `(?<${DEFAULT_FIELD.birthDate.field}>[0-9A-Z<]{6})`, `(?<${DEFAULT_FIELD.birthDateCheck.field}>[0-9A-Z<]{1})`, `(?<${DEFAULT_FIELD.gender.field}>[mfMF]{1})`, `(?<${DEFAULT_FIELD.expireDate.field}>[0-9A-Z<]{6})`, `(?<${DEFAULT_FIELD.expireDateCheck.field}>[0-9A-Z<]{1})`, `(?<${DEFAULT_FIELD.optionalData.field}>[A-Z0-9<]{7})`, `(?<${DEFAULT_FIELD.lineCheck.field}>[0-9A-Z<]{1})`, ].join(''), ), ]; const MRZ_TD_3 = [ new RegExp( [ `(?<${DEFAULT_FIELD.documentType.field}>[A-Z0-9<]{2})`, `(?<${DEFAULT_FIELD.country.field}>[0-9A-Z<]{3})`, `(?<${DEFAULT_FIELD.name.field}>[A-Z0-9<]{39})`, ].join(''), ), new RegExp( [ `(?<${DEFAULT_FIELD.documentNo.field}>[0-9A-Z<]{9})`, `(?<${DEFAULT_FIELD.documentNoCheck.field}>[0-9A-Z<]{1})`, `(?<${DEFAULT_FIELD.nationality.field}>[0-9A-Z<]{3})`, `(?<${DEFAULT_FIELD.birthDate.field}>[0-9A-Z<]{6})`, `(?<${DEFAULT_FIELD.birthDateCheck.field}>[0-9A-Z<]{1})`, `(?<${DEFAULT_FIELD.gender.field}>[mfMF<]{1})`, `(?<${DEFAULT_FIELD.expireDate.field}>[0-9A-Z<]{6})`, `(?<${DEFAULT_FIELD.expireDateCheck.field}>[0-9A-Z<]{1})`, `(?<${DEFAULT_FIELD.optionalData.field}>[A-Z0-9<]{14})`, `(?<${DEFAULT_FIELD.optionalDataCheck.field}>[0-9A-Z<]{1})`, `(?<${DEFAULT_FIELD.lineCheck.field}>[0-9A-Z<]{1})`, ].join(''), ), ]; function mrzCleanResult(obj: Record) { Object.entries(obj).forEach(([k, v]) => { obj[k] = v .replace(/ { obj[result.field] = result.value; }); delete obj[value.field]; continue; } } } return obj; } function mrzFieldExtract(expression: RegExp, line: string) { if (expression.test(line)) { return expression.exec(line)?.groups || {}; } return {}; } export function checkSum(data: string) { if (!/[0-9A-Z<]/.test(data)) return null; const sum = data.split('').reduce((a, v, i) => { if (v === '<') return a; const num = Number(v); const weight = [7, 3, 1][i % 3]; if (Number.isNaN(num)) { return a + (v.charCodeAt(0) - 55) * weight; } return a + num * weight; }, 0); return sum % 10; } export function parseType1(mrz: MRZ) { const result: Record = {}; mrz.zone.forEach((line, i) => Object.assign(result, mrzFieldExtract(MRZ_TD_1[i], line)), ); return { mrz, result: mrzCleanResult(result) }; } export function parseType2(mrz: MRZ) { const result: Record = {}; mrz.zone.forEach((line, i) => Object.assign(result, mrzFieldExtract(MRZ_TD_2[i], line)), ); return { mrz, result: mrzCleanResult(result) }; } export function parseType3(mrz: MRZ) { const result: Record = {}; mrz.zone.forEach((line, i) => Object.assign(result, mrzFieldExtract(MRZ_TD_3[i], line)), ); return { mrz, result: mrzCleanResult(result) }; } export function parseMRZ(mrz: MRZ) { if (mrz.type === 'TD1') return parseType1(mrz); if (mrz.type === 'TD2') return parseType2(mrz); if (mrz.type === 'TD3') return parseType3(mrz); return null; }