jws-frontend/src/utils/mrz.ts
Methapon Metanipat 51993e97f2 fix: wrong format
2024-10-02 16:52:57 +07:00

198 lines
5.8 KiB
TypeScript

import moment from 'moment';
type MRZ = {
type: 'TD1' | 'TD2' | 'TD3';
zone: string[];
};
type Field = {
field: string;
format?: (value: string, obj?: Record<string, string>) => string;
};
type FieldList = Record<string, Field>;
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'),
},
country: {
field: 'country',
format: (value, _) => value.replace(/0/, 'O'),
},
nationality: {
field: 'nationality',
format: (value, _) => value.replace(/0/, 'O'),
},
gender: { field: 'sex' },
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<string, string>) {
Object.entries(obj).forEach(([k, v]) => {
obj[k] = v
.replace(/</g, ' ')
.replace(/\s{2,}/, ' ')
.trim();
});
const original = structuredClone(obj);
for (const value of Object.values(DEFAULT_FIELD)) {
if (obj[value.field] && 'format' in value) {
obj[value.field] = value.format(obj[value.field], original);
}
}
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<string, string> = {};
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<string, string> = {};
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<string, string> = {};
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;
}