@trustchex/react-native-sdk 1.503.2 → 1.505.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/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +7 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +20 -3
- package/lib/module/Shared/Components/EIDScanner.js +21 -2
- package/lib/module/Shared/EIDReader/eidReader.js +9 -1
- package/lib/module/Shared/Libs/mrzTransliteration.js +46 -5
- package/lib/module/Shared/Libs/vizName.js +140 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +7 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts +2 -1
- package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/vizName.d.ts +74 -0
- package/lib/typescript/src/Shared/Libs/vizName.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +8 -1
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +10 -0
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +33 -3
- package/src/Shared/Components/EIDScanner.tsx +38 -2
- package/src/Shared/EIDReader/eidReader.ts +9 -1
- package/src/Shared/Libs/mrzTransliteration.ts +52 -1
- package/src/Shared/Libs/vizName.ts +192 -0
- package/src/Shared/Types/documentReadResult.ts +8 -1
- package/src/version.ts +1 -1
|
@@ -86,6 +86,13 @@ const IdentityDocumentEIDScanningScreen = () => {
|
|
|
86
86
|
// the MRZ is on the BACK (idBackSideData), so it must be included
|
|
87
87
|
// — without it, ID-card scans had no VIZ source and hard-blocked.
|
|
88
88
|
passportData?.mrzFields ?? idBackSideData?.mrzFields ?? idFrontSideData?.mrzFields ?? appContext.identificationInfo.scannedDocument?.mrzFields,
|
|
89
|
+
vizImage:
|
|
90
|
+
// Full data-page image from the EID step's camera scan. EIDScanner
|
|
91
|
+
// runs a one-shot OCR pass over it to recover the diacritic printed
|
|
92
|
+
// name when the chip's DG11 is absent or one-sided (e.g. Turkish
|
|
93
|
+
// passports). The live camera only OCRs a cropped MRZ region, so the
|
|
94
|
+
// printed name must be recovered from the full image here.
|
|
95
|
+
passportData?.image ?? idFrontSideData?.image ?? idBackSideData?.image,
|
|
89
96
|
onScanSuccess: (mrzFields, faceImage, mimeType) => {
|
|
90
97
|
if (mrzFields && faceImage && mimeType) {
|
|
91
98
|
if (!appContext.identificationInfo.scannedDocument) {
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useContext, useEffect, useState } from 'react';
|
|
4
4
|
import { buildDocumentName } from "../../Shared/Libs/mrzTransliteration.js";
|
|
5
|
+
import { recoverVizName, recognizeVizBlocks } from "../../Shared/Libs/vizName.js";
|
|
6
|
+
import { debugLog } from "../../Shared/Libs/debug.utils.js";
|
|
5
7
|
import { isCountryAllowed } from "../../Shared/Libs/country-allow.js";
|
|
6
8
|
import { normalizeFromMRZFields } from "../../Shared/Libs/documentDataNormalizer.js";
|
|
7
9
|
import { Alert, SafeAreaView, StyleSheet, View } from 'react-native';
|
|
@@ -90,10 +92,25 @@ const IdentityDocumentScanningScreen = () => {
|
|
|
90
92
|
|
|
91
93
|
// Track successful document scan completion as funnel step
|
|
92
94
|
trackFunnelStep('Document Scan Completed', 2, 5, 'contract_acceptance', true);
|
|
93
|
-
{
|
|
95
|
+
void (async () => {
|
|
94
96
|
const documentData = normalizeFromMRZFields(mrzFields);
|
|
95
97
|
const faceImg = idFrontSideData?.faceImage || passportData?.faceImage;
|
|
96
|
-
|
|
98
|
+
// Recover the printed (VIZ) name with diacritics the MRZ cannot carry.
|
|
99
|
+
// The live camera OCRs only a cropped MRZ region, so the printed name was
|
|
100
|
+
// never recognised there — run a one-shot full-frame OCR pass on the
|
|
101
|
+
// captured data-page image to surface it. The MRZ sits at the bottom, so
|
|
102
|
+
// restrict the name search to blocks above it.
|
|
103
|
+
const docImage = idFrontSideData?.image || passportData?.image;
|
|
104
|
+
const vizBlocks = await recognizeVizBlocks(docImage);
|
|
105
|
+
const mrzTop = vizBlocks.length ? Math.max(...vizBlocks.map(b => b.top)) : undefined;
|
|
106
|
+
const viz = vizBlocks.length ? recoverVizName(vizBlocks, documentData.firstName, documentData.lastName, mrzTop) : null;
|
|
107
|
+
debugLog('VIZName', 'ocr recovery', {
|
|
108
|
+
vizBlockCount: vizBlocks.length,
|
|
109
|
+
matchedFirst: !!viz?.vizFirst,
|
|
110
|
+
matchedLast: !!viz?.vizLast,
|
|
111
|
+
mismatch: viz?.mismatch ?? false
|
|
112
|
+
});
|
|
113
|
+
const builtName = buildDocumentName(documentData.firstName, documentData.lastName, documentData.issuingCountry, undefined, undefined, viz);
|
|
97
114
|
const documentReadResult = {
|
|
98
115
|
document: {
|
|
99
116
|
...documentData,
|
|
@@ -108,7 +125,7 @@ const IdentityDocumentScanningScreen = () => {
|
|
|
108
125
|
};
|
|
109
126
|
appContext.setLastDocumentRead?.(documentReadResult);
|
|
110
127
|
appContext.onDocumentRead?.(documentReadResult);
|
|
111
|
-
}
|
|
128
|
+
})();
|
|
112
129
|
setTimeout(() => navigationManagerRef.current?.navigateToNextStep(), 1000);
|
|
113
130
|
}
|
|
114
131
|
}, [idFrontSideData, idBackSideData, passportData, appContext.identificationInfo, allowedCountries, allowedDocumentTypes, t, appContext]);
|
|
@@ -7,10 +7,11 @@ import NFCManager from 'react-native-nfc-manager';
|
|
|
7
7
|
import DeviceInfo from 'react-native-device-info';
|
|
8
8
|
import PermissionManager from "../Libs/permissions.utils.js";
|
|
9
9
|
import mrzUtils from "../Libs/mrz.utils.js";
|
|
10
|
-
import { debugError } from "../Libs/debug.utils.js";
|
|
10
|
+
import { debugError, debugLog } from "../Libs/debug.utils.js";
|
|
11
11
|
import { eidReader } from "../EIDReader/eidReader.js";
|
|
12
12
|
import NativeProgressBar from "./NativeProgressBar.js";
|
|
13
13
|
import { buildDocumentName } from "../Libs/mrzTransliteration.js";
|
|
14
|
+
import { recoverVizName, recognizeVizBlocks } from "../Libs/vizName.js";
|
|
14
15
|
import { normalizeFromMRZInfo, normalizeFromMRZFields } from "../Libs/documentDataNormalizer.js";
|
|
15
16
|
import { compareChipToViz } from "../Libs/chipVizMatch.js";
|
|
16
17
|
import { useTranslation } from 'react-i18next';
|
|
@@ -36,6 +37,7 @@ const EIDScanner = ({
|
|
|
36
37
|
dateOfExpiry,
|
|
37
38
|
documentType,
|
|
38
39
|
vizMrzFields,
|
|
40
|
+
vizImage,
|
|
39
41
|
onScanSuccess,
|
|
40
42
|
isNFCSupported
|
|
41
43
|
}) => {
|
|
@@ -246,7 +248,24 @@ const EIDScanner = ({
|
|
|
246
248
|
Vibration.vibrate(120);
|
|
247
249
|
const scanDuration = Date.now() - startTime;
|
|
248
250
|
const documentData = normalizeFromMRZInfo(passportData.mrz);
|
|
249
|
-
|
|
251
|
+
// DG11 (the authenticated chip's own printed name) is the preferred
|
|
252
|
+
// correction. When it is absent or one-sided, fall back to recovering
|
|
253
|
+
// the diacritic printed name from the EID step's VIZ camera blocks —
|
|
254
|
+
// this is what fixes e.g. Turkish passports whose DG11 has no usable
|
|
255
|
+
// name. buildDocumentName ranks viz_ocr above dg11, so only pass the
|
|
256
|
+
// VIZ candidate when DG11 did NOT provide a clean two-part name.
|
|
257
|
+
const dg11HasBothNames = !!passportData.dg11FirstName && !!passportData.dg11LastName;
|
|
258
|
+
// Only OCR the VIZ when DG11 didn't already provide a clean name.
|
|
259
|
+
const vizBlocks = !dg11HasBothNames ? await recognizeVizBlocks(vizImage) : [];
|
|
260
|
+
const viz = vizBlocks.length ? recoverVizName(vizBlocks, documentData.firstName, documentData.lastName, Math.max(...vizBlocks.map(b => b.top))) : null;
|
|
261
|
+
debugLog('VIZName', 'eid recovery', {
|
|
262
|
+
dg11HasBothNames,
|
|
263
|
+
vizBlockCount: vizBlocks.length,
|
|
264
|
+
matchedFirst: !!viz?.vizFirst,
|
|
265
|
+
matchedLast: !!viz?.vizLast,
|
|
266
|
+
mismatch: viz?.mismatch ?? false
|
|
267
|
+
});
|
|
268
|
+
const builtName = buildDocumentName(documentData.firstName, documentData.lastName, documentData.issuingCountry, passportData.dg11FirstName, passportData.dg11LastName, viz);
|
|
250
269
|
// MASAK Tebliğ No. 32 Article 4/C(1)(b): the chip identity data must
|
|
251
270
|
// match the data printed on the passport. The printed/VIZ MRZ is the
|
|
252
271
|
// camera scan the EID step itself runs to obtain the BAC/PACE key,
|
|
@@ -621,8 +621,16 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
621
621
|
dg11FirstName = dg11File.getFirstName();
|
|
622
622
|
dg11LastName = dg11File.getLastName();
|
|
623
623
|
dg11Address = dg11File.getAddress();
|
|
624
|
-
|
|
624
|
+
debugLog('EIDReader', 'DG11 read', {
|
|
625
|
+
firstName: dg11FirstName,
|
|
626
|
+
lastName: dg11LastName,
|
|
627
|
+
hasAddress: !!dg11Address
|
|
628
|
+
});
|
|
629
|
+
} catch (e) {
|
|
625
630
|
// DG11 is optional — not all chips have it, and access may be denied
|
|
631
|
+
debugLog('EIDReader', 'DG11 not available', {
|
|
632
|
+
error: e instanceof Error ? e.message : String(e)
|
|
633
|
+
});
|
|
626
634
|
}
|
|
627
635
|
|
|
628
636
|
// Read DG2 (Face Image)
|
|
@@ -10,17 +10,32 @@ const DANISH = [['AA', 'Å'], ['AE', 'Æ'], ['OE', 'Ø']];
|
|
|
10
10
|
const NORWEGIAN = [['AA', 'Å'], ['AE', 'Æ'], ['OE', 'Ø']];
|
|
11
11
|
const FRENCH = [['OE', 'Œ'], ['AE', 'Æ']];
|
|
12
12
|
const ICELANDIC = [['AE', 'Æ'], ['OE', 'Ö'], ['TH', 'Þ'], ['D', 'Ð']];
|
|
13
|
+
|
|
14
|
+
// Finnish/Estonian share the Swedish-style AE→Ä, OE→Ö pair (no Å in names is
|
|
15
|
+
// common, but AA→Å is safe to include for Finnish-Swedish surnames).
|
|
16
|
+
const FINNISH = [['AE', 'Ä'], ['OE', 'Ö'], ['AA', 'Å']];
|
|
17
|
+
const ESTONIAN = [['AE', 'Ä'], ['OE', 'Ö'], ['UE', 'Ü']];
|
|
13
18
|
const COUNTRY_MAP = {
|
|
14
19
|
DEU: GERMAN,
|
|
15
20
|
AUT: GERMAN,
|
|
16
21
|
CHE: GERMAN,
|
|
22
|
+
LIE: GERMAN,
|
|
17
23
|
SWE: SWEDISH,
|
|
18
24
|
DNK: DANISH,
|
|
19
25
|
NOR: NORWEGIAN,
|
|
20
26
|
FRA: FRENCH,
|
|
21
27
|
LUX: FRENCH,
|
|
22
|
-
|
|
28
|
+
BEL: FRENCH,
|
|
29
|
+
ISL: ICELANDIC,
|
|
30
|
+
FIN: FINNISH,
|
|
31
|
+
EST: ESTONIAN
|
|
23
32
|
};
|
|
33
|
+
// NOTE: Turkish (TUR), Hungarian, Polish, Czech, Slovak, etc. are intentionally
|
|
34
|
+
// absent. Their diacritics (Ş Ğ İ Ç, ő ű, ł, ř ž, ą ę) collapse to bare ASCII in
|
|
35
|
+
// the MRZ with NO digraph, so they cannot be reconstructed from the MRZ at all.
|
|
36
|
+
// For those documents the only correct source is the printed VIZ (viz_ocr) or
|
|
37
|
+
// the chip's DG11 — see buildDocumentName's priority order.
|
|
38
|
+
|
|
24
39
|
function applyDigraphMap(token, map) {
|
|
25
40
|
let result = '';
|
|
26
41
|
let i = 0;
|
|
@@ -44,7 +59,30 @@ function applyDigraphMap(token, map) {
|
|
|
44
59
|
function convertName(rawName, map) {
|
|
45
60
|
return rawName.split(' ').map(word => applyDigraphMap(word, map)).join(' ');
|
|
46
61
|
}
|
|
47
|
-
export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg11Last) {
|
|
62
|
+
export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg11Last, viz) {
|
|
63
|
+
// Highest priority: the printed VIZ name recovered via OCR. This is the only
|
|
64
|
+
// source that can restore diacritics the MRZ cannot represent (e.g. Turkish
|
|
65
|
+
// Ş Ğ İ Ç) when the chip's DG11 is absent. `recoverVizName` only returns a
|
|
66
|
+
// candidate that folds to the SAME ASCII as the MRZ name, so accepting it
|
|
67
|
+
// restores diacritics without changing the letters. A letter-level
|
|
68
|
+
// discrepancy comes back as `mismatch` (no candidate applied) and is surfaced
|
|
69
|
+
// via `nameMismatch` for operator review — the MRZ name stays authoritative.
|
|
70
|
+
if (viz && (viz.vizFirst || viz.vizLast)) {
|
|
71
|
+
return {
|
|
72
|
+
rawFirst,
|
|
73
|
+
rawLast,
|
|
74
|
+
displayFirst: viz.vizFirst ?? rawFirst,
|
|
75
|
+
displayLast: viz.vizLast ?? rawLast,
|
|
76
|
+
source: 'viz_ocr',
|
|
77
|
+
nameMismatch: viz.mismatch || undefined
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A VIZ name was detected but disagreed with the MRZ at the letter level and
|
|
82
|
+
// could not be applied. The MRZ stays authoritative through the branches
|
|
83
|
+
// below, but we carry the review flag through so the discrepancy is recorded.
|
|
84
|
+
const nameMismatch = viz?.mismatch ? true : undefined;
|
|
85
|
+
|
|
48
86
|
// Prefer DG11 (the chip's printed name in UTF-8) ONLY when it carries a proper
|
|
49
87
|
// two-part split — both a surname AND a given name. That is the authoritative
|
|
50
88
|
// correction for chips whose DG1 MRZ name lacks a `<<` separator (e.g. Turkish
|
|
@@ -64,7 +102,8 @@ export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg
|
|
|
64
102
|
rawLast,
|
|
65
103
|
displayFirst: dg11First,
|
|
66
104
|
displayLast: dg11Last,
|
|
67
|
-
source: 'dg11'
|
|
105
|
+
source: 'dg11',
|
|
106
|
+
nameMismatch
|
|
68
107
|
};
|
|
69
108
|
}
|
|
70
109
|
const country = (issuingState ?? '').toUpperCase().trim();
|
|
@@ -75,7 +114,8 @@ export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg
|
|
|
75
114
|
rawLast,
|
|
76
115
|
displayFirst: convertName(rawFirst, map),
|
|
77
116
|
displayLast: convertName(rawLast, map),
|
|
78
|
-
source: 'reverse_table'
|
|
117
|
+
source: 'reverse_table',
|
|
118
|
+
nameMismatch
|
|
79
119
|
};
|
|
80
120
|
}
|
|
81
121
|
|
|
@@ -88,6 +128,7 @@ export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg
|
|
|
88
128
|
rawLast,
|
|
89
129
|
displayFirst,
|
|
90
130
|
displayLast,
|
|
91
|
-
source: changed ? 'reverse_table' : 'raw'
|
|
131
|
+
source: changed ? 'reverse_table' : 'raw',
|
|
132
|
+
nameMismatch
|
|
92
133
|
};
|
|
93
134
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Visual Inspection Zone (VIZ) printed-name recovery.
|
|
5
|
+
*
|
|
6
|
+
* The MRZ stores names in ASCII OCR-B with no diacritics, and for some scripts
|
|
7
|
+
* the diacritics are unrecoverable from the MRZ alone — Turkish in particular
|
|
8
|
+
* collapses Ş→S, Ğ→G, İ→I, Ç→C, Ö→O, Ü→U with no ICAO digraph, so "ŞİMŞEK"
|
|
9
|
+
* reads as "SIMSEK" and the true form is lost. The chip's DG11 carries the
|
|
10
|
+
* printed name in UTF-8, but it is optional and often absent.
|
|
11
|
+
*
|
|
12
|
+
* The printed name IS, however, on the document's data page in its native form.
|
|
13
|
+
* ML Kit already recognises every text block on the page (with bounding boxes)
|
|
14
|
+
* for MRZ parsing; this module reuses those blocks to recover the diacritic
|
|
15
|
+
* printed name and reconcile it against the MRZ.
|
|
16
|
+
*
|
|
17
|
+
* Conflict policy (per product decision): the MRZ name stays authoritative. We
|
|
18
|
+
* accept a VIZ candidate ONLY when it folds to the same ASCII as the MRZ name
|
|
19
|
+
* (i.e. it is the same name, just with diacritics restored). When the VIZ name
|
|
20
|
+
* differs at the letter level — a possible OCR misread or a genuine discrepancy
|
|
21
|
+
* — we keep the MRZ value and raise a `nameMismatch` flag for operator review
|
|
22
|
+
* rather than silently overwriting the machine-read name.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { NativeModules } from 'react-native';
|
|
26
|
+
|
|
27
|
+
/** One recognised text block, in the shape ML Kit returns to JS. */
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run a single full-frame OCR pass over the captured document image and return
|
|
31
|
+
* its text blocks.
|
|
32
|
+
*
|
|
33
|
+
* The live camera path OCRs only a cropped MRZ region (tuned for MRZ sharpness),
|
|
34
|
+
* so the printed name above it is never recognised there. This one-shot pass on
|
|
35
|
+
* the FULL captured image surfaces the printed VIZ name for {@link recoverVizName}.
|
|
36
|
+
* It is best-effort: returns [] when the native module is missing, the image is
|
|
37
|
+
* absent, or OCR throws — the caller then simply falls back to DG11/MRZ.
|
|
38
|
+
*
|
|
39
|
+
* @param base64Image Full document image (no data-URI prefix), or null/undefined.
|
|
40
|
+
*/
|
|
41
|
+
export async function recognizeVizBlocks(base64Image) {
|
|
42
|
+
// Resolved per-call (not captured at import) so module-load ordering can't
|
|
43
|
+
// null it out, and so it is mockable in tests.
|
|
44
|
+
const mlkit = NativeModules.MLKitModule;
|
|
45
|
+
if (!base64Image || !mlkit?.recognizeText) return [];
|
|
46
|
+
try {
|
|
47
|
+
const blocks = await mlkit.recognizeText(base64Image);
|
|
48
|
+
return (blocks ?? []).filter(b => !!b?.text).map(b => ({
|
|
49
|
+
text: b.text,
|
|
50
|
+
top: b.boundingBox?.top ?? 0
|
|
51
|
+
}));
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Fold a string to bare ASCII upper-case letters for comparison: strip
|
|
58
|
+
* combining marks via NFD, map the handful of letters that have no combining
|
|
59
|
+
* decomposition (ı, ł, ø, đ, ð, ß, æ, œ, þ), drop everything that is not A–Z.
|
|
60
|
+
* This is the canonical form in which an MRZ name and its printed counterpart
|
|
61
|
+
* become directly comparable.
|
|
62
|
+
*/
|
|
63
|
+
export function foldAscii(value) {
|
|
64
|
+
if (!value) return '';
|
|
65
|
+
return value.normalize('NFD').replace(/[̀-ͯ]/g, '') // combining diacritical marks
|
|
66
|
+
.replace(/ı/g, 'i').replace(/İ/g, 'I').replace(/ø/gi, 'O').replace(/ł/gi, 'L').replace(/đ/gi, 'D').replace(/ð/gi, 'D').replace(/þ/gi, 'TH').replace(/ß/g, 'SS').replace(/æ/gi, 'AE').replace(/œ/gi, 'OE').toUpperCase().replace(/[^A-Z]/g, '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Split a printed-name line into upper-case word tokens (letters + marks). */
|
|
70
|
+
function tokenize(line) {
|
|
71
|
+
return line.split(/[\s,]+/).map(w => w.trim()).filter(w => w.length > 0 && /\p{L}/u.test(w)).map(w => w.toUpperCase());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find the printed token sequence whose folded ASCII equals the folded MRZ name.
|
|
76
|
+
* Returns the diacritic-preserving printed form when found, else null.
|
|
77
|
+
*
|
|
78
|
+
* The candidate lines are searched in document order; the matched window is the
|
|
79
|
+
* shortest run of consecutive tokens whose concatenated fold equals the target.
|
|
80
|
+
* Because the comparison is on the FOLDED form, only an exact same-name match
|
|
81
|
+
* succeeds — a different printed name will not fold-match and yields null.
|
|
82
|
+
*/
|
|
83
|
+
function recoverPrintedName(lines, mrzName) {
|
|
84
|
+
const target = foldAscii(mrzName);
|
|
85
|
+
if (!target) return null;
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
const tokens = tokenize(line);
|
|
88
|
+
// Slide a window over the tokens; the printed name may sit among other
|
|
89
|
+
// label text on the same block ("Surname / Nom ÖZTÜRK").
|
|
90
|
+
for (let start = 0; start < tokens.length; start++) {
|
|
91
|
+
let acc = '';
|
|
92
|
+
const picked = [];
|
|
93
|
+
for (let end = start; end < tokens.length; end++) {
|
|
94
|
+
acc += foldAscii(tokens[end]);
|
|
95
|
+
picked.push(tokens[end]);
|
|
96
|
+
if (acc.length > target.length) break;
|
|
97
|
+
if (acc === target) {
|
|
98
|
+
return picked.join(' ');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Recover the diacritic printed name from VIZ text blocks and reconcile it with
|
|
108
|
+
* the MRZ-derived raw names.
|
|
109
|
+
*
|
|
110
|
+
* @param blocks Recognised text blocks (text + vertical position).
|
|
111
|
+
* @param mrzFirst Raw MRZ given name (ASCII, may be empty).
|
|
112
|
+
* @param mrzLast Raw MRZ surname (ASCII, may be empty).
|
|
113
|
+
* @param mrzTop Vertical position of the MRZ block, when known. Name fields
|
|
114
|
+
* sit ABOVE the MRZ on the data page; passing this restricts the
|
|
115
|
+
* search to blocks above it, avoiding false matches in the MRZ
|
|
116
|
+
* itself. When undefined, all blocks are searched.
|
|
117
|
+
*/
|
|
118
|
+
export function recoverVizName(blocks, mrzFirst, mrzLast, mrzTop) {
|
|
119
|
+
const above = typeof mrzTop === 'number' ? blocks.filter(b => b.top < mrzTop) : blocks;
|
|
120
|
+
// Each block's text may hold several printed lines.
|
|
121
|
+
const lines = above.flatMap(b => b.text.split(/\r?\n/));
|
|
122
|
+
const vizFirst = mrzFirst ? recoverPrintedName(lines, mrzFirst) : null;
|
|
123
|
+
const vizLast = mrzLast ? recoverPrintedName(lines, mrzLast) : null;
|
|
124
|
+
|
|
125
|
+
// Detect a letter-level discrepancy: a printed line clearly carries the name
|
|
126
|
+
// field but its fold does not equal the MRZ name. We approximate this by
|
|
127
|
+
// checking whether ANY line fold-matches; if the MRZ name is non-trivial and
|
|
128
|
+
// nothing matched, the printed name on the page disagrees with the MRZ.
|
|
129
|
+
const lastMissing = !!mrzLast && vizLast === null;
|
|
130
|
+
const firstMissing = !!mrzFirst && vizFirst === null;
|
|
131
|
+
// Only call it a mismatch when there is printed name-like text to compare
|
|
132
|
+
// against — otherwise the VIZ simply was not captured (not a conflict).
|
|
133
|
+
const hasPrintedNameText = lines.some(l => tokenize(l).length > 0);
|
|
134
|
+
const mismatch = hasPrintedNameText && (lastMissing || firstMissing);
|
|
135
|
+
return {
|
|
136
|
+
vizFirst,
|
|
137
|
+
vizLast,
|
|
138
|
+
mismatch
|
|
139
|
+
};
|
|
140
|
+
}
|
package/lib/module/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IdentityDocumentEIDScanningScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx"],"names":[],"mappings":"AAsBA,QAAA,MAAM,iCAAiC,+
|
|
1
|
+
{"version":3,"file":"IdentityDocumentEIDScanningScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx"],"names":[],"mappings":"AAsBA,QAAA,MAAM,iCAAiC,+CAoQtC,CAAC;AAoBF,eAAe,iCAAiC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IdentityDocumentScanningScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"IdentityDocumentScanningScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx"],"names":[],"mappings":"AA0BA,QAAA,MAAM,8BAA8B,+CAiNnC,CAAC;AAgBF,eAAe,8BAA8B,CAAC"}
|
|
@@ -10,9 +10,15 @@ interface eIDScannerProps {
|
|
|
10
10
|
* because scannedDocument.mrzFields is only persisted after the chip read.
|
|
11
11
|
*/
|
|
12
12
|
vizMrzFields?: MRZFields;
|
|
13
|
+
/**
|
|
14
|
+
* Full data-page image from the EID step's camera scan. A one-shot OCR pass
|
|
15
|
+
* recovers the diacritic printed name when the chip's DG11 is absent or
|
|
16
|
+
* one-sided — DG11 (the authenticated chip's own name) still takes precedence.
|
|
17
|
+
*/
|
|
18
|
+
vizImage?: string;
|
|
13
19
|
onScanSuccess?: (mrzInfo: MRZFields, faceImage: string, faceImageMimeType: string) => void;
|
|
14
20
|
isNFCSupported?: (supported: boolean) => void;
|
|
15
21
|
}
|
|
16
|
-
declare const EIDScanner: ({ documentNumber, dateOfBirth, dateOfExpiry, documentType, vizMrzFields, onScanSuccess, isNFCSupported, }: eIDScannerProps) => import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
declare const EIDScanner: ({ documentNumber, dateOfBirth, dateOfExpiry, documentType, vizMrzFields, vizImage, onScanSuccess, isNFCSupported, }: eIDScannerProps) => import("react/jsx-runtime").JSX.Element;
|
|
17
23
|
export default EIDScanner;
|
|
18
24
|
//# sourceMappingURL=EIDScanner.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EIDScanner.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/EIDScanner.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"EIDScanner.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/EIDScanner.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AA8BpD,UAAU,eAAe;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,CACd,OAAO,EAAE,SAAS,EAClB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,KACtB,IAAI,CAAC;IACV,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CAC/C;AAED,QAAA,MAAM,UAAU,GAAI,qHASjB,eAAe,4CA67BjB,CAAC;AAsOF,eAAe,UAAU,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eidReader.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/EIDReader/eidReader.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AA2Y7C,QAAA,MAAM,SAAS,GACb,gBAAgB,MAAM,EACtB,aAAa,MAAM,EACnB,cAAc,MAAM,EACpB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EAC7C,UAAU;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,KAClC,OAAO,CACN;IACE,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE;QACZ,SAAS,EAAE,OAAO,yBAAyB,EAAE,eAAe,GAAG,IAAI,CAAC;QACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,GACD,SAAS,
|
|
1
|
+
{"version":3,"file":"eidReader.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/EIDReader/eidReader.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AA2Y7C,QAAA,MAAM,SAAS,GACb,gBAAgB,MAAM,EACtB,aAAa,MAAM,EACnB,cAAc,MAAM,EACpB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EAC7C,UAAU;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,KAClC,OAAO,CACN;IACE,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE;QACZ,SAAS,EAAE,OAAO,yBAAyB,EAAE,eAAe,GAAG,IAAI,CAAC;QACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,GACD,SAAS,CA4cZ,CAAC;AAEF,OAAO,EAAE,SAAS,EAAE,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { DocumentName } from '../Types/documentReadResult';
|
|
2
|
-
|
|
2
|
+
import type { VizNameCandidate } from './vizName';
|
|
3
|
+
export declare function buildDocumentName(rawFirst: string, rawLast: string, issuingState: string | null | undefined, dg11First?: string | null, dg11Last?: string | null, viz?: VizNameCandidate | null): DocumentName;
|
|
3
4
|
//# sourceMappingURL=mrzTransliteration.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mrzTransliteration.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/mrzTransliteration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"mrzTransliteration.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/mrzTransliteration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAgHlD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACvC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,EACxB,GAAG,CAAC,EAAE,gBAAgB,GAAG,IAAI,GAC5B,YAAY,CA0Ed"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual Inspection Zone (VIZ) printed-name recovery.
|
|
3
|
+
*
|
|
4
|
+
* The MRZ stores names in ASCII OCR-B with no diacritics, and for some scripts
|
|
5
|
+
* the diacritics are unrecoverable from the MRZ alone — Turkish in particular
|
|
6
|
+
* collapses Ş→S, Ğ→G, İ→I, Ç→C, Ö→O, Ü→U with no ICAO digraph, so "ŞİMŞEK"
|
|
7
|
+
* reads as "SIMSEK" and the true form is lost. The chip's DG11 carries the
|
|
8
|
+
* printed name in UTF-8, but it is optional and often absent.
|
|
9
|
+
*
|
|
10
|
+
* The printed name IS, however, on the document's data page in its native form.
|
|
11
|
+
* ML Kit already recognises every text block on the page (with bounding boxes)
|
|
12
|
+
* for MRZ parsing; this module reuses those blocks to recover the diacritic
|
|
13
|
+
* printed name and reconcile it against the MRZ.
|
|
14
|
+
*
|
|
15
|
+
* Conflict policy (per product decision): the MRZ name stays authoritative. We
|
|
16
|
+
* accept a VIZ candidate ONLY when it folds to the same ASCII as the MRZ name
|
|
17
|
+
* (i.e. it is the same name, just with diacritics restored). When the VIZ name
|
|
18
|
+
* differs at the letter level — a possible OCR misread or a genuine discrepancy
|
|
19
|
+
* — we keep the MRZ value and raise a `nameMismatch` flag for operator review
|
|
20
|
+
* rather than silently overwriting the machine-read name.
|
|
21
|
+
*/
|
|
22
|
+
/** One recognised text block, in the shape ML Kit returns to JS. */
|
|
23
|
+
export interface VizTextBlock {
|
|
24
|
+
/** Full recognised text of the block (may contain multiple lines). */
|
|
25
|
+
text: string;
|
|
26
|
+
/** Vertical position of the block's top edge, in image pixels. */
|
|
27
|
+
top: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run a single full-frame OCR pass over the captured document image and return
|
|
31
|
+
* its text blocks.
|
|
32
|
+
*
|
|
33
|
+
* The live camera path OCRs only a cropped MRZ region (tuned for MRZ sharpness),
|
|
34
|
+
* so the printed name above it is never recognised there. This one-shot pass on
|
|
35
|
+
* the FULL captured image surfaces the printed VIZ name for {@link recoverVizName}.
|
|
36
|
+
* It is best-effort: returns [] when the native module is missing, the image is
|
|
37
|
+
* absent, or OCR throws — the caller then simply falls back to DG11/MRZ.
|
|
38
|
+
*
|
|
39
|
+
* @param base64Image Full document image (no data-URI prefix), or null/undefined.
|
|
40
|
+
*/
|
|
41
|
+
export declare function recognizeVizBlocks(base64Image: string | null | undefined): Promise<VizTextBlock[]>;
|
|
42
|
+
export interface VizNameCandidate {
|
|
43
|
+
/** Diacritic given name recovered from the VIZ, or null when none matched. */
|
|
44
|
+
vizFirst: string | null;
|
|
45
|
+
/** Diacritic surname recovered from the VIZ, or null when none matched. */
|
|
46
|
+
vizLast: string | null;
|
|
47
|
+
/**
|
|
48
|
+
* True when a printed name line was found but its folded ASCII does not match
|
|
49
|
+
* the MRZ name — the candidate is NOT applied; the caller should flag review.
|
|
50
|
+
*/
|
|
51
|
+
mismatch: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Fold a string to bare ASCII upper-case letters for comparison: strip
|
|
55
|
+
* combining marks via NFD, map the handful of letters that have no combining
|
|
56
|
+
* decomposition (ı, ł, ø, đ, ð, ß, æ, œ, þ), drop everything that is not A–Z.
|
|
57
|
+
* This is the canonical form in which an MRZ name and its printed counterpart
|
|
58
|
+
* become directly comparable.
|
|
59
|
+
*/
|
|
60
|
+
export declare function foldAscii(value: string | null | undefined): string;
|
|
61
|
+
/**
|
|
62
|
+
* Recover the diacritic printed name from VIZ text blocks and reconcile it with
|
|
63
|
+
* the MRZ-derived raw names.
|
|
64
|
+
*
|
|
65
|
+
* @param blocks Recognised text blocks (text + vertical position).
|
|
66
|
+
* @param mrzFirst Raw MRZ given name (ASCII, may be empty).
|
|
67
|
+
* @param mrzLast Raw MRZ surname (ASCII, may be empty).
|
|
68
|
+
* @param mrzTop Vertical position of the MRZ block, when known. Name fields
|
|
69
|
+
* sit ABOVE the MRZ on the data page; passing this restricts the
|
|
70
|
+
* search to blocks above it, avoiding false matches in the MRZ
|
|
71
|
+
* itself. When undefined, all blocks are searched.
|
|
72
|
+
*/
|
|
73
|
+
export declare function recoverVizName(blocks: VizTextBlock[], mrzFirst: string, mrzLast: string, mrzTop?: number): VizNameCandidate;
|
|
74
|
+
//# sourceMappingURL=vizName.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vizName.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/vizName.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;CACb;AAWD;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,OAAO,CAAC,YAAY,EAAE,CAAC,CAczB;AAED,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAiBlE;AA4CD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,YAAY,EAAE,EACtB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,gBAAgB,CAuBlB"}
|
|
@@ -3,11 +3,12 @@ import type { ChipVizMatchResult } from '../Libs/chipVizMatch';
|
|
|
3
3
|
/**
|
|
4
4
|
* How the display name was derived from the raw MRZ ASCII name.
|
|
5
5
|
*
|
|
6
|
+
* - `viz_ocr` — recovered from the printed VIZ via OCR (exact printed name with diacritics)
|
|
6
7
|
* - `dg11` — read from the chip's DG11 supplemental data file (exact printed name)
|
|
7
8
|
* - `reverse_table` — reconstructed via country-aware ICAO 9303 reverse transliteration table
|
|
8
9
|
* - `raw` — no conversion applied; display equals the raw MRZ value
|
|
9
10
|
*/
|
|
10
|
-
export type DocumentNameSource = 'dg11' | 'reverse_table' | 'raw';
|
|
11
|
+
export type DocumentNameSource = 'viz_ocr' | 'dg11' | 'reverse_table' | 'raw';
|
|
11
12
|
export interface DocumentName {
|
|
12
13
|
/** First name exactly as stored in the MRZ (ASCII, OCR-B, e.g. "GOEKHAN"). */
|
|
13
14
|
rawFirst: string;
|
|
@@ -19,6 +20,12 @@ export interface DocumentName {
|
|
|
19
20
|
displayLast: string;
|
|
20
21
|
/** How displayFirst / displayLast were derived. */
|
|
21
22
|
source: DocumentNameSource;
|
|
23
|
+
/**
|
|
24
|
+
* True when a printed VIZ name was detected but disagreed with the MRZ name
|
|
25
|
+
* at the letter level (beyond diacritics). The MRZ value is kept as the
|
|
26
|
+
* display name; this flag signals the discrepancy for operator review.
|
|
27
|
+
*/
|
|
28
|
+
nameMismatch?: boolean;
|
|
22
29
|
}
|
|
23
30
|
export interface DocumentFaceImage {
|
|
24
31
|
/** Base64-encoded image data (no data-URI prefix). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documentReadResult.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Types/documentReadResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D
|
|
1
|
+
{"version":3,"file":"documentReadResult.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Types/documentReadResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,MAAM,GAAG,eAAe,GAAG,KAAK,CAAC;AAE9E,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,MAAM,EAAE,kBAAkB,CAAC;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,QAAQ,EAAE,YAAY,CAAC;IACvB;;;;OAIG;IACH,IAAI,EAAE,YAAY,CAAC;IACnB;;;;OAIG;IACH,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC;;;;;OAKG;IACH,WAAW,CAAC,EAAE;QACZ,eAAe,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,IAAI,CAAC;QAC5D,qEAAqE;QACrE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.505.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -116,6 +116,16 @@ const IdentityDocumentEIDScanningScreen = () => {
|
|
|
116
116
|
idFrontSideData?.mrzFields ??
|
|
117
117
|
appContext.identificationInfo.scannedDocument?.mrzFields
|
|
118
118
|
}
|
|
119
|
+
vizImage={
|
|
120
|
+
// Full data-page image from the EID step's camera scan. EIDScanner
|
|
121
|
+
// runs a one-shot OCR pass over it to recover the diacritic printed
|
|
122
|
+
// name when the chip's DG11 is absent or one-sided (e.g. Turkish
|
|
123
|
+
// passports). The live camera only OCRs a cropped MRZ region, so the
|
|
124
|
+
// printed name must be recovered from the full image here.
|
|
125
|
+
passportData?.image ??
|
|
126
|
+
idFrontSideData?.image ??
|
|
127
|
+
idBackSideData?.image
|
|
128
|
+
}
|
|
119
129
|
onScanSuccess={(mrzFields, faceImage, mimeType) => {
|
|
120
130
|
if (mrzFields && faceImage && mimeType) {
|
|
121
131
|
if (!appContext.identificationInfo.scannedDocument) {
|
|
@@ -2,6 +2,8 @@ import React, { useContext, useEffect, useState } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import type { MRZFields } from '../../Shared/Types/mrzFields';
|
|
4
4
|
import { buildDocumentName } from '../../Shared/Libs/mrzTransliteration';
|
|
5
|
+
import { recoverVizName, recognizeVizBlocks } from '../../Shared/Libs/vizName';
|
|
6
|
+
import { debugLog } from '../../Shared/Libs/debug.utils';
|
|
5
7
|
import { isCountryAllowed } from '../../Shared/Libs/country-allow';
|
|
6
8
|
import { normalizeFromMRZFields } from '../../Shared/Libs/documentDataNormalizer';
|
|
7
9
|
import { Alert, SafeAreaView, StyleSheet, View } from 'react-native';
|
|
@@ -134,13 +136,41 @@ const IdentityDocumentScanningScreen = () => {
|
|
|
134
136
|
true
|
|
135
137
|
);
|
|
136
138
|
|
|
137
|
-
{
|
|
139
|
+
void (async () => {
|
|
138
140
|
const documentData = normalizeFromMRZFields(mrzFields);
|
|
139
141
|
const faceImg = idFrontSideData?.faceImage || passportData?.faceImage;
|
|
142
|
+
// Recover the printed (VIZ) name with diacritics the MRZ cannot carry.
|
|
143
|
+
// The live camera OCRs only a cropped MRZ region, so the printed name was
|
|
144
|
+
// never recognised there — run a one-shot full-frame OCR pass on the
|
|
145
|
+
// captured data-page image to surface it. The MRZ sits at the bottom, so
|
|
146
|
+
// restrict the name search to blocks above it.
|
|
147
|
+
const docImage = (idFrontSideData?.image ||
|
|
148
|
+
passportData?.image) as string | undefined;
|
|
149
|
+
const vizBlocks = await recognizeVizBlocks(docImage);
|
|
150
|
+
const mrzTop = vizBlocks.length
|
|
151
|
+
? Math.max(...vizBlocks.map((b) => b.top))
|
|
152
|
+
: undefined;
|
|
153
|
+
const viz = vizBlocks.length
|
|
154
|
+
? recoverVizName(
|
|
155
|
+
vizBlocks,
|
|
156
|
+
documentData.firstName,
|
|
157
|
+
documentData.lastName,
|
|
158
|
+
mrzTop
|
|
159
|
+
)
|
|
160
|
+
: null;
|
|
161
|
+
debugLog('VIZName', 'ocr recovery', {
|
|
162
|
+
vizBlockCount: vizBlocks.length,
|
|
163
|
+
matchedFirst: !!viz?.vizFirst,
|
|
164
|
+
matchedLast: !!viz?.vizLast,
|
|
165
|
+
mismatch: viz?.mismatch ?? false,
|
|
166
|
+
});
|
|
140
167
|
const builtName = buildDocumentName(
|
|
141
168
|
documentData.firstName,
|
|
142
169
|
documentData.lastName,
|
|
143
|
-
documentData.issuingCountry
|
|
170
|
+
documentData.issuingCountry,
|
|
171
|
+
undefined,
|
|
172
|
+
undefined,
|
|
173
|
+
viz
|
|
144
174
|
);
|
|
145
175
|
const documentReadResult = {
|
|
146
176
|
document: {
|
|
@@ -153,7 +183,7 @@ const IdentityDocumentScanningScreen = () => {
|
|
|
153
183
|
};
|
|
154
184
|
appContext.setLastDocumentRead?.(documentReadResult);
|
|
155
185
|
appContext.onDocumentRead?.(documentReadResult);
|
|
156
|
-
}
|
|
186
|
+
})();
|
|
157
187
|
|
|
158
188
|
setTimeout(
|
|
159
189
|
() => navigationManagerRef.current?.navigateToNextStep(),
|
|
@@ -13,12 +13,13 @@ import NFCManager from 'react-native-nfc-manager';
|
|
|
13
13
|
import DeviceInfo from 'react-native-device-info';
|
|
14
14
|
import PermissionManager from '../Libs/permissions.utils';
|
|
15
15
|
import mrzUtils from '../Libs/mrz.utils';
|
|
16
|
-
import { debugError } from '../Libs/debug.utils';
|
|
16
|
+
import { debugError, debugLog } from '../Libs/debug.utils';
|
|
17
17
|
import { MRZInfo } from '../EIDReader/lds/icao/mrzInfo';
|
|
18
18
|
import { eidReader } from '../EIDReader/eidReader';
|
|
19
19
|
import NativeProgressBar from './NativeProgressBar';
|
|
20
20
|
import type { MRZFields } from '../Types/mrzFields';
|
|
21
21
|
import { buildDocumentName } from '../Libs/mrzTransliteration';
|
|
22
|
+
import { recoverVizName, recognizeVizBlocks } from '../Libs/vizName';
|
|
22
23
|
import type { DocumentName } from '../Types/documentReadResult';
|
|
23
24
|
import {
|
|
24
25
|
normalizeFromMRZInfo,
|
|
@@ -57,6 +58,12 @@ interface eIDScannerProps {
|
|
|
57
58
|
* because scannedDocument.mrzFields is only persisted after the chip read.
|
|
58
59
|
*/
|
|
59
60
|
vizMrzFields?: MRZFields;
|
|
61
|
+
/**
|
|
62
|
+
* Full data-page image from the EID step's camera scan. A one-shot OCR pass
|
|
63
|
+
* recovers the diacritic printed name when the chip's DG11 is absent or
|
|
64
|
+
* one-sided — DG11 (the authenticated chip's own name) still takes precedence.
|
|
65
|
+
*/
|
|
66
|
+
vizImage?: string;
|
|
60
67
|
onScanSuccess?: (
|
|
61
68
|
mrzInfo: MRZFields,
|
|
62
69
|
faceImage: string,
|
|
@@ -71,6 +78,7 @@ const EIDScanner = ({
|
|
|
71
78
|
dateOfExpiry,
|
|
72
79
|
documentType,
|
|
73
80
|
vizMrzFields,
|
|
81
|
+
vizImage,
|
|
74
82
|
onScanSuccess,
|
|
75
83
|
isNFCSupported,
|
|
76
84
|
}: eIDScannerProps) => {
|
|
@@ -337,12 +345,40 @@ const EIDScanner = ({
|
|
|
337
345
|
Vibration.vibrate(120);
|
|
338
346
|
const scanDuration = Date.now() - startTime;
|
|
339
347
|
const documentData = normalizeFromMRZInfo(passportData.mrz);
|
|
348
|
+
// DG11 (the authenticated chip's own printed name) is the preferred
|
|
349
|
+
// correction. When it is absent or one-sided, fall back to recovering
|
|
350
|
+
// the diacritic printed name from the EID step's VIZ camera blocks —
|
|
351
|
+
// this is what fixes e.g. Turkish passports whose DG11 has no usable
|
|
352
|
+
// name. buildDocumentName ranks viz_ocr above dg11, so only pass the
|
|
353
|
+
// VIZ candidate when DG11 did NOT provide a clean two-part name.
|
|
354
|
+
const dg11HasBothNames =
|
|
355
|
+
!!passportData.dg11FirstName && !!passportData.dg11LastName;
|
|
356
|
+
// Only OCR the VIZ when DG11 didn't already provide a clean name.
|
|
357
|
+
const vizBlocks = !dg11HasBothNames
|
|
358
|
+
? await recognizeVizBlocks(vizImage)
|
|
359
|
+
: [];
|
|
360
|
+
const viz = vizBlocks.length
|
|
361
|
+
? recoverVizName(
|
|
362
|
+
vizBlocks,
|
|
363
|
+
documentData.firstName,
|
|
364
|
+
documentData.lastName,
|
|
365
|
+
Math.max(...vizBlocks.map((b) => b.top))
|
|
366
|
+
)
|
|
367
|
+
: null;
|
|
368
|
+
debugLog('VIZName', 'eid recovery', {
|
|
369
|
+
dg11HasBothNames,
|
|
370
|
+
vizBlockCount: vizBlocks.length,
|
|
371
|
+
matchedFirst: !!viz?.vizFirst,
|
|
372
|
+
matchedLast: !!viz?.vizLast,
|
|
373
|
+
mismatch: viz?.mismatch ?? false,
|
|
374
|
+
});
|
|
340
375
|
const builtName = buildDocumentName(
|
|
341
376
|
documentData.firstName,
|
|
342
377
|
documentData.lastName,
|
|
343
378
|
documentData.issuingCountry,
|
|
344
379
|
passportData.dg11FirstName,
|
|
345
|
-
passportData.dg11LastName
|
|
380
|
+
passportData.dg11LastName,
|
|
381
|
+
viz
|
|
346
382
|
);
|
|
347
383
|
// MASAK Tebliğ No. 32 Article 4/C(1)(b): the chip identity data must
|
|
348
384
|
// match the data printed on the passport. The printed/VIZ MRZ is the
|
|
@@ -732,8 +732,16 @@ const eidReader = async (
|
|
|
732
732
|
dg11FirstName = dg11File.getFirstName();
|
|
733
733
|
dg11LastName = dg11File.getLastName();
|
|
734
734
|
dg11Address = dg11File.getAddress();
|
|
735
|
-
|
|
735
|
+
debugLog('EIDReader', 'DG11 read', {
|
|
736
|
+
firstName: dg11FirstName,
|
|
737
|
+
lastName: dg11LastName,
|
|
738
|
+
hasAddress: !!dg11Address,
|
|
739
|
+
});
|
|
740
|
+
} catch (e) {
|
|
736
741
|
// DG11 is optional — not all chips have it, and access may be denied
|
|
742
|
+
debugLog('EIDReader', 'DG11 not available', {
|
|
743
|
+
error: e instanceof Error ? e.message : String(e),
|
|
744
|
+
});
|
|
737
745
|
}
|
|
738
746
|
|
|
739
747
|
// Read DG2 (Face Image)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DocumentName } from '../Types/documentReadResult';
|
|
2
|
+
import type { VizNameCandidate } from './vizName';
|
|
2
3
|
|
|
3
4
|
type DigraphMap = [string, string][];
|
|
4
5
|
|
|
@@ -47,17 +48,40 @@ const ICELANDIC: DigraphMap = [
|
|
|
47
48
|
['D', 'Ð'],
|
|
48
49
|
];
|
|
49
50
|
|
|
51
|
+
// Finnish/Estonian share the Swedish-style AE→Ä, OE→Ö pair (no Å in names is
|
|
52
|
+
// common, but AA→Å is safe to include for Finnish-Swedish surnames).
|
|
53
|
+
const FINNISH: DigraphMap = [
|
|
54
|
+
['AE', 'Ä'],
|
|
55
|
+
['OE', 'Ö'],
|
|
56
|
+
['AA', 'Å'],
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const ESTONIAN: DigraphMap = [
|
|
60
|
+
['AE', 'Ä'],
|
|
61
|
+
['OE', 'Ö'],
|
|
62
|
+
['UE', 'Ü'],
|
|
63
|
+
];
|
|
64
|
+
|
|
50
65
|
const COUNTRY_MAP: Record<string, DigraphMap> = {
|
|
51
66
|
DEU: GERMAN,
|
|
52
67
|
AUT: GERMAN,
|
|
53
68
|
CHE: GERMAN,
|
|
69
|
+
LIE: GERMAN,
|
|
54
70
|
SWE: SWEDISH,
|
|
55
71
|
DNK: DANISH,
|
|
56
72
|
NOR: NORWEGIAN,
|
|
57
73
|
FRA: FRENCH,
|
|
58
74
|
LUX: FRENCH,
|
|
75
|
+
BEL: FRENCH,
|
|
59
76
|
ISL: ICELANDIC,
|
|
77
|
+
FIN: FINNISH,
|
|
78
|
+
EST: ESTONIAN,
|
|
60
79
|
};
|
|
80
|
+
// NOTE: Turkish (TUR), Hungarian, Polish, Czech, Slovak, etc. are intentionally
|
|
81
|
+
// absent. Their diacritics (Ş Ğ İ Ç, ő ű, ł, ř ž, ą ę) collapse to bare ASCII in
|
|
82
|
+
// the MRZ with NO digraph, so they cannot be reconstructed from the MRZ at all.
|
|
83
|
+
// For those documents the only correct source is the printed VIZ (viz_ocr) or
|
|
84
|
+
// the chip's DG11 — see buildDocumentName's priority order.
|
|
61
85
|
|
|
62
86
|
function applyDigraphMap(token: string, map: DigraphMap): string {
|
|
63
87
|
let result = '';
|
|
@@ -92,8 +116,32 @@ export function buildDocumentName(
|
|
|
92
116
|
rawLast: string,
|
|
93
117
|
issuingState: string | null | undefined,
|
|
94
118
|
dg11First?: string | null,
|
|
95
|
-
dg11Last?: string | null
|
|
119
|
+
dg11Last?: string | null,
|
|
120
|
+
viz?: VizNameCandidate | null
|
|
96
121
|
): DocumentName {
|
|
122
|
+
// Highest priority: the printed VIZ name recovered via OCR. This is the only
|
|
123
|
+
// source that can restore diacritics the MRZ cannot represent (e.g. Turkish
|
|
124
|
+
// Ş Ğ İ Ç) when the chip's DG11 is absent. `recoverVizName` only returns a
|
|
125
|
+
// candidate that folds to the SAME ASCII as the MRZ name, so accepting it
|
|
126
|
+
// restores diacritics without changing the letters. A letter-level
|
|
127
|
+
// discrepancy comes back as `mismatch` (no candidate applied) and is surfaced
|
|
128
|
+
// via `nameMismatch` for operator review — the MRZ name stays authoritative.
|
|
129
|
+
if (viz && (viz.vizFirst || viz.vizLast)) {
|
|
130
|
+
return {
|
|
131
|
+
rawFirst,
|
|
132
|
+
rawLast,
|
|
133
|
+
displayFirst: viz.vizFirst ?? rawFirst,
|
|
134
|
+
displayLast: viz.vizLast ?? rawLast,
|
|
135
|
+
source: 'viz_ocr',
|
|
136
|
+
nameMismatch: viz.mismatch || undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// A VIZ name was detected but disagreed with the MRZ at the letter level and
|
|
141
|
+
// could not be applied. The MRZ stays authoritative through the branches
|
|
142
|
+
// below, but we carry the review flag through so the discrepancy is recorded.
|
|
143
|
+
const nameMismatch = viz?.mismatch ? true : undefined;
|
|
144
|
+
|
|
97
145
|
// Prefer DG11 (the chip's printed name in UTF-8) ONLY when it carries a proper
|
|
98
146
|
// two-part split — both a surname AND a given name. That is the authoritative
|
|
99
147
|
// correction for chips whose DG1 MRZ name lacks a `<<` separator (e.g. Turkish
|
|
@@ -114,6 +162,7 @@ export function buildDocumentName(
|
|
|
114
162
|
displayFirst: dg11First as string,
|
|
115
163
|
displayLast: dg11Last as string,
|
|
116
164
|
source: 'dg11',
|
|
165
|
+
nameMismatch,
|
|
117
166
|
};
|
|
118
167
|
}
|
|
119
168
|
|
|
@@ -127,6 +176,7 @@ export function buildDocumentName(
|
|
|
127
176
|
displayFirst: convertName(rawFirst, map),
|
|
128
177
|
displayLast: convertName(rawLast, map),
|
|
129
178
|
source: 'reverse_table',
|
|
179
|
+
nameMismatch,
|
|
130
180
|
};
|
|
131
181
|
}
|
|
132
182
|
|
|
@@ -140,5 +190,6 @@ export function buildDocumentName(
|
|
|
140
190
|
displayFirst,
|
|
141
191
|
displayLast,
|
|
142
192
|
source: changed ? 'reverse_table' : 'raw',
|
|
193
|
+
nameMismatch,
|
|
143
194
|
};
|
|
144
195
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual Inspection Zone (VIZ) printed-name recovery.
|
|
3
|
+
*
|
|
4
|
+
* The MRZ stores names in ASCII OCR-B with no diacritics, and for some scripts
|
|
5
|
+
* the diacritics are unrecoverable from the MRZ alone — Turkish in particular
|
|
6
|
+
* collapses Ş→S, Ğ→G, İ→I, Ç→C, Ö→O, Ü→U with no ICAO digraph, so "ŞİMŞEK"
|
|
7
|
+
* reads as "SIMSEK" and the true form is lost. The chip's DG11 carries the
|
|
8
|
+
* printed name in UTF-8, but it is optional and often absent.
|
|
9
|
+
*
|
|
10
|
+
* The printed name IS, however, on the document's data page in its native form.
|
|
11
|
+
* ML Kit already recognises every text block on the page (with bounding boxes)
|
|
12
|
+
* for MRZ parsing; this module reuses those blocks to recover the diacritic
|
|
13
|
+
* printed name and reconcile it against the MRZ.
|
|
14
|
+
*
|
|
15
|
+
* Conflict policy (per product decision): the MRZ name stays authoritative. We
|
|
16
|
+
* accept a VIZ candidate ONLY when it folds to the same ASCII as the MRZ name
|
|
17
|
+
* (i.e. it is the same name, just with diacritics restored). When the VIZ name
|
|
18
|
+
* differs at the letter level — a possible OCR misread or a genuine discrepancy
|
|
19
|
+
* — we keep the MRZ value and raise a `nameMismatch` flag for operator review
|
|
20
|
+
* rather than silently overwriting the machine-read name.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { NativeModules } from 'react-native';
|
|
24
|
+
|
|
25
|
+
/** One recognised text block, in the shape ML Kit returns to JS. */
|
|
26
|
+
export interface VizTextBlock {
|
|
27
|
+
/** Full recognised text of the block (may contain multiple lines). */
|
|
28
|
+
text: string;
|
|
29
|
+
/** Vertical position of the block's top edge, in image pixels. */
|
|
30
|
+
top: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface NativeTextBlock {
|
|
34
|
+
text?: string;
|
|
35
|
+
boundingBox?: { left: number; top: number; right: number; bottom: number };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface MLKitNativeModule {
|
|
39
|
+
recognizeText: (base64Image: string) => Promise<NativeTextBlock[]>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run a single full-frame OCR pass over the captured document image and return
|
|
44
|
+
* its text blocks.
|
|
45
|
+
*
|
|
46
|
+
* The live camera path OCRs only a cropped MRZ region (tuned for MRZ sharpness),
|
|
47
|
+
* so the printed name above it is never recognised there. This one-shot pass on
|
|
48
|
+
* the FULL captured image surfaces the printed VIZ name for {@link recoverVizName}.
|
|
49
|
+
* It is best-effort: returns [] when the native module is missing, the image is
|
|
50
|
+
* absent, or OCR throws — the caller then simply falls back to DG11/MRZ.
|
|
51
|
+
*
|
|
52
|
+
* @param base64Image Full document image (no data-URI prefix), or null/undefined.
|
|
53
|
+
*/
|
|
54
|
+
export async function recognizeVizBlocks(
|
|
55
|
+
base64Image: string | null | undefined
|
|
56
|
+
): Promise<VizTextBlock[]> {
|
|
57
|
+
// Resolved per-call (not captured at import) so module-load ordering can't
|
|
58
|
+
// null it out, and so it is mockable in tests.
|
|
59
|
+
const mlkit = (NativeModules as { MLKitModule?: MLKitNativeModule })
|
|
60
|
+
.MLKitModule;
|
|
61
|
+
if (!base64Image || !mlkit?.recognizeText) return [];
|
|
62
|
+
try {
|
|
63
|
+
const blocks = await mlkit.recognizeText(base64Image);
|
|
64
|
+
return (blocks ?? [])
|
|
65
|
+
.filter((b): b is NativeTextBlock & { text: string } => !!b?.text)
|
|
66
|
+
.map((b) => ({ text: b.text, top: b.boundingBox?.top ?? 0 }));
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface VizNameCandidate {
|
|
73
|
+
/** Diacritic given name recovered from the VIZ, or null when none matched. */
|
|
74
|
+
vizFirst: string | null;
|
|
75
|
+
/** Diacritic surname recovered from the VIZ, or null when none matched. */
|
|
76
|
+
vizLast: string | null;
|
|
77
|
+
/**
|
|
78
|
+
* True when a printed name line was found but its folded ASCII does not match
|
|
79
|
+
* the MRZ name — the candidate is NOT applied; the caller should flag review.
|
|
80
|
+
*/
|
|
81
|
+
mismatch: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fold a string to bare ASCII upper-case letters for comparison: strip
|
|
86
|
+
* combining marks via NFD, map the handful of letters that have no combining
|
|
87
|
+
* decomposition (ı, ł, ø, đ, ð, ß, æ, œ, þ), drop everything that is not A–Z.
|
|
88
|
+
* This is the canonical form in which an MRZ name and its printed counterpart
|
|
89
|
+
* become directly comparable.
|
|
90
|
+
*/
|
|
91
|
+
export function foldAscii(value: string | null | undefined): string {
|
|
92
|
+
if (!value) return '';
|
|
93
|
+
return value
|
|
94
|
+
.normalize('NFD')
|
|
95
|
+
.replace(/[̀-ͯ]/g, '') // combining diacritical marks
|
|
96
|
+
.replace(/ı/g, 'i')
|
|
97
|
+
.replace(/İ/g, 'I')
|
|
98
|
+
.replace(/ø/gi, 'O')
|
|
99
|
+
.replace(/ł/gi, 'L')
|
|
100
|
+
.replace(/đ/gi, 'D')
|
|
101
|
+
.replace(/ð/gi, 'D')
|
|
102
|
+
.replace(/þ/gi, 'TH')
|
|
103
|
+
.replace(/ß/g, 'SS')
|
|
104
|
+
.replace(/æ/gi, 'AE')
|
|
105
|
+
.replace(/œ/gi, 'OE')
|
|
106
|
+
.toUpperCase()
|
|
107
|
+
.replace(/[^A-Z]/g, '');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Split a printed-name line into upper-case word tokens (letters + marks). */
|
|
111
|
+
function tokenize(line: string): string[] {
|
|
112
|
+
return line
|
|
113
|
+
.split(/[\s,]+/)
|
|
114
|
+
.map((w) => w.trim())
|
|
115
|
+
.filter((w) => w.length > 0 && /\p{L}/u.test(w))
|
|
116
|
+
.map((w) => w.toUpperCase());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Find the printed token sequence whose folded ASCII equals the folded MRZ name.
|
|
121
|
+
* Returns the diacritic-preserving printed form when found, else null.
|
|
122
|
+
*
|
|
123
|
+
* The candidate lines are searched in document order; the matched window is the
|
|
124
|
+
* shortest run of consecutive tokens whose concatenated fold equals the target.
|
|
125
|
+
* Because the comparison is on the FOLDED form, only an exact same-name match
|
|
126
|
+
* succeeds — a different printed name will not fold-match and yields null.
|
|
127
|
+
*/
|
|
128
|
+
function recoverPrintedName(lines: string[], mrzName: string): string | null {
|
|
129
|
+
const target = foldAscii(mrzName);
|
|
130
|
+
if (!target) return null;
|
|
131
|
+
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
const tokens = tokenize(line);
|
|
134
|
+
// Slide a window over the tokens; the printed name may sit among other
|
|
135
|
+
// label text on the same block ("Surname / Nom ÖZTÜRK").
|
|
136
|
+
for (let start = 0; start < tokens.length; start++) {
|
|
137
|
+
let acc = '';
|
|
138
|
+
const picked: string[] = [];
|
|
139
|
+
for (let end = start; end < tokens.length; end++) {
|
|
140
|
+
acc += foldAscii(tokens[end]);
|
|
141
|
+
picked.push(tokens[end]);
|
|
142
|
+
if (acc.length > target.length) break;
|
|
143
|
+
if (acc === target) {
|
|
144
|
+
return picked.join(' ');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Recover the diacritic printed name from VIZ text blocks and reconcile it with
|
|
154
|
+
* the MRZ-derived raw names.
|
|
155
|
+
*
|
|
156
|
+
* @param blocks Recognised text blocks (text + vertical position).
|
|
157
|
+
* @param mrzFirst Raw MRZ given name (ASCII, may be empty).
|
|
158
|
+
* @param mrzLast Raw MRZ surname (ASCII, may be empty).
|
|
159
|
+
* @param mrzTop Vertical position of the MRZ block, when known. Name fields
|
|
160
|
+
* sit ABOVE the MRZ on the data page; passing this restricts the
|
|
161
|
+
* search to blocks above it, avoiding false matches in the MRZ
|
|
162
|
+
* itself. When undefined, all blocks are searched.
|
|
163
|
+
*/
|
|
164
|
+
export function recoverVizName(
|
|
165
|
+
blocks: VizTextBlock[],
|
|
166
|
+
mrzFirst: string,
|
|
167
|
+
mrzLast: string,
|
|
168
|
+
mrzTop?: number
|
|
169
|
+
): VizNameCandidate {
|
|
170
|
+
const above =
|
|
171
|
+
typeof mrzTop === 'number'
|
|
172
|
+
? blocks.filter((b) => b.top < mrzTop)
|
|
173
|
+
: blocks;
|
|
174
|
+
// Each block's text may hold several printed lines.
|
|
175
|
+
const lines = above.flatMap((b) => b.text.split(/\r?\n/));
|
|
176
|
+
|
|
177
|
+
const vizFirst = mrzFirst ? recoverPrintedName(lines, mrzFirst) : null;
|
|
178
|
+
const vizLast = mrzLast ? recoverPrintedName(lines, mrzLast) : null;
|
|
179
|
+
|
|
180
|
+
// Detect a letter-level discrepancy: a printed line clearly carries the name
|
|
181
|
+
// field but its fold does not equal the MRZ name. We approximate this by
|
|
182
|
+
// checking whether ANY line fold-matches; if the MRZ name is non-trivial and
|
|
183
|
+
// nothing matched, the printed name on the page disagrees with the MRZ.
|
|
184
|
+
const lastMissing = !!mrzLast && vizLast === null;
|
|
185
|
+
const firstMissing = !!mrzFirst && vizFirst === null;
|
|
186
|
+
// Only call it a mismatch when there is printed name-like text to compare
|
|
187
|
+
// against — otherwise the VIZ simply was not captured (not a conflict).
|
|
188
|
+
const hasPrintedNameText = lines.some((l) => tokenize(l).length > 0);
|
|
189
|
+
const mismatch = hasPrintedNameText && (lastMissing || firstMissing);
|
|
190
|
+
|
|
191
|
+
return { vizFirst, vizLast, mismatch };
|
|
192
|
+
}
|
|
@@ -4,11 +4,12 @@ import type { ChipVizMatchResult } from '../Libs/chipVizMatch';
|
|
|
4
4
|
/**
|
|
5
5
|
* How the display name was derived from the raw MRZ ASCII name.
|
|
6
6
|
*
|
|
7
|
+
* - `viz_ocr` — recovered from the printed VIZ via OCR (exact printed name with diacritics)
|
|
7
8
|
* - `dg11` — read from the chip's DG11 supplemental data file (exact printed name)
|
|
8
9
|
* - `reverse_table` — reconstructed via country-aware ICAO 9303 reverse transliteration table
|
|
9
10
|
* - `raw` — no conversion applied; display equals the raw MRZ value
|
|
10
11
|
*/
|
|
11
|
-
export type DocumentNameSource = 'dg11' | 'reverse_table' | 'raw';
|
|
12
|
+
export type DocumentNameSource = 'viz_ocr' | 'dg11' | 'reverse_table' | 'raw';
|
|
12
13
|
|
|
13
14
|
export interface DocumentName {
|
|
14
15
|
/** First name exactly as stored in the MRZ (ASCII, OCR-B, e.g. "GOEKHAN"). */
|
|
@@ -21,6 +22,12 @@ export interface DocumentName {
|
|
|
21
22
|
displayLast: string;
|
|
22
23
|
/** How displayFirst / displayLast were derived. */
|
|
23
24
|
source: DocumentNameSource;
|
|
25
|
+
/**
|
|
26
|
+
* True when a printed VIZ name was detected but disagreed with the MRZ name
|
|
27
|
+
* at the letter level (beyond diacritics). The MRZ value is kept as the
|
|
28
|
+
* display name; this flag signals the discrepancy for operator review.
|
|
29
|
+
*/
|
|
30
|
+
nameMismatch?: boolean;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export interface DocumentFaceImage {
|
package/src/version.ts
CHANGED