@trustchex/react-native-sdk 1.361.0 → 1.362.1
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/TrustchexSDK.podspec +3 -1
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +0 -13
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraManager.kt +0 -8
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +60 -39
- package/android/src/main/java/com/trustchex/reactnativesdk/opencv/OpenCVModule.kt +94 -13
- package/ios/Camera/TrustchexCameraManager.m +0 -2
- package/ios/Camera/TrustchexCameraManager.swift +0 -7
- package/ios/Camera/TrustchexCameraView.swift +16 -47
- package/ios/OpenCV/OpenCVHelper.h +17 -0
- package/ios/OpenCV/OpenCVHelper.mm +128 -0
- package/ios/OpenCV/OpenCVModule.h +6 -0
- package/ios/OpenCV/OpenCVModule.mm +141 -0
- package/ios/TrustchexSDK-Bridging-Header.h +8 -0
- package/lib/module/Screens/Debug/MRZTestScreen.js +175 -0
- package/lib/module/Shared/Components/DebugNavigationPanel.js +4 -0
- package/lib/module/Shared/Components/EIDScanner.js +0 -78
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +188 -150
- package/lib/module/Shared/Components/QrCodeScannerCamera.js +0 -3
- package/lib/module/Shared/Libs/mrz.utils.js +265 -0
- package/lib/module/Trustchex.js +4 -0
- package/lib/module/index.js +1 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts +3 -0
- package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts +3 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +0 -19
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +18 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +2 -1
- package/src/Screens/Debug/MRZTestScreen.tsx +209 -0
- package/src/Shared/Components/DebugNavigationPanel.tsx +5 -0
- package/src/Shared/Components/EIDScanner.tsx +0 -53
- package/src/Shared/Components/IdentityDocumentCamera.tsx +228 -146
- package/src/Shared/Components/QrCodeScannerCamera.tsx +0 -9
- package/src/Shared/Components/TrustchexCamera.tsx +0 -20
- package/src/Shared/Libs/mrz.utils.ts +289 -1
- package/src/Trustchex.tsx +5 -0
- package/src/index.tsx +3 -0
- package/src/version.ts +1 -1
- package/android/src/main/java/com/trustchex/reactnativesdk/mrz/MRZValidationModule.kt +0 -785
- package/android/src/main/java/com/trustchex/reactnativesdk/mrz/MRZValidator.kt +0 -419
- package/ios/MRZValidation.m +0 -39
- package/ios/MRZValidation.swift +0 -802
- package/ios/MRZValidator.swift +0 -466
package/ios/MRZValidator.swift
DELETED
|
@@ -1,466 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Pure MRZ validation logic with no React Native dependencies.
|
|
5
|
-
* Used by both MRZValidation and TrustchexCameraView.
|
|
6
|
-
*/
|
|
7
|
-
class MRZValidator {
|
|
8
|
-
|
|
9
|
-
enum MRZFormat: String {
|
|
10
|
-
case TD1 = "TD1"
|
|
11
|
-
case TD2 = "TD2"
|
|
12
|
-
case TD3 = "TD3"
|
|
13
|
-
case UNKNOWN = "UNKNOWN"
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
struct MRZResult {
|
|
17
|
-
let valid: Bool
|
|
18
|
-
let format: String
|
|
19
|
-
let error: String?
|
|
20
|
-
let documentCode: String?
|
|
21
|
-
let issuingState: String?
|
|
22
|
-
let documentNumber: String?
|
|
23
|
-
let lastName: String?
|
|
24
|
-
let firstName: String?
|
|
25
|
-
let birthDate: String?
|
|
26
|
-
let sex: String?
|
|
27
|
-
let expirationDate: String?
|
|
28
|
-
let nationality: String?
|
|
29
|
-
let optional1: String?
|
|
30
|
-
let optional2: String?
|
|
31
|
-
let rawLines: String?
|
|
32
|
-
|
|
33
|
-
func toDictionary() -> [String: Any] {
|
|
34
|
-
var dict: [String: Any] = ["valid": valid, "format": format]
|
|
35
|
-
if let e = error { dict["error"] = e }
|
|
36
|
-
if let v = documentCode { dict["documentCode"] = v }
|
|
37
|
-
if let v = issuingState { dict["issuingState"] = v }
|
|
38
|
-
if let v = documentNumber { dict["documentNumber"] = v }
|
|
39
|
-
if let v = lastName { dict["lastName"] = v }
|
|
40
|
-
if let v = firstName { dict["firstName"] = v }
|
|
41
|
-
if let v = birthDate { dict["birthDate"] = v }
|
|
42
|
-
if let v = sex { dict["sex"] = v }
|
|
43
|
-
if let v = expirationDate { dict["expirationDate"] = v }
|
|
44
|
-
if let v = nationality { dict["nationality"] = v }
|
|
45
|
-
if let v = optional1 { dict["optional1"] = v }
|
|
46
|
-
if let v = optional2 { dict["optional2"] = v }
|
|
47
|
-
if let v = rawLines { dict["rawLines"] = v }
|
|
48
|
-
return dict
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
func validateWithCorrections(_ ocrText: String) -> MRZResult {
|
|
53
|
-
let fixedText = fixMRZ(ocrText)
|
|
54
|
-
var lines = fixedText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
55
|
-
.components(separatedBy: "\n")
|
|
56
|
-
.map { $0.trimmingCharacters(in: .whitespaces) }
|
|
57
|
-
|
|
58
|
-
if lines.isEmpty || fixedText.count < 60 {
|
|
59
|
-
return MRZResult(valid: false, format: "UNKNOWN", error: "MRZ text too short",
|
|
60
|
-
documentCode: nil, issuingState: nil, documentNumber: nil, lastName: nil,
|
|
61
|
-
firstName: nil, birthDate: nil, sex: nil, expirationDate: nil,
|
|
62
|
-
nationality: nil, optional1: nil, optional2: nil, rawLines: nil)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
let format = detectMRZFormat(lines)
|
|
66
|
-
guard format != .UNKNOWN else {
|
|
67
|
-
return MRZResult(valid: false, format: "UNKNOWN", error: "Unknown MRZ format",
|
|
68
|
-
documentCode: nil, issuingState: nil, documentNumber: nil, lastName: nil,
|
|
69
|
-
firstName: nil, birthDate: nil, sex: nil, expirationDate: nil,
|
|
70
|
-
nationality: nil, optional1: nil, optional2: nil, rawLines: nil)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Phase 1
|
|
74
|
-
var result = parseMRZ(fixedText)
|
|
75
|
-
if result.valid { return result }
|
|
76
|
-
|
|
77
|
-
// Phase 2: composite brute-force
|
|
78
|
-
let compositePos: (line: Int, pos: Int)?
|
|
79
|
-
switch format {
|
|
80
|
-
case .TD1: compositePos = (1, 29)
|
|
81
|
-
case .TD2: compositePos = (1, 35)
|
|
82
|
-
case .TD3: compositePos = (1, 43)
|
|
83
|
-
case .UNKNOWN: compositePos = nil
|
|
84
|
-
}
|
|
85
|
-
if let pos = compositePos {
|
|
86
|
-
for digit in 0...9 {
|
|
87
|
-
var testLines = lines
|
|
88
|
-
var chars = Array(testLines[pos.line])
|
|
89
|
-
chars[pos.pos] = Character(String(digit))
|
|
90
|
-
testLines[pos.line] = String(chars)
|
|
91
|
-
let testResult = parseMRZ(testLines.joined(separator: "\n"))
|
|
92
|
-
if testResult.valid { return testResult }
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Phase 3: O/0 permutations
|
|
97
|
-
for permutation in generateO0Permutations(fixedText) {
|
|
98
|
-
let testResult = parseMRZ(permutation)
|
|
99
|
-
if testResult.valid { return testResult }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Phase 4: I/1 permutations
|
|
103
|
-
for permutation in generateI1Permutations(fixedText) {
|
|
104
|
-
let testResult = parseMRZ(permutation)
|
|
105
|
-
if testResult.valid { return testResult }
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Phase 5: S/5 permutations
|
|
109
|
-
for permutation in generatePairPermutations(fixedText, letter: "S", digit: "5") {
|
|
110
|
-
let testResult = parseMRZ(permutation)
|
|
111
|
-
if testResult.valid { return testResult }
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Phase 6: B/8 permutations
|
|
115
|
-
for permutation in generatePairPermutations(fixedText, letter: "B", digit: "8") {
|
|
116
|
-
let testResult = parseMRZ(permutation)
|
|
117
|
-
if testResult.valid { return testResult }
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Phase 7: Z/2 permutations
|
|
121
|
-
for permutation in generatePairPermutations(fixedText, letter: "Z", digit: "2") {
|
|
122
|
-
let testResult = parseMRZ(permutation)
|
|
123
|
-
if testResult.valid { return testResult }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Phase 8: G/6 permutations
|
|
127
|
-
for permutation in generatePairPermutations(fixedText, letter: "G", digit: "6") {
|
|
128
|
-
let testResult = parseMRZ(permutation)
|
|
129
|
-
if testResult.valid { return testResult }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return result
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private func parseMRZ(_ rawText: String) -> MRZResult {
|
|
136
|
-
let lines = rawText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
137
|
-
.components(separatedBy: "\n")
|
|
138
|
-
.map { $0.trimmingCharacters(in: .whitespaces) }
|
|
139
|
-
|
|
140
|
-
let format = detectMRZFormat(lines)
|
|
141
|
-
guard format != .UNKNOWN else {
|
|
142
|
-
return MRZResult(valid: false, format: "UNKNOWN", error: "Unknown MRZ format",
|
|
143
|
-
documentCode: nil, issuingState: nil, documentNumber: nil, lastName: nil,
|
|
144
|
-
firstName: nil, birthDate: nil, sex: nil, expirationDate: nil,
|
|
145
|
-
nationality: nil, optional1: nil, optional2: nil, rawLines: nil)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
let fields: MRZResult
|
|
149
|
-
switch format {
|
|
150
|
-
case .TD1: fields = parseTD1(lines, format: format)
|
|
151
|
-
case .TD2: fields = parseTD2(lines, format: format)
|
|
152
|
-
case .TD3: fields = parseTD3(lines, format: format)
|
|
153
|
-
case .UNKNOWN:
|
|
154
|
-
return MRZResult(valid: false, format: "UNKNOWN", error: "Unsupported format",
|
|
155
|
-
documentCode: nil, issuingState: nil, documentNumber: nil, lastName: nil,
|
|
156
|
-
firstName: nil, birthDate: nil, sex: nil, expirationDate: nil,
|
|
157
|
-
nationality: nil, optional1: nil, optional2: nil, rawLines: nil)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let allValid = validateChecksums(format: format, lines: lines)
|
|
161
|
-
return MRZResult(valid: allValid, format: format.rawValue, error: fields.error,
|
|
162
|
-
documentCode: fields.documentCode, issuingState: fields.issuingState,
|
|
163
|
-
documentNumber: fields.documentNumber, lastName: fields.lastName,
|
|
164
|
-
firstName: fields.firstName, birthDate: fields.birthDate, sex: fields.sex,
|
|
165
|
-
expirationDate: fields.expirationDate, nationality: fields.nationality,
|
|
166
|
-
optional1: fields.optional1, optional2: fields.optional2,
|
|
167
|
-
rawLines: allValid ? rawText.trimmingCharacters(in: .whitespacesAndNewlines) : nil)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private func detectMRZFormat(_ lines: [String]) -> MRZFormat {
|
|
171
|
-
if lines.count == 3 && lines.allSatisfy({ $0.count == 30 }) { return .TD1 }
|
|
172
|
-
if lines.count == 2 && lines.allSatisfy({ $0.count == 36 }) { return .TD2 }
|
|
173
|
-
if lines.count == 2 && lines.allSatisfy({ $0.count == 44 }) { return .TD3 }
|
|
174
|
-
return .UNKNOWN
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
private func str(_ s: String, from: Int, length: Int) -> String {
|
|
178
|
-
let start = s.index(s.startIndex, offsetBy: from)
|
|
179
|
-
let end = s.index(start, offsetBy: length)
|
|
180
|
-
return String(s[start..<end]).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private func raw(_ s: String, from: Int, length: Int) -> String {
|
|
184
|
-
let start = s.index(s.startIndex, offsetBy: from)
|
|
185
|
-
let end = s.index(start, offsetBy: length)
|
|
186
|
-
return String(s[start..<end])
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
private func parseTD1(_ lines: [String], format: MRZFormat) -> MRZResult {
|
|
190
|
-
let names = lines[2].components(separatedBy: "<<")
|
|
191
|
-
return MRZResult(valid: false, format: format.rawValue, error: nil,
|
|
192
|
-
documentCode: str(lines[0], from: 0, length: 2),
|
|
193
|
-
issuingState: str(lines[0], from: 2, length: 3),
|
|
194
|
-
documentNumber: str(lines[0], from: 5, length: 9),
|
|
195
|
-
lastName: names[0].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces),
|
|
196
|
-
firstName: names.count > 1 ? names[1].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces) : "",
|
|
197
|
-
birthDate: raw(lines[1], from: 0, length: 6),
|
|
198
|
-
sex: raw(lines[1], from: 7, length: 1),
|
|
199
|
-
expirationDate: raw(lines[1], from: 8, length: 6),
|
|
200
|
-
nationality: str(lines[1], from: 15, length: 3),
|
|
201
|
-
optional1: str(lines[0], from: 15, length: 15),
|
|
202
|
-
optional2: str(lines[1], from: 18, length: 11), rawLines: nil)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private func parseTD2(_ lines: [String], format: MRZFormat) -> MRZResult {
|
|
206
|
-
let names = String(lines[0].dropFirst(5)).components(separatedBy: "<<")
|
|
207
|
-
return MRZResult(valid: false, format: format.rawValue, error: nil,
|
|
208
|
-
documentCode: str(lines[0], from: 0, length: 2),
|
|
209
|
-
issuingState: str(lines[0], from: 2, length: 3),
|
|
210
|
-
documentNumber: str(lines[1], from: 0, length: 9),
|
|
211
|
-
lastName: names[0].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces),
|
|
212
|
-
firstName: names.count > 1 ? names[1].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces) : "",
|
|
213
|
-
birthDate: raw(lines[1], from: 13, length: 6),
|
|
214
|
-
sex: raw(lines[1], from: 20, length: 1),
|
|
215
|
-
expirationDate: raw(lines[1], from: 21, length: 6),
|
|
216
|
-
nationality: str(lines[1], from: 10, length: 3),
|
|
217
|
-
optional1: str(lines[1], from: 28, length: 7),
|
|
218
|
-
optional2: nil, rawLines: nil)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private func parseTD3(_ lines: [String], format: MRZFormat) -> MRZResult {
|
|
222
|
-
let names = String(lines[0].dropFirst(5)).components(separatedBy: "<<")
|
|
223
|
-
return MRZResult(valid: false, format: format.rawValue, error: nil,
|
|
224
|
-
documentCode: str(lines[0], from: 0, length: 2),
|
|
225
|
-
issuingState: str(lines[0], from: 2, length: 3),
|
|
226
|
-
documentNumber: str(lines[1], from: 0, length: 9),
|
|
227
|
-
lastName: names[0].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces),
|
|
228
|
-
firstName: names.count > 1 ? names[1].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces) : "",
|
|
229
|
-
birthDate: raw(lines[1], from: 13, length: 6),
|
|
230
|
-
sex: raw(lines[1], from: 20, length: 1),
|
|
231
|
-
expirationDate: raw(lines[1], from: 21, length: 6),
|
|
232
|
-
nationality: str(lines[1], from: 10, length: 3),
|
|
233
|
-
optional1: str(lines[1], from: 28, length: 14),
|
|
234
|
-
optional2: nil, rawLines: nil)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private func validateChecksums(format: MRZFormat, lines: [String]) -> Bool {
|
|
238
|
-
guard lines.count >= (format == .TD1 ? 3 : 2) else { return false }
|
|
239
|
-
do {
|
|
240
|
-
switch format {
|
|
241
|
-
case .TD1:
|
|
242
|
-
let l0 = lines[0], l1 = lines[1]
|
|
243
|
-
let docValid = try calculateChecksum(raw(l0, from: 5, length: 9)) == l0[l0.index(l0.startIndex, offsetBy: 14)]
|
|
244
|
-
let birthValid = try calculateChecksum(raw(l1, from: 0, length: 6)) == l1[l1.index(l1.startIndex, offsetBy: 6)]
|
|
245
|
-
let expiryValid = try calculateChecksum(raw(l1, from: 8, length: 6)) == l1[l1.index(l1.startIndex, offsetBy: 14)]
|
|
246
|
-
let composite = String(l0.dropFirst(5)) + String(l1.prefix(7)) + String(l1.dropFirst(8).prefix(7)) + String(l1.dropFirst(18).prefix(11))
|
|
247
|
-
let compValid = try calculateChecksum(composite) == l1[l1.index(l1.startIndex, offsetBy: 29)]
|
|
248
|
-
return docValid && birthValid && expiryValid && compValid
|
|
249
|
-
case .TD2:
|
|
250
|
-
let l = lines[1]
|
|
251
|
-
let docValid = try calculateChecksum(raw(l, from: 0, length: 9)) == l[l.index(l.startIndex, offsetBy: 9)]
|
|
252
|
-
let birthValid = try calculateChecksum(raw(l, from: 13, length: 6)) == l[l.index(l.startIndex, offsetBy: 19)]
|
|
253
|
-
let expiryValid = try calculateChecksum(raw(l, from: 21, length: 6)) == l[l.index(l.startIndex, offsetBy: 27)]
|
|
254
|
-
let composite = String(l.prefix(10)) + String(l.dropFirst(13).prefix(7)) + String(l.dropFirst(21).prefix(14))
|
|
255
|
-
let compValid = try calculateChecksum(composite) == l[l.index(l.startIndex, offsetBy: 35)]
|
|
256
|
-
return docValid && birthValid && expiryValid && compValid
|
|
257
|
-
case .TD3:
|
|
258
|
-
let l = lines[1]
|
|
259
|
-
let docValid = try calculateChecksum(raw(l, from: 0, length: 9)) == l[l.index(l.startIndex, offsetBy: 9)]
|
|
260
|
-
let birthValid = try calculateChecksum(raw(l, from: 13, length: 6)) == l[l.index(l.startIndex, offsetBy: 19)]
|
|
261
|
-
let expiryValid = try calculateChecksum(raw(l, from: 21, length: 6)) == l[l.index(l.startIndex, offsetBy: 27)]
|
|
262
|
-
let personalValid = try calculateChecksum(raw(l, from: 28, length: 14)) == l[l.index(l.startIndex, offsetBy: 42)]
|
|
263
|
-
let composite = String(l.prefix(10)) + String(l.dropFirst(13).prefix(7)) + String(l.dropFirst(21).prefix(22))
|
|
264
|
-
let compValid = try calculateChecksum(composite) == l[l.index(l.startIndex, offsetBy: 43)]
|
|
265
|
-
return docValid && birthValid && expiryValid && personalValid && compValid
|
|
266
|
-
case .UNKNOWN:
|
|
267
|
-
return false
|
|
268
|
-
}
|
|
269
|
-
} catch {
|
|
270
|
-
return false
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
private func calculateChecksum(_ data: String) throws -> Character {
|
|
275
|
-
let weights = [7, 3, 1]
|
|
276
|
-
var sum = 0
|
|
277
|
-
for (index, char) in data.enumerated() {
|
|
278
|
-
let value: Int
|
|
279
|
-
switch char {
|
|
280
|
-
case "0"..."9": value = Int(char.asciiValue! - Character("0").asciiValue!)
|
|
281
|
-
case "A"..."Z": value = Int(char.asciiValue! - Character("A").asciiValue!) + 10
|
|
282
|
-
default: value = 0
|
|
283
|
-
}
|
|
284
|
-
sum += value * weights[index % 3]
|
|
285
|
-
}
|
|
286
|
-
return Character(String(sum % 10))
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
func fixMRZ(_ rawText: String) -> String {
|
|
290
|
-
var fixed = rawText
|
|
291
|
-
.replacingOccurrences(of: " ", with: "")
|
|
292
|
-
.replacingOccurrences(of: "«", with: "")
|
|
293
|
-
if let regex = try? NSRegularExpression(pattern: "<K+|r+K+|<r+K+") {
|
|
294
|
-
fixed = regex.stringByReplacingMatches(in: fixed, range: NSRange(fixed.startIndex..., in: fixed), withTemplate: "")
|
|
295
|
-
}
|
|
296
|
-
if let regex = try? NSRegularExpression(pattern: "\\bI<TUR([A-Z0-9]{3})0([A-Z0-9]{6})\\b") {
|
|
297
|
-
fixed = regex.stringByReplacingMatches(in: fixed, range: NSRange(fixed.startIndex..., in: fixed), withTemplate: "I<TUR$1O$2")
|
|
298
|
-
}
|
|
299
|
-
let mrzCharRegex = try? NSRegularExpression(pattern: "^[A-Z0-9<]+$")
|
|
300
|
-
let allLines = fixed.components(separatedBy: "\n").map { $0.trimmingCharacters(in: .whitespaces) }
|
|
301
|
-
|
|
302
|
-
// Extract only lines that are exact MRZ lengths with valid MRZ characters
|
|
303
|
-
let mrzLines = allLines.filter { line in
|
|
304
|
-
(line.count == 30 || line.count == 36 || line.count == 44) &&
|
|
305
|
-
mrzCharRegex?.firstMatch(in: line, range: NSRange(line.startIndex..., in: line)) != nil
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Fall back to lines containing '<' if no exact-length lines found
|
|
309
|
-
let candidateLines = mrzLines.isEmpty ? allLines.filter { $0.contains("<") } : mrzLines
|
|
310
|
-
|
|
311
|
-
let targetLength: Int
|
|
312
|
-
if candidateLines.allSatisfy({ $0.count <= 30 }) { targetLength = 30 }
|
|
313
|
-
else if candidateLines.allSatisfy({ $0.count <= 36 }) { targetLength = 36 }
|
|
314
|
-
else if candidateLines.allSatisfy({ $0.count <= 44 }) { targetLength = 44 }
|
|
315
|
-
else { return rawText }
|
|
316
|
-
|
|
317
|
-
// Keep only lines that fit the target length and have valid MRZ characters
|
|
318
|
-
let filteredLines = candidateLines.filter { line in
|
|
319
|
-
line.count <= targetLength &&
|
|
320
|
-
mrzCharRegex?.firstMatch(in: line, range: NSRange(line.startIndex..., in: line)) != nil
|
|
321
|
-
}
|
|
322
|
-
if filteredLines.isEmpty { return rawText }
|
|
323
|
-
|
|
324
|
-
let paddedLines = filteredLines.map { line in
|
|
325
|
-
line.count < targetLength ? line.padding(toLength: targetLength, withPad: "<", startingAt: 0) : line
|
|
326
|
-
}
|
|
327
|
-
return applyOCRCorrections(paddedLines.joined(separator: "\n"))
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
private func applyOCRCorrections(_ mrzText: String) -> String {
|
|
331
|
-
var lines = mrzText.components(separatedBy: "\n")
|
|
332
|
-
if lines.isEmpty { return mrzText }
|
|
333
|
-
let format = detectMRZFormat(lines)
|
|
334
|
-
if format == .UNKNOWN { return mrzText }
|
|
335
|
-
switch format {
|
|
336
|
-
case .TD1:
|
|
337
|
-
lines[0] = correctLetterPositions(lines[0], ranges: [0...1, 2...4])
|
|
338
|
-
lines[0] = correctCheckDigitPositions(lines[0], positions: [14])
|
|
339
|
-
lines[1] = correctDigitPositions(lines[1], ranges: [0...5, 8...13])
|
|
340
|
-
lines[1] = correctCheckDigitPositions(lines[1], positions: [6, 14, 29])
|
|
341
|
-
lines[1] = correctLetterPositions(lines[1], ranges: [7...7, 15...17])
|
|
342
|
-
lines[2] = correctLetterPositions(lines[2], ranges: [0...29])
|
|
343
|
-
case .TD2:
|
|
344
|
-
lines[0] = correctLetterPositions(lines[0], ranges: [0...1, 2...4, 5...35])
|
|
345
|
-
lines[1] = correctDigitPositions(lines[1], ranges: [13...18, 21...26])
|
|
346
|
-
lines[1] = correctCheckDigitPositions(lines[1], positions: [9, 19, 27, 35])
|
|
347
|
-
lines[1] = correctLetterPositions(lines[1], ranges: [10...12, 20...20])
|
|
348
|
-
case .TD3:
|
|
349
|
-
lines[0] = correctLetterPositions(lines[0], ranges: [0...1, 2...4, 5...43])
|
|
350
|
-
lines[1] = correctDigitPositions(lines[1], ranges: [13...18, 21...26])
|
|
351
|
-
lines[1] = correctCheckDigitPositions(lines[1], positions: [9, 19, 27, 42])
|
|
352
|
-
lines[1] = correctLetterPositions(lines[1], ranges: [10...12, 20...20])
|
|
353
|
-
case .UNKNOWN: break
|
|
354
|
-
}
|
|
355
|
-
return lines.joined(separator: "\n")
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
private func correctDigitPositions(_ line: String, ranges: [ClosedRange<Int>]) -> String {
|
|
359
|
-
var c = Array(line)
|
|
360
|
-
ranges.forEach { r in r.forEach { i in
|
|
361
|
-
if i < c.count {
|
|
362
|
-
switch c[i] {
|
|
363
|
-
case "O", "o": c[i] = "0"
|
|
364
|
-
case "I", "l": c[i] = "1"
|
|
365
|
-
case "Z": c[i] = "2"
|
|
366
|
-
case "S": c[i] = "5"
|
|
367
|
-
case "G": c[i] = "6"
|
|
368
|
-
case "B": c[i] = "8"
|
|
369
|
-
case "D", "Q": c[i] = "0"
|
|
370
|
-
default: break
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}}
|
|
374
|
-
return String(c)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
private func correctLetterPositions(_ line: String, ranges: [ClosedRange<Int>]) -> String {
|
|
378
|
-
var c = Array(line)
|
|
379
|
-
ranges.forEach { r in r.forEach { i in
|
|
380
|
-
if i < c.count {
|
|
381
|
-
switch c[i] {
|
|
382
|
-
case "0": c[i] = "O"
|
|
383
|
-
case "1": c[i] = "I"
|
|
384
|
-
case "5": c[i] = "S"
|
|
385
|
-
case "8": c[i] = "B"
|
|
386
|
-
case "2": c[i] = "Z"
|
|
387
|
-
case "6": c[i] = "G"
|
|
388
|
-
default: break
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}}
|
|
392
|
-
return String(c)
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
private func correctCheckDigitPositions(_ line: String, positions: [Int]) -> String {
|
|
396
|
-
var c = Array(line)
|
|
397
|
-
positions.forEach { i in
|
|
398
|
-
if i < c.count {
|
|
399
|
-
switch c[i] {
|
|
400
|
-
case "O", "o": c[i] = "0"
|
|
401
|
-
case "I", "l": c[i] = "1"
|
|
402
|
-
case "Z": c[i] = "2"
|
|
403
|
-
case "S": c[i] = "5"
|
|
404
|
-
case "G": c[i] = "6"
|
|
405
|
-
case "B": c[i] = "8"
|
|
406
|
-
default: break
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return String(c)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
private func generateO0Permutations(_ mrzText: String, maxPermutations: Int = 100) -> [String] {
|
|
414
|
-
var permutations = Set<String>([mrzText])
|
|
415
|
-
let positions = Array(mrzText.enumerated().compactMap { (i, c) in
|
|
416
|
-
(c == "O" || c == "o" || c == "0") ? i : nil
|
|
417
|
-
}.prefix(7))
|
|
418
|
-
let limit = min(1 << positions.count, maxPermutations)
|
|
419
|
-
for i in 0..<limit {
|
|
420
|
-
var variant = Array(mrzText)
|
|
421
|
-
for (j, pos) in positions.enumerated() {
|
|
422
|
-
let useZero = (i & (1 << j)) != 0
|
|
423
|
-
if useZero && variant[pos] != "0" { variant[pos] = "0" }
|
|
424
|
-
else if !useZero && variant[pos] != "O" { variant[pos] = "O" }
|
|
425
|
-
}
|
|
426
|
-
permutations.insert(String(variant))
|
|
427
|
-
}
|
|
428
|
-
return Array(permutations)
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
private func generatePairPermutations(_ mrzText: String, letter: Character, digit: Character, maxPermutations: Int = 30) -> [String] {
|
|
432
|
-
var permutations = Set<String>([mrzText])
|
|
433
|
-
let positions = Array(mrzText.enumerated().compactMap { (i, c) in
|
|
434
|
-
(c == letter || c == Character(letter.lowercased()) || c == digit) ? i : nil
|
|
435
|
-
}.prefix(5))
|
|
436
|
-
let limit = min(1 << positions.count, maxPermutations)
|
|
437
|
-
for i in 0..<limit {
|
|
438
|
-
var variant = Array(mrzText)
|
|
439
|
-
for (j, pos) in positions.enumerated() {
|
|
440
|
-
let useDigit = (i & (1 << j)) != 0
|
|
441
|
-
if useDigit && variant[pos] != digit { variant[pos] = digit }
|
|
442
|
-
else if !useDigit && variant[pos] != letter { variant[pos] = letter }
|
|
443
|
-
}
|
|
444
|
-
permutations.insert(String(variant))
|
|
445
|
-
}
|
|
446
|
-
return Array(permutations)
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
private func generateI1Permutations(_ mrzText: String, maxPermutations: Int = 50) -> [String] {
|
|
450
|
-
var permutations = Set<String>([mrzText])
|
|
451
|
-
let positions = Array(mrzText.enumerated().compactMap { (i, c) in
|
|
452
|
-
(c == "I" || c == "i" || c == "l" || c == "1") ? i : nil
|
|
453
|
-
}.prefix(6))
|
|
454
|
-
let limit = min(1 << positions.count, maxPermutations)
|
|
455
|
-
for i in 0..<limit {
|
|
456
|
-
var variant = Array(mrzText)
|
|
457
|
-
for (j, pos) in positions.enumerated() {
|
|
458
|
-
let useOne = (i & (1 << j)) != 0
|
|
459
|
-
if useOne && variant[pos] != "1" { variant[pos] = "1" }
|
|
460
|
-
else if !useOne && (variant[pos] == "1" || variant[pos] == "l" || variant[pos] == "i") { variant[pos] = "I" }
|
|
461
|
-
}
|
|
462
|
-
permutations.insert(String(variant))
|
|
463
|
-
}
|
|
464
|
-
return Array(permutations)
|
|
465
|
-
}
|
|
466
|
-
}
|