@trustchex/react-native-sdk 1.361.0 → 1.362.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.
Files changed (53) hide show
  1. package/TrustchexSDK.podspec +3 -1
  2. package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +0 -13
  3. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraManager.kt +0 -8
  4. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +59 -39
  5. package/android/src/main/java/com/trustchex/reactnativesdk/opencv/OpenCVModule.kt +94 -13
  6. package/ios/Camera/TrustchexCameraManager.m +0 -2
  7. package/ios/Camera/TrustchexCameraManager.swift +0 -7
  8. package/ios/Camera/TrustchexCameraView.swift +16 -47
  9. package/ios/OpenCV/OpenCVHelper.h +17 -0
  10. package/ios/OpenCV/OpenCVHelper.mm +128 -0
  11. package/ios/OpenCV/OpenCVModule.h +6 -0
  12. package/ios/OpenCV/OpenCVModule.mm +141 -0
  13. package/ios/TrustchexSDK-Bridging-Header.h +8 -0
  14. package/lib/module/Screens/Debug/MRZTestScreen.js +175 -0
  15. package/lib/module/Shared/Components/DebugNavigationPanel.js +4 -0
  16. package/lib/module/Shared/Components/EIDScanner.js +0 -78
  17. package/lib/module/Shared/Components/IdentityDocumentCamera.js +188 -150
  18. package/lib/module/Shared/Components/QrCodeScannerCamera.js +0 -3
  19. package/lib/module/Shared/Libs/mrz.utils.js +265 -0
  20. package/lib/module/Trustchex.js +4 -0
  21. package/lib/module/index.js +1 -0
  22. package/lib/module/version.js +1 -1
  23. package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts +3 -0
  24. package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -0
  25. package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
  26. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  27. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts +3 -1
  28. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  29. package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
  30. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +0 -19
  31. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
  32. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +18 -1
  33. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  34. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  35. package/lib/typescript/src/index.d.ts +3 -0
  36. package/lib/typescript/src/index.d.ts.map +1 -1
  37. package/lib/typescript/src/version.d.ts +1 -1
  38. package/package.json +2 -1
  39. package/src/Screens/Debug/MRZTestScreen.tsx +209 -0
  40. package/src/Shared/Components/DebugNavigationPanel.tsx +5 -0
  41. package/src/Shared/Components/EIDScanner.tsx +0 -53
  42. package/src/Shared/Components/IdentityDocumentCamera.tsx +228 -146
  43. package/src/Shared/Components/QrCodeScannerCamera.tsx +0 -9
  44. package/src/Shared/Components/TrustchexCamera.tsx +0 -20
  45. package/src/Shared/Libs/mrz.utils.ts +289 -1
  46. package/src/Trustchex.tsx +5 -0
  47. package/src/index.tsx +3 -0
  48. package/src/version.ts +1 -1
  49. package/android/src/main/java/com/trustchex/reactnativesdk/mrz/MRZValidationModule.kt +0 -785
  50. package/android/src/main/java/com/trustchex/reactnativesdk/mrz/MRZValidator.kt +0 -419
  51. package/ios/MRZValidation.m +0 -39
  52. package/ios/MRZValidation.swift +0 -802
  53. package/ios/MRZValidator.swift +0 -466
@@ -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
- }