@test-web/react-native-sdk 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build/.transforms/246c075ea944392e66db7aa639265547/results.bin +1 -0
- package/android/build/.transforms/246c075ea944392e66db7aa639265547/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/yourcompany/sdk/BuildConfig.dex +0 -0
- package/android/build/.transforms/246c075ea944392e66db7aa639265547/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/yourcompany/sdk/YourSDKModule.dex +0 -0
- package/android/build/.transforms/246c075ea944392e66db7aa639265547/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/yourcompany/sdk/YourSDKPackage.dex +0 -0
- package/android/build/.transforms/246c075ea944392e66db7aa639265547/transformed/bundleLibRuntimeToDirDebug/desugar_graph.bin +0 -0
- package/android/build/.transforms/952af5a0e7b5b2ac3d48ad66eccefd1f/results.bin +1 -0
- package/android/build/.transforms/952af5a0e7b5b2ac3d48ad66eccefd1f/transformed/classes/classes_dex/classes.dex +0 -0
- package/android/build/generated/source/buildConfig/debug/com/yourcompany/sdk/BuildConfig.java +10 -0
- package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +34 -0
- package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json +18 -0
- package/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties +6 -0
- package/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json +1 -0
- package/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +0 -0
- package/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar +0 -0
- package/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt +0 -0
- package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -0
- package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +2 -0
- package/android/build/intermediates/incremental/mergeDebugAssets/merger.xml +2 -0
- package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +2 -0
- package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +2 -0
- package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/yourcompany/sdk/BuildConfig.class +0 -0
- package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/yourcompany/sdk/YourSDKModule.class +0 -0
- package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/yourcompany/sdk/YourSDKPackage.class +0 -0
- package/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt +2 -0
- package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +56 -0
- package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +34 -0
- package/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json +1 -0
- package/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt +1 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/yourcompany/sdk/BuildConfig.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/yourcompany/sdk/YourSDKModule.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/yourcompany/sdk/YourSDKPackage.class +0 -0
- package/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar +0 -0
- package/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt +1 -0
- package/android/build/outputs/logs/manifest-merger-debug-report.txt +61 -0
- package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
- package/package.json +1 -1
- package/src/apis/captureBarcode.ts +37 -0
- package/src/apis/captureMRZ.ts +39 -0
- package/src/apis/checkLiveness.ts +48 -0
- package/src/apis/index.ts +26 -75
- package/src/components/common/Footer.tsx +9 -12
- package/src/components/common/Header.tsx +8 -11
- package/src/context/FailedCountContext.tsx +35 -0
- package/src/context/IDMConfigurationContext.tsx +12 -2
- package/src/index.tsx +40 -28
- package/src/screens/BackDocumentAdvice.tsx +13 -18
- package/src/screens/BarcodeAdvice.tsx +39 -19
- package/src/screens/BarcodeCapture.tsx +127 -158
- package/src/screens/DocumentCaptureBack.tsx +145 -102
- package/src/screens/DocumentCaptureFront.tsx +146 -113
- package/src/screens/FrontDocumentAdvice.tsx +13 -18
- package/src/screens/LocationPermission.tsx +124 -17
- package/src/screens/MrzAdvice.tsx +27 -13
- package/src/screens/MrzCapture.tsx +233 -206
- package/src/screens/SelectDocuments.tsx +57 -66
- package/src/screens/SelfieCapture.tsx +135 -103
- package/src/screens/ThankYou.tsx +25 -31
- package/src/screens/VerifyIdentity.tsx +1 -0
- package/src/styles/BarcodeAdviceStyles.ts +6 -6
- package/src/styles/DocumentCaptureBackStyle.ts +70 -0
- package/src/styles/DocumentCaptureFrontStyle.ts +70 -0
- package/src/styles/FooterStyles.ts +10 -1
- package/src/styles/ScannerStyles.ts +46 -9
- package/src/styles/ThankYouStyles.ts +8 -11
- package/src/utils/MRZ_README.md +111 -0
- package/src/utils/imagesHelper.ts +264 -0
- package/src/utils/metadata_new.json +11373 -1
- package/src/utils/mrzExamples.ts +127 -0
- package/src/utils/mrzParser.ts +303 -0
- package/src/utils/mrzUtils.ts +70 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRZ Examples and Utilities
|
|
3
|
+
* Generic utilities for testing MRZ parsing (works with any country)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parseMRZ, formatMRZData } from './mrzParser';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract just the MRZ portion from full card text
|
|
10
|
+
* This helps separate the MRZ lines from other card text
|
|
11
|
+
*/
|
|
12
|
+
export function extractMRZFromFullText(fullText: string): string | null {
|
|
13
|
+
const normalized = fullText.toUpperCase().replace(/\s/g, '');
|
|
14
|
+
|
|
15
|
+
// Try to find TD1 pattern (90 chars)
|
|
16
|
+
const td1Match = normalized.match(/([A-Z0-9<]{30})([A-Z0-9<]{30})([A-Z0-9<]{30})/);
|
|
17
|
+
if (td1Match) {
|
|
18
|
+
const potential = td1Match[1] + td1Match[2] + td1Match[3];
|
|
19
|
+
const parsed = parseMRZ(potential);
|
|
20
|
+
if (parsed && parsed.documentType === 'TD1') {
|
|
21
|
+
return potential;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Try to find TD3 pattern (88 chars)
|
|
26
|
+
const td3Match = normalized.match(/([A-Z0-9<]{44})([A-Z0-9<]{44})/);
|
|
27
|
+
if (td3Match) {
|
|
28
|
+
const potential = td3Match[1] + td3Match[2];
|
|
29
|
+
const parsed = parseMRZ(potential);
|
|
30
|
+
if (parsed && parsed.documentType === 'TD3') {
|
|
31
|
+
return potential;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Try to find TD2 pattern (72 chars)
|
|
36
|
+
const td2Match = normalized.match(/([A-Z0-9<]{36})([A-Z0-9<]{36})/);
|
|
37
|
+
if (td2Match) {
|
|
38
|
+
const potential = td2Match[1] + td2Match[2];
|
|
39
|
+
const parsed = parseMRZ(potential);
|
|
40
|
+
if (parsed && parsed.documentType === 'TD2') {
|
|
41
|
+
return potential;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Test a single MRZ string
|
|
50
|
+
*/
|
|
51
|
+
export function testMRZ(mrzString: string) {
|
|
52
|
+
console.log('\n========== MRZ TEST ==========');
|
|
53
|
+
console.log('Input:', mrzString);
|
|
54
|
+
console.log('Length:', mrzString.length);
|
|
55
|
+
|
|
56
|
+
const parsed = parseMRZ(mrzString);
|
|
57
|
+
|
|
58
|
+
if (parsed) {
|
|
59
|
+
console.log('\n✓ Successfully parsed!');
|
|
60
|
+
console.log(formatMRZData(parsed));
|
|
61
|
+
} else {
|
|
62
|
+
console.log('\n✗ Failed to parse MRZ');
|
|
63
|
+
console.log('Expected lengths: 90 (TD1), 72 (TD2), or 88 (TD3)');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log('==============================\n');
|
|
67
|
+
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Common OCR errors and corrections
|
|
73
|
+
*/
|
|
74
|
+
export const OCR_CORRECTIONS = {
|
|
75
|
+
'O': '0', // Letter O to zero
|
|
76
|
+
'I': '1', // Letter I to one
|
|
77
|
+
'S': '5', // Letter S to five
|
|
78
|
+
'Z': '2', // Letter Z to two
|
|
79
|
+
'B': '8', // Letter B to eight
|
|
80
|
+
'G': '6', // Letter G to six
|
|
81
|
+
'«': '<', // Double angle quote to less than
|
|
82
|
+
'»': '<', // Double angle quote to less than
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Tips for scanning MRZ codes
|
|
87
|
+
*/
|
|
88
|
+
export const SCANNING_TIPS = [
|
|
89
|
+
'Ensure good lighting - avoid shadows and glare',
|
|
90
|
+
'Hold the document flat and steady',
|
|
91
|
+
'Align the MRZ zone within the camera frame',
|
|
92
|
+
'Keep the camera parallel to the document',
|
|
93
|
+
'Clean the camera lens if text is blurry',
|
|
94
|
+
'For passports, scan the bottom 2 lines',
|
|
95
|
+
'For ID cards, scan the bottom 3 lines',
|
|
96
|
+
'Avoid reflections from laminated surfaces',
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Document type detection guide
|
|
101
|
+
*/
|
|
102
|
+
export const DOCUMENT_TYPES = {
|
|
103
|
+
TD1: {
|
|
104
|
+
name: 'TD1 - ID Card Size',
|
|
105
|
+
lines: 3,
|
|
106
|
+
charsPerLine: 30,
|
|
107
|
+
totalChars: 90,
|
|
108
|
+
examples: ['National ID Cards', 'Residence Permits', 'Some Driver Licenses'],
|
|
109
|
+
description: 'Used by most countries for national ID cards',
|
|
110
|
+
},
|
|
111
|
+
TD2: {
|
|
112
|
+
name: 'TD2 - Intermediate Size',
|
|
113
|
+
lines: 2,
|
|
114
|
+
charsPerLine: 36,
|
|
115
|
+
totalChars: 72,
|
|
116
|
+
examples: ['Some Passports', 'Travel Documents'],
|
|
117
|
+
description: 'Less common format for travel documents',
|
|
118
|
+
},
|
|
119
|
+
TD3: {
|
|
120
|
+
name: 'TD3 - Passport Size',
|
|
121
|
+
lines: 2,
|
|
122
|
+
charsPerLine: 44,
|
|
123
|
+
totalChars: 88,
|
|
124
|
+
examples: ['Passports', 'Passport Cards'],
|
|
125
|
+
description: 'Standard passport format used worldwide',
|
|
126
|
+
},
|
|
127
|
+
};
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRZ (Machine Readable Zone) Parser
|
|
3
|
+
* Supports TD1 (ID Cards), TD2, and TD3 (Passports) formats
|
|
4
|
+
*
|
|
5
|
+
* Based on ICAO Document 9303 standards
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface MRZData {
|
|
9
|
+
documentType: 'TD1' | 'TD2' | 'TD3';
|
|
10
|
+
documentCode: string;
|
|
11
|
+
issuingCountry: string;
|
|
12
|
+
documentNumber: string;
|
|
13
|
+
dateOfBirth: string;
|
|
14
|
+
sex: string;
|
|
15
|
+
expirationDate: string;
|
|
16
|
+
nationality: string;
|
|
17
|
+
surname: string;
|
|
18
|
+
givenNames: string;
|
|
19
|
+
optionalData?: string;
|
|
20
|
+
rawMRZ: string;
|
|
21
|
+
isValid: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Normalize MRZ text by replacing common OCR errors
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeMRZ(text: string): string {
|
|
28
|
+
return text
|
|
29
|
+
.toUpperCase()
|
|
30
|
+
.replace(/\s/g, '')
|
|
31
|
+
.replace(/[«»]/g, '<')
|
|
32
|
+
.replace(/O/g, '0')
|
|
33
|
+
.replace(/[^A-Z0-9<]/g, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Calculate check digit for MRZ validation
|
|
38
|
+
*/
|
|
39
|
+
function calculateCheckDigit(input: string): number {
|
|
40
|
+
const weights = [7, 3, 1];
|
|
41
|
+
let sum = 0;
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < input.length; i++) {
|
|
44
|
+
const char = input[i];
|
|
45
|
+
let value: number;
|
|
46
|
+
|
|
47
|
+
if (char === '<') {
|
|
48
|
+
value = 0;
|
|
49
|
+
} else if (char >= '0' && char <= '9') {
|
|
50
|
+
value = parseInt(char, 10);
|
|
51
|
+
} else if (char >= 'A' && char <= 'Z') {
|
|
52
|
+
value = char.charCodeAt(0) - 'A'.charCodeAt(0) + 10;
|
|
53
|
+
} else {
|
|
54
|
+
value = 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sum += value * weights[i % 3];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return sum % 10;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate check digit
|
|
65
|
+
*/
|
|
66
|
+
function validateCheckDigit(data: string, checkDigit: string): boolean {
|
|
67
|
+
if (checkDigit === '<') return true; // Optional field
|
|
68
|
+
const expected = calculateCheckDigit(data);
|
|
69
|
+
return expected === parseInt(checkDigit, 10);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse date from MRZ format (YYMMDD)
|
|
74
|
+
*/
|
|
75
|
+
function parseDate(dateStr: string): string {
|
|
76
|
+
if (dateStr.length !== 6) return '';
|
|
77
|
+
|
|
78
|
+
const year = parseInt(dateStr.substring(0, 2), 10);
|
|
79
|
+
const month = dateStr.substring(2, 4);
|
|
80
|
+
const day = dateStr.substring(4, 6);
|
|
81
|
+
|
|
82
|
+
// Assume dates > 50 are 19xx, otherwise 20xx
|
|
83
|
+
const fullYear = year > 50 ? 1900 + year : 2000 + year;
|
|
84
|
+
|
|
85
|
+
return `${fullYear}-${month}-${day}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse names from MRZ format
|
|
90
|
+
*/
|
|
91
|
+
function parseNames(nameField: string): { surname: string; givenNames: string } {
|
|
92
|
+
const parts = nameField.split('<<');
|
|
93
|
+
const surname = parts[0].replace(/</g, ' ').trim();
|
|
94
|
+
const givenNames = parts.length > 1 ? parts[1].replace(/</g, ' ').trim() : '';
|
|
95
|
+
|
|
96
|
+
return { surname, givenNames };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse TD1 format (ID Cards - 3 lines of 30 characters)
|
|
101
|
+
* Example from Argentina ID:
|
|
102
|
+
* IDARG323352335<0<<<<<<<<<<<<<<<<<
|
|
103
|
+
* 8612179M3504037ARG<<<<<<<<<<<<8
|
|
104
|
+
* SALVADOR<<HECTOR<MAURICIO<<<<<<
|
|
105
|
+
*/
|
|
106
|
+
export function parseTD1(mrz: string): MRZData | null {
|
|
107
|
+
if (mrz.length !== 90) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const line1 = mrz.substring(0, 30);
|
|
112
|
+
const line2 = mrz.substring(30, 60);
|
|
113
|
+
const line3 = mrz.substring(60, 90);
|
|
114
|
+
|
|
115
|
+
// Line 1: Document code (2), Country (3), Document number (9), Check digit (1), Optional (15)
|
|
116
|
+
const documentCode = line1.substring(0, 2);
|
|
117
|
+
const issuingCountry = line1.substring(2, 5);
|
|
118
|
+
const documentNumber = line1.substring(5, 14).replace(/</g, '');
|
|
119
|
+
const docNumCheck = line1.substring(14, 15);
|
|
120
|
+
const optionalData1 = line1.substring(15, 30).replace(/</g, '');
|
|
121
|
+
|
|
122
|
+
// Line 2: DOB (6), DOB check (1), Sex (1), Expiry (6), Expiry check (1), Nationality (3), Optional (11), Composite check (1)
|
|
123
|
+
const dateOfBirth = parseDate(line2.substring(0, 6));
|
|
124
|
+
const dobCheck = line2.substring(6, 7);
|
|
125
|
+
const sex = line2.substring(7, 8);
|
|
126
|
+
const expirationDate = parseDate(line2.substring(8, 14));
|
|
127
|
+
const expCheck = line2.substring(14, 15);
|
|
128
|
+
const nationality = line2.substring(15, 18);
|
|
129
|
+
const optionalData2 = line2.substring(18, 29).replace(/</g, '');
|
|
130
|
+
|
|
131
|
+
// Line 3: Names
|
|
132
|
+
const { surname, givenNames } = parseNames(line3);
|
|
133
|
+
|
|
134
|
+
// Validate check digits
|
|
135
|
+
const isDocNumValid = validateCheckDigit(line1.substring(5, 14), docNumCheck);
|
|
136
|
+
const isDobValid = validateCheckDigit(line2.substring(0, 6), dobCheck);
|
|
137
|
+
const isExpValid = validateCheckDigit(line2.substring(8, 14), expCheck);
|
|
138
|
+
|
|
139
|
+
const isValid = isDocNumValid && isDobValid && isExpValid;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
documentType: 'TD1',
|
|
143
|
+
documentCode,
|
|
144
|
+
issuingCountry,
|
|
145
|
+
documentNumber,
|
|
146
|
+
dateOfBirth,
|
|
147
|
+
sex: sex === 'M' ? 'Male' : sex === 'F' ? 'Female' : 'Unknown',
|
|
148
|
+
expirationDate,
|
|
149
|
+
nationality,
|
|
150
|
+
surname,
|
|
151
|
+
givenNames,
|
|
152
|
+
optionalData: optionalData1 + optionalData2,
|
|
153
|
+
rawMRZ: mrz,
|
|
154
|
+
isValid,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Parse TD2 format (2 lines of 36 characters)
|
|
160
|
+
*/
|
|
161
|
+
export function parseTD2(mrz: string): MRZData | null {
|
|
162
|
+
if (mrz.length !== 72) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const line1 = mrz.substring(0, 36);
|
|
167
|
+
const line2 = mrz.substring(36, 72);
|
|
168
|
+
|
|
169
|
+
// Line 1: Document code (2), Country (3), Names (31)
|
|
170
|
+
const documentCode = line1.substring(0, 2);
|
|
171
|
+
const issuingCountry = line1.substring(2, 5);
|
|
172
|
+
const { surname, givenNames } = parseNames(line1.substring(5, 36));
|
|
173
|
+
|
|
174
|
+
// Line 2: Document number (9), Check (1), Nationality (3), DOB (6), Check (1), Sex (1), Expiry (6), Check (1), Optional (7), Check (1)
|
|
175
|
+
const documentNumber = line2.substring(0, 9).replace(/</g, '');
|
|
176
|
+
const docNumCheck = line2.substring(9, 10);
|
|
177
|
+
const nationality = line2.substring(10, 13);
|
|
178
|
+
const dateOfBirth = parseDate(line2.substring(13, 19));
|
|
179
|
+
const dobCheck = line2.substring(19, 20);
|
|
180
|
+
const sex = line2.substring(20, 21);
|
|
181
|
+
const expirationDate = parseDate(line2.substring(21, 27));
|
|
182
|
+
const expCheck = line2.substring(27, 28);
|
|
183
|
+
const optionalData = line2.substring(28, 35).replace(/</g, '');
|
|
184
|
+
|
|
185
|
+
const isDocNumValid = validateCheckDigit(line2.substring(0, 9), docNumCheck);
|
|
186
|
+
const isDobValid = validateCheckDigit(line2.substring(13, 19), dobCheck);
|
|
187
|
+
const isExpValid = validateCheckDigit(line2.substring(21, 27), expCheck);
|
|
188
|
+
|
|
189
|
+
const isValid = isDocNumValid && isDobValid && isExpValid;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
documentType: 'TD2',
|
|
193
|
+
documentCode,
|
|
194
|
+
issuingCountry,
|
|
195
|
+
documentNumber,
|
|
196
|
+
dateOfBirth,
|
|
197
|
+
sex: sex === 'M' ? 'Male' : sex === 'F' ? 'Female' : 'Unknown',
|
|
198
|
+
expirationDate,
|
|
199
|
+
nationality,
|
|
200
|
+
surname,
|
|
201
|
+
givenNames,
|
|
202
|
+
optionalData,
|
|
203
|
+
rawMRZ: mrz,
|
|
204
|
+
isValid,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parse TD3 format (Passports - 2 lines of 44 characters)
|
|
210
|
+
* Example from US Passport:
|
|
211
|
+
* P<USAGATCHALIAN<<NICOLE<ANGELINE<LUCIENNE<JA
|
|
212
|
+
* 5778700136USA8904160F28012456149724051<605856
|
|
213
|
+
*/
|
|
214
|
+
export function parseTD3(mrz: string): MRZData | null {
|
|
215
|
+
if (mrz.length !== 88) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const line1 = mrz.substring(0, 44);
|
|
220
|
+
const line2 = mrz.substring(44, 88);
|
|
221
|
+
|
|
222
|
+
// Line 1: Document code (2), Country (3), Names (39)
|
|
223
|
+
const documentCode = line1.substring(0, 2);
|
|
224
|
+
const issuingCountry = line1.substring(2, 5);
|
|
225
|
+
const { surname, givenNames } = parseNames(line1.substring(5, 44));
|
|
226
|
+
|
|
227
|
+
// Line 2: Passport number (9), Check (1), Nationality (3), DOB (6), Check (1), Sex (1), Expiry (6), Check (1), Optional (14), Check (1), Composite check (1)
|
|
228
|
+
const documentNumber = line2.substring(0, 9).replace(/</g, '');
|
|
229
|
+
const docNumCheck = line2.substring(9, 10);
|
|
230
|
+
const nationality = line2.substring(10, 13);
|
|
231
|
+
const dateOfBirth = parseDate(line2.substring(13, 19));
|
|
232
|
+
const dobCheck = line2.substring(19, 20);
|
|
233
|
+
const sex = line2.substring(20, 21);
|
|
234
|
+
const expirationDate = parseDate(line2.substring(21, 27));
|
|
235
|
+
const expCheck = line2.substring(27, 28);
|
|
236
|
+
const optionalData = line2.substring(28, 42).replace(/</g, '');
|
|
237
|
+
|
|
238
|
+
const isDocNumValid = validateCheckDigit(line2.substring(0, 9), docNumCheck);
|
|
239
|
+
const isDobValid = validateCheckDigit(line2.substring(13, 19), dobCheck);
|
|
240
|
+
const isExpValid = validateCheckDigit(line2.substring(21, 27), expCheck);
|
|
241
|
+
|
|
242
|
+
const isValid = isDocNumValid && isDobValid && isExpValid;
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
documentType: 'TD3',
|
|
246
|
+
documentCode,
|
|
247
|
+
issuingCountry,
|
|
248
|
+
documentNumber,
|
|
249
|
+
dateOfBirth,
|
|
250
|
+
sex: sex === 'M' ? 'Male' : sex === 'F' ? 'Female' : 'Unknown',
|
|
251
|
+
expirationDate,
|
|
252
|
+
nationality,
|
|
253
|
+
surname,
|
|
254
|
+
givenNames,
|
|
255
|
+
optionalData,
|
|
256
|
+
rawMRZ: mrz,
|
|
257
|
+
isValid,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Auto-detect and parse MRZ format
|
|
263
|
+
*/
|
|
264
|
+
export function parseMRZ(mrzText: string): MRZData | null {
|
|
265
|
+
const normalized = normalizeMRZ(mrzText);
|
|
266
|
+
|
|
267
|
+
// TD1: 90 characters (3 lines x 30)
|
|
268
|
+
if (normalized.length === 90) {
|
|
269
|
+
return parseTD1(normalized);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// TD2: 72 characters (2 lines x 36)
|
|
273
|
+
if (normalized.length === 72) {
|
|
274
|
+
return parseTD2(normalized);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// TD3: 88 characters (2 lines x 44)
|
|
278
|
+
if (normalized.length === 88) {
|
|
279
|
+
return parseTD3(normalized);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Format MRZ data for display
|
|
287
|
+
*/
|
|
288
|
+
export function formatMRZData(data: MRZData): string {
|
|
289
|
+
return `
|
|
290
|
+
Document Type: ${data.documentType}
|
|
291
|
+
Document Code: ${data.documentCode}
|
|
292
|
+
Issuing Country: ${data.issuingCountry}
|
|
293
|
+
Document Number: ${data.documentNumber}
|
|
294
|
+
Surname: ${data.surname}
|
|
295
|
+
Given Names: ${data.givenNames}
|
|
296
|
+
Date of Birth: ${data.dateOfBirth}
|
|
297
|
+
Sex: ${data.sex}
|
|
298
|
+
Nationality: ${data.nationality}
|
|
299
|
+
Expiration Date: ${data.expirationDate}
|
|
300
|
+
${data.optionalData ? `Optional Data: ${data.optionalData}` : ''}
|
|
301
|
+
Valid: ${data.isValid ? 'Yes' : 'No'}
|
|
302
|
+
`.trim();
|
|
303
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRZ Utilities
|
|
3
|
+
* Based on demo-testing-sdk implementation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalize MRZ text by replacing common OCR errors
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeMrz(text: string): string {
|
|
10
|
+
return text
|
|
11
|
+
.replace(/\s/g, '') // Remove spaces
|
|
12
|
+
.replace(/[«»]/g, '<') // Replace angle quotes
|
|
13
|
+
.replace(/O/g, '0') // Letter O to zero
|
|
14
|
+
.replace(/I/g, '1') // Letter I to one
|
|
15
|
+
.replace(/S/g, '5') // Letter S to five
|
|
16
|
+
.replace(/[^A-Z0-9<]/gi, ''); // Keep only alphanumeric and <
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a line is a valid MRZ line
|
|
21
|
+
*/
|
|
22
|
+
export function isMRZLine(text: string): boolean {
|
|
23
|
+
return /^[A-Z0-9<]{28,46}$/.test(text);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extract MRZ lines from text blocks
|
|
28
|
+
*/
|
|
29
|
+
export function extractMRZLines(textLines: string[]): string[] {
|
|
30
|
+
return textLines
|
|
31
|
+
.map(line => normalizeMrz(line))
|
|
32
|
+
.filter(line => isMRZLine(line));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Detect document type from MRZ lines
|
|
37
|
+
*/
|
|
38
|
+
export function detectDocumentType(mrzLines: string[]): 'TD1' | 'TD2' | 'TD3' | null {
|
|
39
|
+
if (mrzLines.length === 0) return null;
|
|
40
|
+
|
|
41
|
+
const firstLineLength = mrzLines[0].length;
|
|
42
|
+
|
|
43
|
+
// TD1: 3 lines of 30 characters (ID cards)
|
|
44
|
+
if (mrzLines.length >= 3 && firstLineLength === 30) {
|
|
45
|
+
return 'TD1';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// TD3: 2 lines of 44 characters (Passports)
|
|
49
|
+
if (mrzLines.length >= 2 && firstLineLength === 44) {
|
|
50
|
+
return 'TD3';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// TD2: 2 lines of 36 characters
|
|
54
|
+
if (mrzLines.length >= 2 && firstLineLength === 36) {
|
|
55
|
+
return 'TD2';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate MRZ format
|
|
63
|
+
*/
|
|
64
|
+
export function validateMRZ(fullText: string): boolean {
|
|
65
|
+
const lines = fullText.split('\n').filter(l => l.length > 0);
|
|
66
|
+
const mrzLines = extractMRZLines(lines);
|
|
67
|
+
const docType = detectDocumentType(mrzLines);
|
|
68
|
+
|
|
69
|
+
return docType !== null;
|
|
70
|
+
}
|