@trustchex/react-native-sdk 1.355.1 → 1.357.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/README.md +2 -9
- package/TrustchexSDK.podspec +5 -4
- package/android/build.gradle +6 -4
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +45 -25
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraManager.kt +168 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +871 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +245 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/mrz/MRZValidationModule.kt +785 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/mrz/MRZValidator.kt +419 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/opencv/OpenCVModule.kt +818 -0
- package/ios/Camera/TrustchexCameraManager.m +37 -0
- package/ios/Camera/TrustchexCameraManager.swift +125 -0
- package/ios/Camera/TrustchexCameraView.swift +1176 -0
- package/ios/MLKit/MLKitModule.m +23 -0
- package/ios/MLKit/MLKitModule.swift +250 -0
- package/ios/MRZValidation.m +39 -0
- package/ios/MRZValidation.swift +802 -0
- package/ios/MRZValidator.swift +466 -0
- package/ios/OpenCV/OpenCVModule.h +4 -0
- package/ios/OpenCV/OpenCVModule.mm +810 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +2 -3
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +1 -2
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +418 -193
- package/lib/module/Screens/Static/OTPVerificationScreen.js +11 -11
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +5 -1
- package/lib/module/Screens/Static/ResultScreen.js +25 -2
- package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +25 -7
- package/lib/module/Shared/Components/DebugNavigationPanel.js +234 -24
- package/lib/module/Shared/Components/EIDScanner.js +99 -9
- package/lib/module/Shared/Components/FaceCamera.js +170 -179
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +2151 -771
- package/lib/module/Shared/Components/QrCodeScannerCamera.js +109 -107
- package/lib/module/Shared/Components/TrustchexCamera.js +122 -0
- package/lib/module/Shared/EIDReader/tlv/tlv.helpers.js +91 -0
- package/lib/module/Shared/EIDReader/tlv/tlv.utils.js +2 -124
- package/lib/module/Shared/EIDReader/tlv/tlvInputStream.js +4 -4
- package/lib/module/Shared/EIDReader/tlv/tlvOutputState.js +4 -4
- package/lib/module/Shared/EIDReader/tlv/tlvOutputStream.js +4 -4
- package/lib/module/Shared/Libs/analytics.utils.js +2 -2
- package/lib/module/Shared/Libs/debug.utils.js +132 -0
- package/lib/module/Shared/Libs/deeplink.utils.js +6 -5
- package/lib/module/Shared/Libs/demo.utils.js +13 -3
- package/lib/module/Shared/Libs/mrz.utils.js +1 -175
- package/lib/module/Shared/Libs/native-device-info.utils.js +12 -6
- package/lib/module/Shared/Libs/tts.utils.js +40 -6
- package/lib/module/Shared/Services/AnalyticsService.js +9 -8
- package/lib/module/Shared/Types/mrzFields.js +1 -0
- package/lib/module/Translation/Resources/en.js +87 -88
- package/lib/module/Translation/Resources/tr.js +84 -85
- package/lib/module/Trustchex.js +9 -2
- package/lib/module/index.js +1 -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/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +2 -2
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts +18 -4
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts +3 -4
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts +2 -1
- package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +124 -0
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/tlv/tlv.helpers.d.ts +11 -0
- package/lib/typescript/src/Shared/EIDReader/tlv/tlv.helpers.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts +2 -39
- package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/debug.utils.d.ts +42 -0
- package/lib/typescript/src/Shared/Libs/debug.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/demo.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +0 -4
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/tts.utils.d.ts +4 -3
- package/lib/typescript/src/Shared/Libs/tts.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +2 -2
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/mrzFields.d.ts +11 -0
- package/lib/typescript/src/Shared/Types/mrzFields.d.ts.map +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts +4 -5
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +4 -5
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts +2 -0
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +4 -35
- package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +1 -1
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +7 -5
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +2 -3
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +498 -216
- package/src/Screens/Static/OTPVerificationScreen.tsx +37 -31
- package/src/Screens/Static/QrCodeScanningScreen.tsx +8 -1
- package/src/Screens/Static/ResultScreen.tsx +136 -88
- package/src/Screens/Static/VerificationSessionCheckScreen.tsx +46 -13
- package/src/Shared/Components/DebugNavigationPanel.tsx +290 -34
- package/src/Shared/Components/EIDScanner.tsx +94 -16
- package/src/Shared/Components/FaceCamera.tsx +236 -203
- package/src/Shared/Components/IdentityDocumentCamera.tsx +3073 -1030
- package/src/Shared/Components/QrCodeScannerCamera.tsx +133 -127
- package/src/Shared/Components/TrustchexCamera.tsx +289 -0
- package/src/Shared/Config/camera-enhancement.config.ts +2 -2
- package/src/Shared/EIDReader/tlv/tlv.helpers.ts +96 -0
- package/src/Shared/EIDReader/tlv/tlv.utils.ts +2 -125
- package/src/Shared/EIDReader/tlv/tlvInputStream.ts +4 -4
- package/src/Shared/EIDReader/tlv/tlvOutputState.ts +4 -4
- package/src/Shared/EIDReader/tlv/tlvOutputStream.ts +4 -4
- package/src/Shared/Libs/analytics.utils.ts +48 -20
- package/src/Shared/Libs/debug.utils.ts +149 -0
- package/src/Shared/Libs/deeplink.utils.ts +7 -5
- package/src/Shared/Libs/demo.utils.ts +4 -0
- package/src/Shared/Libs/http-client.ts +12 -8
- package/src/Shared/Libs/mrz.utils.ts +1 -163
- package/src/Shared/Libs/native-device-info.utils.ts +12 -6
- package/src/Shared/Libs/tts.utils.ts +48 -6
- package/src/Shared/Services/AnalyticsService.ts +69 -24
- package/src/Shared/Types/identificationInfo.ts +2 -2
- package/src/Shared/Types/mrzFields.ts +29 -0
- package/src/Translation/Resources/en.ts +90 -100
- package/src/Translation/Resources/tr.ts +89 -97
- package/src/Translation/index.ts +1 -1
- package/src/Trustchex.tsx +21 -4
- package/src/index.tsx +14 -0
- package/src/version.ts +1 -1
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/barcodescanner/BarcodeScannerFrameProcessorPlugin.kt +0 -301
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/BitmapUtils.kt +0 -205
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/CropperPlugin.kt +0 -72
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/FrameMetadata.kt +0 -4
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/facedetector/FaceDetectorFrameProcessorPlugin.kt +0 -303
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/textrecognition/TextRecognitionFrameProcessorPlugin.kt +0 -115
- package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin-Bridging-Header.h +0 -9
- package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin.mm +0 -22
- package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin.swift +0 -188
- package/ios/VisionCameraPlugins/Cropper/Cropper-Bridging-Header.h +0 -13
- package/ios/VisionCameraPlugins/Cropper/Cropper.h +0 -20
- package/ios/VisionCameraPlugins/Cropper/Cropper.mm +0 -22
- package/ios/VisionCameraPlugins/Cropper/Cropper.swift +0 -145
- package/ios/VisionCameraPlugins/Cropper/CropperUtils.swift +0 -49
- package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin-Bridging-Header.h +0 -4
- package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin.mm +0 -22
- package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin.swift +0 -320
- package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin-Bridging-Header.h +0 -4
- package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin.mm +0 -27
- package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin.swift +0 -144
- package/lib/module/Shared/Libs/camera.utils.js +0 -308
- package/lib/module/Shared/Libs/frame-enhancement.utils.js +0 -133
- package/lib/module/Shared/Libs/opencv.utils.js +0 -21
- package/lib/module/Shared/Libs/worklet.utils.js +0 -68
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.js +0 -46
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.js +0 -35
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/index.js +0 -19
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.js +0 -26
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/types.js +0 -3
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.js +0 -197
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.js +0 -101
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.js +0 -60
- package/lib/module/Shared/VisionCameraPlugins/Cropper/index.js +0 -47
- package/lib/module/Shared/VisionCameraPlugins/FaceDetector/Camera.js +0 -42
- package/lib/module/Shared/VisionCameraPlugins/FaceDetector/detectFaces.js +0 -35
- package/lib/module/Shared/VisionCameraPlugins/FaceDetector/index.js +0 -4
- package/lib/module/Shared/VisionCameraPlugins/FaceDetector/types.js +0 -3
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/Camera.js +0 -56
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.js +0 -20
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.js +0 -9
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/index.js +0 -6
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/scanText.js +0 -20
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/translateText.js +0 -19
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/types.js +0 -3
- package/lib/typescript/src/Shared/Libs/camera.utils.d.ts +0 -87
- package/lib/typescript/src/Shared/Libs/camera.utils.d.ts.map +0 -1
- package/lib/typescript/src/Shared/Libs/frame-enhancement.utils.d.ts +0 -25
- package/lib/typescript/src/Shared/Libs/frame-enhancement.utils.d.ts.map +0 -1
- package/lib/typescript/src/Shared/Libs/opencv.utils.d.ts +0 -3
- package/lib/typescript/src/Shared/Libs/opencv.utils.d.ts.map +0 -1
- package/lib/typescript/src/Shared/Libs/worklet.utils.d.ts +0 -9
- package/lib/typescript/src/Shared/Libs/worklet.utils.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.d.ts +0 -13
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts +0 -6
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/index.d.ts +0 -12
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/index.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/types.d.ts +0 -52
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/types.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.d.ts +0 -62
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.d.ts +0 -34
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.d.ts +0 -32
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/Cropper/index.d.ts +0 -23
- package/lib/typescript/src/Shared/VisionCameraPlugins/Cropper/index.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/Camera.d.ts +0 -9
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/Camera.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/index.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/index.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/types.d.ts +0 -79
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/types.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/Camera.d.ts +0 -6
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/Camera.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/index.d.ts +0 -5
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/index.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/scanText.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/scanText.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/translateText.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/translateText.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/types.d.ts +0 -67
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/types.d.ts.map +0 -1
- package/src/Shared/Libs/camera.utils.ts +0 -345
- package/src/Shared/Libs/frame-enhancement.utils.ts +0 -217
- package/src/Shared/Libs/opencv.utils.ts +0 -40
- package/src/Shared/Libs/worklet.utils.ts +0 -72
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.ts +0 -79
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.ts +0 -46
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/index.ts +0 -60
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.ts +0 -32
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/types.ts +0 -82
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.ts +0 -195
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.ts +0 -135
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.ts +0 -84
- package/src/Shared/VisionCameraPlugins/Cropper/index.ts +0 -78
- package/src/Shared/VisionCameraPlugins/FaceDetector/Camera.tsx +0 -63
- package/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.ts +0 -44
- package/src/Shared/VisionCameraPlugins/FaceDetector/index.ts +0 -3
- package/src/Shared/VisionCameraPlugins/FaceDetector/types.ts +0 -99
- package/src/Shared/VisionCameraPlugins/TextRecognition/Camera.tsx +0 -76
- package/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.ts +0 -18
- package/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.ts +0 -7
- package/src/Shared/VisionCameraPlugins/TextRecognition/index.ts +0 -7
- package/src/Shared/VisionCameraPlugins/TextRecognition/scanText.ts +0 -27
- package/src/Shared/VisionCameraPlugins/TextRecognition/translateText.ts +0 -21
- package/src/Shared/VisionCameraPlugins/TextRecognition/types.ts +0 -141
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Native MRZ Validation Module for iOS
|
|
6
|
+
*
|
|
7
|
+
* Performs high-performance MRZ detection and validation with checksum verification
|
|
8
|
+
* according to ICAO 9303 standards. This native implementation is significantly faster
|
|
9
|
+
* than JavaScript parsing, especially for real-time camera frame processing.
|
|
10
|
+
*
|
|
11
|
+
* ICAO 9303 Compliance:
|
|
12
|
+
* - TD1: Identity cards (3 lines × 30 characters)
|
|
13
|
+
* - TD2: Identity cards (2 lines × 36 characters)
|
|
14
|
+
* - TD3: Passports (2 lines × 44 characters)
|
|
15
|
+
* - Check digit algorithm: 7-3-1 weighted modulo 10
|
|
16
|
+
* - Character values: 0-9 = 0-9, A-Z = 10-35, < = 0
|
|
17
|
+
* - Validates: document number, birth date, expiry date, optional/personal number, composite checksums
|
|
18
|
+
*
|
|
19
|
+
* Key features:
|
|
20
|
+
* - Native checksum calculation (7-3-1 algorithm)
|
|
21
|
+
* - MRZ format detection (TD1, TD2, TD3)
|
|
22
|
+
* - Field extraction and validation
|
|
23
|
+
* - OCR correction with position-aware character mapping
|
|
24
|
+
* - Composite checksum brute-force correction
|
|
25
|
+
*
|
|
26
|
+
* @see https://www.icao.int/publications/Documents/9303_p3_cons_en.pdf
|
|
27
|
+
*/
|
|
28
|
+
@objc(MRZValidation)
|
|
29
|
+
class MRZValidation: NSObject {
|
|
30
|
+
|
|
31
|
+
enum MRZFormat: String {
|
|
32
|
+
case TD1 = "TD1" // ID card, 3 lines of 30 chars
|
|
33
|
+
case TD2 = "TD2" // ID card, 2 lines of 36 chars
|
|
34
|
+
case TD3 = "TD3" // Passport, 2 lines of 44 chars
|
|
35
|
+
case UNKNOWN = "UNKNOWN"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Primary MRZ validation with OCR corrections and composite checksum brute-force
|
|
40
|
+
* This is the main entry point for fast native processing
|
|
41
|
+
*/
|
|
42
|
+
@objc
|
|
43
|
+
func validateMRZWithCorrections(_ ocrText: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
44
|
+
do {
|
|
45
|
+
// Fix and clean raw OCR text
|
|
46
|
+
let fixedText = fixMRZ(ocrText)
|
|
47
|
+
var lines = fixedText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
48
|
+
.components(separatedBy: "\n")
|
|
49
|
+
.map { $0.trimmingCharacters(in: .whitespaces) }
|
|
50
|
+
|
|
51
|
+
if lines.isEmpty || fixedText.count < 60 {
|
|
52
|
+
resolve(createInvalidResult(error: "MRZ text too short"))
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let format = detectMRZFormat(lines)
|
|
57
|
+
guard format != .UNKNOWN else {
|
|
58
|
+
resolve(createInvalidResult(error: "Unknown MRZ format"))
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Phase 1: Try parsing with position-aware OCR corrections
|
|
63
|
+
print("[MRZ Debug] Phase 1: Attempting position-aware OCR corrections")
|
|
64
|
+
var result = try parseMRZ(fixedText)
|
|
65
|
+
if let valid = result["valid"] as? Bool, valid == true {
|
|
66
|
+
print("[MRZ Debug] Phase 1: Valid MRZ found via position-aware corrections")
|
|
67
|
+
resolve(result)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Phase 2: Try composite checksum brute-force if only composite is invalid
|
|
72
|
+
if let checksums = result["checksums"] as? [String: Any],
|
|
73
|
+
let invalidFields = checksums["invalidFields"] as? [String],
|
|
74
|
+
invalidFields.count == 1,
|
|
75
|
+
invalidFields[0] == "compositeCheckDigit" {
|
|
76
|
+
|
|
77
|
+
print("[MRZ Debug] Phase 2: Attempting composite checksum brute-force")
|
|
78
|
+
|
|
79
|
+
let compositePos: (line: Int, pos: Int)?
|
|
80
|
+
switch format {
|
|
81
|
+
case .TD1: compositePos = (1, 29)
|
|
82
|
+
case .TD2: compositePos = (1, 35)
|
|
83
|
+
case .TD3: compositePos = (1, 43)
|
|
84
|
+
case .UNKNOWN: compositePos = nil
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if let pos = compositePos {
|
|
88
|
+
for digit in 0...9 {
|
|
89
|
+
var testLines = lines
|
|
90
|
+
let targetLine = testLines[pos.line]
|
|
91
|
+
let startIdx = targetLine.index(targetLine.startIndex, offsetBy: pos.pos)
|
|
92
|
+
let endIdx = targetLine.index(startIdx, offsetBy: 1)
|
|
93
|
+
var chars = Array(targetLine)
|
|
94
|
+
chars[pos.pos] = Character(String(digit))
|
|
95
|
+
testLines[pos.line] = String(chars)
|
|
96
|
+
|
|
97
|
+
let testMRZ = testLines.joined(separator: "\n")
|
|
98
|
+
let testResult = try parseMRZ(testMRZ)
|
|
99
|
+
|
|
100
|
+
if let valid = testResult["valid"] as? Bool, valid == true {
|
|
101
|
+
print("[MRZ Debug] Phase 2: Valid MRZ found via composite digit \(digit)")
|
|
102
|
+
resolve(testResult)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Phase 3: Try O/0 permutations
|
|
110
|
+
print("[MRZ Debug] Phase 3: Attempting O/0 permutations")
|
|
111
|
+
let o0Permutations = generateO0Permutations(fixedText, maxPermutations: 100)
|
|
112
|
+
for (index, permutation) in o0Permutations.enumerated() {
|
|
113
|
+
let testResult = try parseMRZ(permutation)
|
|
114
|
+
if let valid = testResult["valid"] as? Bool, valid == true {
|
|
115
|
+
print("[MRZ Debug] Phase 3: Valid MRZ found via O/0 permutation \(index + 1)/\(o0Permutations.count)")
|
|
116
|
+
resolve(testResult)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Phase 4: Try I/1 permutations as last resort
|
|
122
|
+
print("[MRZ Debug] Phase 4: Attempting I/1 permutations")
|
|
123
|
+
let i1Permutations = generateI1Permutations(fixedText, maxPermutations: 50)
|
|
124
|
+
for (index, permutation) in i1Permutations.enumerated() {
|
|
125
|
+
let testResult = try parseMRZ(permutation)
|
|
126
|
+
if let valid = testResult["valid"] as? Bool, valid == true {
|
|
127
|
+
print("[MRZ Debug] Phase 4: Valid MRZ found via I/1 permutation \(index + 1)/\(i1Permutations.count)")
|
|
128
|
+
resolve(testResult)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Return invalid result if all attempts failed
|
|
134
|
+
print("[MRZ Debug] All correction phases failed")
|
|
135
|
+
resolve(result)
|
|
136
|
+
} catch {
|
|
137
|
+
reject("MRZ_VALIDATION_ERROR", "Failed to validate MRZ: \(error.localizedDescription)", error)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validates MRZ text and returns parse result with checksum validation
|
|
143
|
+
*/
|
|
144
|
+
@objc
|
|
145
|
+
func validateMRZ(_ mrzText: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
146
|
+
do {
|
|
147
|
+
let result = try parseMRZ(mrzText)
|
|
148
|
+
resolve(result)
|
|
149
|
+
} catch {
|
|
150
|
+
reject("MRZ_VALIDATION_ERROR", "Failed to validate MRZ: \(error.localizedDescription)", error)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Create invalid result response
|
|
156
|
+
*/
|
|
157
|
+
private func createInvalidResult(error: String) -> [String: Any] {
|
|
158
|
+
return [
|
|
159
|
+
"valid": false,
|
|
160
|
+
"error": error,
|
|
161
|
+
"format": "UNKNOWN"
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Calculates checksum for MRZ field using 7-3-1 algorithm
|
|
167
|
+
*/
|
|
168
|
+
@objc
|
|
169
|
+
func calculateChecksum(_ data: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
170
|
+
do {
|
|
171
|
+
let checksum = try calculateMRZChecksum(data)
|
|
172
|
+
resolve(String(checksum))
|
|
173
|
+
} catch {
|
|
174
|
+
reject("CHECKSUM_ERROR", "Failed to calculate checksum: \(error.localizedDescription)", error)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Fixes common OCR errors in MRZ text
|
|
180
|
+
*/
|
|
181
|
+
@objc
|
|
182
|
+
func fixMRZText(_ mrzText: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
183
|
+
do {
|
|
184
|
+
let fixed = fixMRZ(mrzText)
|
|
185
|
+
resolve(fixed)
|
|
186
|
+
} catch {
|
|
187
|
+
reject("MRZ_FIX_ERROR", "Failed to fix MRZ text: \(error.localizedDescription)", error)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// MARK: - Private Methods
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Main MRZ parser - validates format and checksums
|
|
195
|
+
*/
|
|
196
|
+
private func parseMRZ(_ rawText: String) throws -> [String: Any] {
|
|
197
|
+
var result: [String: Any] = [:]
|
|
198
|
+
let lines = rawText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
199
|
+
.components(separatedBy: "\n")
|
|
200
|
+
.map { $0.trimmingCharacters(in: .whitespaces) }
|
|
201
|
+
|
|
202
|
+
// Detect format
|
|
203
|
+
let format = detectMRZFormat(lines)
|
|
204
|
+
result["format"] = format.rawValue
|
|
205
|
+
|
|
206
|
+
guard format != .UNKNOWN else {
|
|
207
|
+
result["valid"] = false
|
|
208
|
+
result["error"] = "Unknown MRZ format"
|
|
209
|
+
return result
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Parse based on format
|
|
213
|
+
let fields: [String: Any]
|
|
214
|
+
switch format {
|
|
215
|
+
case .TD1:
|
|
216
|
+
fields = try parseTD1(lines)
|
|
217
|
+
case .TD2:
|
|
218
|
+
fields = try parseTD2(lines)
|
|
219
|
+
case .TD3:
|
|
220
|
+
fields = try parseTD3(lines)
|
|
221
|
+
case .UNKNOWN:
|
|
222
|
+
result["valid"] = false
|
|
223
|
+
result["error"] = "Unsupported format"
|
|
224
|
+
return result
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
result["fields"] = fields
|
|
228
|
+
|
|
229
|
+
// Validate all checksums
|
|
230
|
+
let checksumValidation = try validateChecksums(format: format, lines: lines)
|
|
231
|
+
result["valid"] = checksumValidation["allValid"] as? Bool ?? false
|
|
232
|
+
result["checksums"] = checksumValidation
|
|
233
|
+
|
|
234
|
+
return result
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Detects MRZ format from line structure
|
|
239
|
+
*/
|
|
240
|
+
private func detectMRZFormat(_ lines: [String]) -> MRZFormat {
|
|
241
|
+
if lines.count == 3 && lines.allSatisfy({ $0.count == 30 }) {
|
|
242
|
+
return .TD1
|
|
243
|
+
} else if lines.count == 2 && lines.allSatisfy({ $0.count == 36 }) {
|
|
244
|
+
return .TD2
|
|
245
|
+
} else if lines.count == 2 && lines.allSatisfy({ $0.count == 44 }) {
|
|
246
|
+
return .TD3
|
|
247
|
+
}
|
|
248
|
+
return .UNKNOWN
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Parse TD1 format (ID card, 3 lines of 30 chars)
|
|
253
|
+
*/
|
|
254
|
+
private func parseTD1(_ lines: [String]) throws -> [String: Any] {
|
|
255
|
+
guard lines.count >= 3 else {
|
|
256
|
+
throw NSError(domain: "MRZValidation", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid TD1 format"])
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var fields: [String: Any] = [:]
|
|
260
|
+
|
|
261
|
+
// Line 1
|
|
262
|
+
let line1 = lines[0]
|
|
263
|
+
fields["documentCode"] = String(line1.prefix(2)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
264
|
+
fields["issuingState"] = String(line1.dropFirst(2).prefix(3)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
265
|
+
fields["documentNumber"] = String(line1.dropFirst(5).prefix(9)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
266
|
+
fields["documentNumberCheckDigit"] = String(line1.dropFirst(14).prefix(1))
|
|
267
|
+
fields["optional1"] = String(line1.dropFirst(15)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
268
|
+
|
|
269
|
+
// Line 2
|
|
270
|
+
let line2 = lines[1]
|
|
271
|
+
fields["birthDate"] = String(line2.prefix(6))
|
|
272
|
+
fields["birthDateCheckDigit"] = String(line2.dropFirst(6).prefix(1))
|
|
273
|
+
fields["sex"] = String(line2.dropFirst(7).prefix(1))
|
|
274
|
+
fields["expirationDate"] = String(line2.dropFirst(8).prefix(6))
|
|
275
|
+
fields["expirationDateCheckDigit"] = String(line2.dropFirst(14).prefix(1))
|
|
276
|
+
fields["nationality"] = String(line2.dropFirst(15).prefix(3)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
277
|
+
fields["optional2"] = String(line2.dropFirst(18).prefix(11)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
278
|
+
fields["compositeCheckDigit"] = String(line2.dropFirst(29).prefix(1))
|
|
279
|
+
|
|
280
|
+
// Line 3 - Names
|
|
281
|
+
let line3 = lines[2]
|
|
282
|
+
let names = line3.components(separatedBy: "<<")
|
|
283
|
+
fields["lastName"] = names[0].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
284
|
+
fields["firstName"] = names.count > 1 ? names[1].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces) : ""
|
|
285
|
+
|
|
286
|
+
return fields
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Parse TD2 format (ID card, 2 lines of 36 chars)
|
|
291
|
+
*/
|
|
292
|
+
private func parseTD2(_ lines: [String]) throws -> [String: Any] {
|
|
293
|
+
guard lines.count >= 2 else {
|
|
294
|
+
throw NSError(domain: "MRZValidation", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid TD2 format"])
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
var fields: [String: Any] = [:]
|
|
298
|
+
|
|
299
|
+
// Line 1
|
|
300
|
+
let line1 = lines[0]
|
|
301
|
+
fields["documentCode"] = String(line1.prefix(2)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
302
|
+
fields["issuingState"] = String(line1.dropFirst(2).prefix(3)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
303
|
+
let names = String(line1.dropFirst(5)).components(separatedBy: "<<")
|
|
304
|
+
fields["lastName"] = names[0].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
305
|
+
fields["firstName"] = names.count > 1 ? names[1].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces) : ""
|
|
306
|
+
|
|
307
|
+
// Line 2
|
|
308
|
+
let line2 = lines[1]
|
|
309
|
+
fields["documentNumber"] = String(line2.prefix(9)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
310
|
+
fields["documentNumberCheckDigit"] = String(line2.dropFirst(9).prefix(1))
|
|
311
|
+
fields["nationality"] = String(line2.dropFirst(10).prefix(3)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
312
|
+
fields["birthDate"] = String(line2.dropFirst(13).prefix(6))
|
|
313
|
+
fields["birthDateCheckDigit"] = String(line2.dropFirst(19).prefix(1))
|
|
314
|
+
fields["sex"] = String(line2.dropFirst(20).prefix(1))
|
|
315
|
+
fields["expirationDate"] = String(line2.dropFirst(21).prefix(6))
|
|
316
|
+
fields["expirationDateCheckDigit"] = String(line2.dropFirst(27).prefix(1))
|
|
317
|
+
fields["optional1"] = String(line2.dropFirst(28).prefix(7)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
318
|
+
fields["compositeCheckDigit"] = String(line2.dropFirst(35).prefix(1))
|
|
319
|
+
|
|
320
|
+
return fields
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Parse TD3 format (Passport, 2 lines of 44 chars)
|
|
325
|
+
*/
|
|
326
|
+
private func parseTD3(_ lines: [String]) throws -> [String: Any] {
|
|
327
|
+
guard lines.count >= 2 else {
|
|
328
|
+
throw NSError(domain: "MRZValidation", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid TD3 format"])
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
var fields: [String: Any] = [:]
|
|
332
|
+
|
|
333
|
+
// Line 1
|
|
334
|
+
let line1 = lines[0]
|
|
335
|
+
fields["documentCode"] = String(line1.prefix(2)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
336
|
+
fields["issuingState"] = String(line1.dropFirst(2).prefix(3)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
337
|
+
let names = String(line1.dropFirst(5)).components(separatedBy: "<<")
|
|
338
|
+
fields["lastName"] = names[0].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
339
|
+
fields["firstName"] = names.count > 1 ? names[1].replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces) : ""
|
|
340
|
+
|
|
341
|
+
// Line 2
|
|
342
|
+
let line2 = lines[1]
|
|
343
|
+
fields["documentNumber"] = String(line2.prefix(9)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
344
|
+
fields["documentNumberCheckDigit"] = String(line2.dropFirst(9).prefix(1))
|
|
345
|
+
fields["nationality"] = String(line2.dropFirst(10).prefix(3)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
346
|
+
fields["birthDate"] = String(line2.dropFirst(13).prefix(6))
|
|
347
|
+
fields["birthDateCheckDigit"] = String(line2.dropFirst(19).prefix(1))
|
|
348
|
+
fields["sex"] = String(line2.dropFirst(20).prefix(1))
|
|
349
|
+
fields["expirationDate"] = String(line2.dropFirst(21).prefix(6))
|
|
350
|
+
fields["expirationDateCheckDigit"] = String(line2.dropFirst(27).prefix(1))
|
|
351
|
+
fields["optional1"] = String(line2.dropFirst(28).prefix(14)).replacingOccurrences(of: "<", with: " ").trimmingCharacters(in: .whitespaces)
|
|
352
|
+
fields["optional1CheckDigit"] = String(line2.dropFirst(42).prefix(1))
|
|
353
|
+
fields["compositeCheckDigit"] = String(line2.dropFirst(43).prefix(1))
|
|
354
|
+
|
|
355
|
+
return fields
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Validates all checksums in MRZ according to ICAO 9303 Part 3
|
|
360
|
+
*
|
|
361
|
+
* ICAO 9303 Check Digit Calculation:
|
|
362
|
+
* - Weights: 7, 3, 1 (cycling)
|
|
363
|
+
* - Character values: 0-9 = 0-9, A-Z = 10-35, < (filler) = 0
|
|
364
|
+
* - Result: (sum of weighted values) mod 10
|
|
365
|
+
*
|
|
366
|
+
* @see https://www2023.icao.int/publications/Documents/9303_p3_cons_en.pdf Section 4.9
|
|
367
|
+
*/
|
|
368
|
+
private func validateChecksums(format: MRZFormat, lines: [String]) throws -> [String: Any] {
|
|
369
|
+
var result: [String: Any] = [:]
|
|
370
|
+
var invalidFields: [String] = []
|
|
371
|
+
var allValid = true
|
|
372
|
+
|
|
373
|
+
switch format {
|
|
374
|
+
case .TD1:
|
|
375
|
+
// TD1 Format (ID card, 3 lines × 30 characters)
|
|
376
|
+
// Line 1: Document code(2) + Issuing state(3) + Document number(9) + Check(1) + Optional(15)
|
|
377
|
+
// Line 2: Birth date(6) + Check(1) + Sex(1) + Expiry(6) + Check(1) + Nationality(3) + Optional(11) + Composite check(1)
|
|
378
|
+
// Line 3: Names
|
|
379
|
+
|
|
380
|
+
let line1 = lines[0]
|
|
381
|
+
let line2 = lines[1]
|
|
382
|
+
|
|
383
|
+
// Document number checksum - ICAO 9303 Part 3, Section 4.9
|
|
384
|
+
let docNum = String(line1.dropFirst(5).prefix(9)) // Positions 5-13
|
|
385
|
+
let docNumCheck = line1[line1.index(line1.startIndex, offsetBy: 14)] // Position 14
|
|
386
|
+
let docNumValid = try calculateMRZChecksum(docNum) == docNumCheck
|
|
387
|
+
result["documentNumberCheckDigit"] = docNumValid
|
|
388
|
+
if !docNumValid {
|
|
389
|
+
invalidFields.append("documentNumberCheckDigit")
|
|
390
|
+
allValid = false
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Birth date checksum
|
|
394
|
+
let birthDate = String(line2.prefix(6)) // Positions 0-5
|
|
395
|
+
let birthDateCheck = line2[line2.index(line2.startIndex, offsetBy: 6)] // Position 6
|
|
396
|
+
let birthDateValid = try calculateMRZChecksum(birthDate) == birthDateCheck
|
|
397
|
+
result["birthDateCheckDigit"] = birthDateValid
|
|
398
|
+
if !birthDateValid {
|
|
399
|
+
invalidFields.append("birthDateCheckDigit")
|
|
400
|
+
allValid = false
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Expiry date checksum
|
|
404
|
+
let expiryDate = String(line2.dropFirst(8).prefix(6)) // Positions 8-13
|
|
405
|
+
let expiryDateCheck = line2[line2.index(line2.startIndex, offsetBy: 14)] // Position 14
|
|
406
|
+
let expiryDateValid = try calculateMRZChecksum(expiryDate) == expiryDateCheck
|
|
407
|
+
result["expirationDateCheckDigit"] = expiryDateValid
|
|
408
|
+
if !expiryDateValid {
|
|
409
|
+
invalidFields.append("expirationDateCheckDigit")
|
|
410
|
+
allValid = false
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Composite checksum - ICAO 9303 Part 3, Section 4.9
|
|
414
|
+
// TD1 Composite = Line1(5-29) + Line2(0-6) + Line2(8-14) + Line2(18-28)
|
|
415
|
+
// This includes: doc number(9) + check(1) + optional(15) + birth(6) + check(1) + expiry(6) + check(1) + optional(11)
|
|
416
|
+
// Total: 50 characters
|
|
417
|
+
let composite = String(line1.dropFirst(5)) + String(line2.prefix(7)) + String(line2.dropFirst(8).prefix(7)) + String(line2.dropFirst(18).prefix(11))
|
|
418
|
+
let compositeCheck = line2[line2.index(line2.startIndex, offsetBy: 29)] // Position 29
|
|
419
|
+
let compositeValid = try calculateMRZChecksum(composite) == compositeCheck
|
|
420
|
+
result["compositeCheckDigit"] = compositeValid
|
|
421
|
+
if !compositeValid {
|
|
422
|
+
invalidFields.append("compositeCheckDigit")
|
|
423
|
+
allValid = false
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
case .TD2, .TD3:
|
|
427
|
+
// TD2 Format (ID card, 2 lines × 36 characters)
|
|
428
|
+
// Line 1: Document code(2) + Issuing state(3) + Names(31)
|
|
429
|
+
// Line 2: Doc number(9) + Check(1) + Nationality(3) + Birth(6) + Check(1) + Sex(1) + Expiry(6) + Check(1) + Optional(7) + Composite check(1)
|
|
430
|
+
//
|
|
431
|
+
// TD3 Format (Passport, 2 lines × 44 characters)
|
|
432
|
+
// Line 1: Document code(2) + Issuing state(3) + Names(39)
|
|
433
|
+
// Line 2: Doc number(9) + Check(1) + Nationality(3) + Birth(6) + Check(1) + Sex(1) + Expiry(6) + Check(1) + Personal number(14) + Check(1) + Composite check(1)
|
|
434
|
+
|
|
435
|
+
let line = lines[1]
|
|
436
|
+
|
|
437
|
+
// Document number checksum - ICAO 9303 Part 3, Section 4.9
|
|
438
|
+
let docNum = String(line.prefix(9)) // Positions 0-8
|
|
439
|
+
let docNumCheck = line[line.index(line.startIndex, offsetBy: 9)] // Position 9
|
|
440
|
+
let docNumValid = try calculateMRZChecksum(docNum) == docNumCheck
|
|
441
|
+
result["documentNumberCheckDigit"] = docNumValid
|
|
442
|
+
if !docNumValid {
|
|
443
|
+
invalidFields.append("documentNumberCheckDigit")
|
|
444
|
+
allValid = false
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Birth date checksum
|
|
448
|
+
let birthDate = String(line.dropFirst(13).prefix(6)) // Positions 13-18
|
|
449
|
+
let birthDateCheck = line[line.index(line.startIndex, offsetBy: 19)] // Position 19
|
|
450
|
+
let birthDateValid = try calculateMRZChecksum(birthDate) == birthDateCheck
|
|
451
|
+
result["birthDateCheckDigit"] = birthDateValid
|
|
452
|
+
if !birthDateValid {
|
|
453
|
+
invalidFields.append("birthDateCheckDigit")
|
|
454
|
+
allValid = false
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Expiry date checksum
|
|
458
|
+
let expiryDate = String(line.dropFirst(21).prefix(6)) // Positions 21-26
|
|
459
|
+
let expiryDateCheck = line[line.index(line.startIndex, offsetBy: 27)] // Position 27
|
|
460
|
+
let expiryDateValid = try calculateMRZChecksum(expiryDate) == expiryDateCheck
|
|
461
|
+
result["expirationDateCheckDigit"] = expiryDateValid
|
|
462
|
+
if !expiryDateValid {
|
|
463
|
+
invalidFields.append("expirationDateCheckDigit")
|
|
464
|
+
allValid = false
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// TD3 has personal number checksum (optional field)
|
|
468
|
+
if format == .TD3 {
|
|
469
|
+
let personalNum = String(line.dropFirst(28).prefix(14)) // Positions 28-41
|
|
470
|
+
let personalNumCheck = line[line.index(line.startIndex, offsetBy: 42)] // Position 42
|
|
471
|
+
let personalNumValid = try calculateMRZChecksum(personalNum) == personalNumCheck
|
|
472
|
+
result["optional1CheckDigit"] = personalNumValid
|
|
473
|
+
if !personalNumValid {
|
|
474
|
+
invalidFields.append("optional1CheckDigit")
|
|
475
|
+
allValid = false
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Composite checksum - ICAO 9303 Part 3, Section 4.9
|
|
480
|
+
// TD2: Doc number(9) + check(1) + Birth(6) + check(1) + Expiry(6) + check(1) + Optional(7) = 31 chars
|
|
481
|
+
// TD3: Doc number(9) + check(1) + Birth(6) + check(1) + Expiry(6) + check(1) + Personal(14) + check(1) = 39 chars
|
|
482
|
+
let compositeCheck = line[line.index(line.startIndex, offsetBy: format == .TD2 ? 35 : 43)]
|
|
483
|
+
let composite: String
|
|
484
|
+
if format == .TD2 {
|
|
485
|
+
// TD2: positions 0-9 + 13-19 + 21-34 (excluding composite at 35)
|
|
486
|
+
composite = String(line.prefix(10)) + String(line.dropFirst(13).prefix(7)) + String(line.dropFirst(21).prefix(14))
|
|
487
|
+
} else {
|
|
488
|
+
// TD3: positions 0-9 + 13-19 + 21-42 (excluding composite at 43)
|
|
489
|
+
composite = String(line.prefix(10)) + String(line.dropFirst(13).prefix(7)) + String(line.dropFirst(21).prefix(22))
|
|
490
|
+
}
|
|
491
|
+
let compositeValid = try calculateMRZChecksum(composite) == compositeCheck
|
|
492
|
+
result["compositeCheckDigit"] = compositeValid
|
|
493
|
+
if !compositeValid {
|
|
494
|
+
invalidFields.append("compositeCheckDigit")
|
|
495
|
+
allValid = false
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
case .UNKNOWN:
|
|
499
|
+
break
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
result["allValid"] = allValid
|
|
503
|
+
result["invalidFields"] = invalidFields
|
|
504
|
+
|
|
505
|
+
return result
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Calculate MRZ checksum using 7-3-1 weighted algorithm per ICAO 9303
|
|
510
|
+
*
|
|
511
|
+
* ICAO 9303 Part 3, Section 4.9 - Check Digit Calculation:
|
|
512
|
+
*
|
|
513
|
+
* Algorithm:
|
|
514
|
+
* 1. Assign numerical values to characters:
|
|
515
|
+
* - Digits 0-9: values 0-9
|
|
516
|
+
* - Letters A-Z: values 10-35 (A=10, B=11, ..., Z=35)
|
|
517
|
+
* - Filler character '<': value 0
|
|
518
|
+
*
|
|
519
|
+
* 2. Apply weights [7, 3, 1] cyclically to each character position
|
|
520
|
+
*
|
|
521
|
+
* 3. Calculate: sum = Σ(character_value × weight) for all positions
|
|
522
|
+
*
|
|
523
|
+
* 4. Check digit = sum mod 10
|
|
524
|
+
*
|
|
525
|
+
* Example: "AB2134<"
|
|
526
|
+
* A(10)×7 + B(11)×3 + 2(2)×1 + 1(1)×7 + 3(3)×3 + 4(4)×1 + <(0)×7
|
|
527
|
+
* = 70 + 33 + 2 + 7 + 9 + 4 + 0 = 125
|
|
528
|
+
* Check digit = 125 mod 10 = 5
|
|
529
|
+
*
|
|
530
|
+
* @param data The string to calculate checksum for
|
|
531
|
+
* @return The check digit as a character ('0'-'9')
|
|
532
|
+
* @see https://www2023.icao.int/publications/Documents/9303_p3_cons_en.pdf
|
|
533
|
+
*/
|
|
534
|
+
private func calculateMRZChecksum(_ data: String) throws -> Character {
|
|
535
|
+
let weights = [7, 3, 1]
|
|
536
|
+
var sum = 0
|
|
537
|
+
|
|
538
|
+
for (index, char) in data.enumerated() {
|
|
539
|
+
let value: Int
|
|
540
|
+
switch char {
|
|
541
|
+
case "0"..."9":
|
|
542
|
+
value = Int(char.asciiValue! - Character("0").asciiValue!)
|
|
543
|
+
case "A"..."Z":
|
|
544
|
+
value = Int(char.asciiValue! - Character("A").asciiValue!) + 10
|
|
545
|
+
case "<":
|
|
546
|
+
value = 0
|
|
547
|
+
default:
|
|
548
|
+
value = 0
|
|
549
|
+
}
|
|
550
|
+
sum += value * weights[index % 3]
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
let checksumDigit = sum % 10
|
|
554
|
+
return Character(String(checksumDigit))
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Fix common OCR errors in MRZ text with mrz-fast inspired corrections
|
|
559
|
+
*/
|
|
560
|
+
private func fixMRZ(_ rawText: String) -> String {
|
|
561
|
+
var fixed = rawText
|
|
562
|
+
.replacingOccurrences(of: " ", with: "")
|
|
563
|
+
.replacingOccurrences(of: "«", with: "")
|
|
564
|
+
|
|
565
|
+
// Remove invalid patterns
|
|
566
|
+
if let regex = try? NSRegularExpression(pattern: "<K+|r+K+|<r+K+", options: []) {
|
|
567
|
+
fixed = regex.stringByReplacingMatches(in: fixed, options: [], range: NSRange(fixed.startIndex..., in: fixed), withTemplate: "")
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Fix common O/0 confusion in Turkish ID document numbers
|
|
571
|
+
if let regex = try? NSRegularExpression(pattern: "\\bI<TUR([A-Z0-9]{3})0([A-Z0-9]{6})\\b", options: []) {
|
|
572
|
+
fixed = regex.stringByReplacingMatches(in: fixed, options: [], range: NSRange(fixed.startIndex..., in: fixed), withTemplate: "I<TUR$1O$2")
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
let lines = fixed.components(separatedBy: "\n")
|
|
576
|
+
.map { $0.trimmingCharacters(in: .whitespaces) }
|
|
577
|
+
.filter { $0.contains("<") }
|
|
578
|
+
|
|
579
|
+
// Detect format and pad lines
|
|
580
|
+
let targetLength: Int
|
|
581
|
+
if lines.allSatisfy({ $0.count <= 30 }) {
|
|
582
|
+
targetLength = 30 // TD1
|
|
583
|
+
} else if lines.allSatisfy({ $0.count <= 36 }) {
|
|
584
|
+
targetLength = 36 // TD2
|
|
585
|
+
} else if lines.allSatisfy({ $0.count <= 44 }) {
|
|
586
|
+
targetLength = 44 // TD3
|
|
587
|
+
} else {
|
|
588
|
+
return rawText // Cannot detect format
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
let paddedLines = lines.map { line in
|
|
592
|
+
line.count < targetLength ? line.padding(toLength: targetLength, withPad: "<", startingAt: 0) : line
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return applyOCRCorrections(paddedLines.joined(separator: "\n"))
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Apply position-aware OCR corrections inspired by mrz-fast
|
|
600
|
+
* Character confusion mapping: O↔0, I↔1↔l, S↔5, B↔8, Z↔2, G↔6
|
|
601
|
+
*/
|
|
602
|
+
private func applyOCRCorrections(_ mrzText: String) -> String {
|
|
603
|
+
var lines = mrzText.components(separatedBy: "\n")
|
|
604
|
+
if lines.isEmpty { return mrzText }
|
|
605
|
+
|
|
606
|
+
let format = detectMRZFormat(lines)
|
|
607
|
+
if format == .UNKNOWN { return mrzText }
|
|
608
|
+
|
|
609
|
+
switch format {
|
|
610
|
+
case .TD1:
|
|
611
|
+
// Line 2: positions 0-5 (birth date), 8-13 (expiry date), 6,14,29 (check digits)
|
|
612
|
+
lines[1] = correctDigitPositions(lines[1], ranges: [
|
|
613
|
+
0...5, 8...13 // Date fields need digits
|
|
614
|
+
])
|
|
615
|
+
lines[1] = correctCheckDigitPositions(lines[1], positions: [6, 14, 29])
|
|
616
|
+
// Nationality (15-17) needs letters
|
|
617
|
+
lines[1] = correctLetterPositions(lines[1], ranges: [15...17])
|
|
618
|
+
|
|
619
|
+
case .TD2:
|
|
620
|
+
// Line 2: positions 13-18 (birth), 21-26 (expiry), 9,19,27,35 (checks)
|
|
621
|
+
lines[1] = correctDigitPositions(lines[1], ranges: [
|
|
622
|
+
13...18, 21...26
|
|
623
|
+
])
|
|
624
|
+
lines[1] = correctCheckDigitPositions(lines[1], positions: [9, 19, 27, 35])
|
|
625
|
+
// Nationality (10-12) needs letters
|
|
626
|
+
lines[1] = correctLetterPositions(lines[1], ranges: [10...12])
|
|
627
|
+
|
|
628
|
+
case .TD3:
|
|
629
|
+
// Line 2: positions 13-18 (birth), 21-26 (expiry), 9,19,27,42 (checks)
|
|
630
|
+
lines[1] = correctDigitPositions(lines[1], ranges: [
|
|
631
|
+
13...18, 21...26
|
|
632
|
+
])
|
|
633
|
+
lines[1] = correctCheckDigitPositions(lines[1], positions: [9, 19, 27, 42])
|
|
634
|
+
// Nationality (10-12) needs letters
|
|
635
|
+
lines[1] = correctLetterPositions(lines[1], ranges: [10...12])
|
|
636
|
+
|
|
637
|
+
case .UNKNOWN:
|
|
638
|
+
break
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return lines.joined(separator: "\n")
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Correct positions that should contain digits (0-9)
|
|
646
|
+
*/
|
|
647
|
+
private func correctDigitPositions(_ line: String, ranges: [ClosedRange<Int>]) -> String {
|
|
648
|
+
var corrected = Array(line)
|
|
649
|
+
ranges.forEach { range in
|
|
650
|
+
range.forEach { pos in
|
|
651
|
+
if pos < corrected.count {
|
|
652
|
+
let char = corrected[pos]
|
|
653
|
+
let replacement: Character
|
|
654
|
+
switch char {
|
|
655
|
+
case "O", "o": replacement = "0"
|
|
656
|
+
case "I", "l": replacement = "1"
|
|
657
|
+
case "Z": replacement = "2"
|
|
658
|
+
case "S": replacement = "5"
|
|
659
|
+
case "G": replacement = "6"
|
|
660
|
+
case "B": replacement = "8"
|
|
661
|
+
case "D", "Q": replacement = "0"
|
|
662
|
+
default: replacement = char
|
|
663
|
+
}
|
|
664
|
+
corrected[pos] = replacement
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return String(corrected)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Correct positions that should contain letters (A-Z) or <
|
|
673
|
+
*/
|
|
674
|
+
private func correctLetterPositions(_ line: String, ranges: [ClosedRange<Int>]) -> String {
|
|
675
|
+
var corrected = Array(line)
|
|
676
|
+
ranges.forEach { range in
|
|
677
|
+
range.forEach { pos in
|
|
678
|
+
if pos < corrected.count {
|
|
679
|
+
let char = corrected[pos]
|
|
680
|
+
let replacement: Character
|
|
681
|
+
switch char {
|
|
682
|
+
case "0": replacement = "O"
|
|
683
|
+
case "1": replacement = "I"
|
|
684
|
+
case "5": replacement = "S"
|
|
685
|
+
case "8": replacement = "B"
|
|
686
|
+
case "2": replacement = "Z"
|
|
687
|
+
case "6": replacement = "G"
|
|
688
|
+
default: replacement = char
|
|
689
|
+
}
|
|
690
|
+
corrected[pos] = replacement
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return String(corrected)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Correct check digit positions (should be 0-9 only)
|
|
699
|
+
*/
|
|
700
|
+
private func correctCheckDigitPositions(_ line: String, positions: [Int]) -> String {
|
|
701
|
+
var corrected = Array(line)
|
|
702
|
+
positions.forEach { pos in
|
|
703
|
+
if pos < corrected.count {
|
|
704
|
+
let char = corrected[pos]
|
|
705
|
+
let replacement: Character
|
|
706
|
+
switch char {
|
|
707
|
+
case "O", "o": replacement = "0"
|
|
708
|
+
case "I", "l": replacement = "1"
|
|
709
|
+
case "Z": replacement = "2"
|
|
710
|
+
case "S": replacement = "5"
|
|
711
|
+
case "G": replacement = "6"
|
|
712
|
+
case "B": replacement = "8"
|
|
713
|
+
default: replacement = char
|
|
714
|
+
}
|
|
715
|
+
corrected[pos] = replacement
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return String(corrected)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Generate all possible O/0 permutations for MRZ text
|
|
723
|
+
* This handles the most common OCR confusion between O and 0
|
|
724
|
+
*
|
|
725
|
+
* @param mrzText Original MRZ text
|
|
726
|
+
* @param maxPermutations Maximum number of permutations to generate (default: 100)
|
|
727
|
+
* @return Array of all possible O/0 permutations
|
|
728
|
+
*/
|
|
729
|
+
private func generateO0Permutations(_ mrzText: String, maxPermutations: Int = 100) -> [String] {
|
|
730
|
+
var permutations = Set<String>([mrzText])
|
|
731
|
+
|
|
732
|
+
// Find all positions with O or 0
|
|
733
|
+
let ambiguousPositions = mrzText.enumerated().compactMap { (index, char) in
|
|
734
|
+
(char == "O" || char == "o" || char == "0") ? index : nil
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Limit positions to avoid combinatorial explosion
|
|
738
|
+
let positions = Array(ambiguousPositions.prefix(min(7, ambiguousPositions.count)))
|
|
739
|
+
|
|
740
|
+
// Generate all combinations (2^n where n = number of positions)
|
|
741
|
+
let numCombinations = 1 << positions.count // 2^n
|
|
742
|
+
let limit = min(numCombinations, maxPermutations)
|
|
743
|
+
|
|
744
|
+
for i in 0..<limit {
|
|
745
|
+
var variant = Array(mrzText)
|
|
746
|
+
|
|
747
|
+
// For each bit in the combination number, decide O or 0
|
|
748
|
+
for (j, pos) in positions.enumerated() {
|
|
749
|
+
let useZero = (i & (1 << j)) != 0
|
|
750
|
+
let currentChar = variant[pos]
|
|
751
|
+
|
|
752
|
+
// Replace with appropriate character
|
|
753
|
+
if useZero && currentChar != "0" {
|
|
754
|
+
variant[pos] = "0"
|
|
755
|
+
} else if !useZero && currentChar != "O" {
|
|
756
|
+
variant[pos] = "O"
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
permutations.insert(String(variant))
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return Array(permutations)
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Generate I/1 permutations for additional OCR confusion cases
|
|
768
|
+
*/
|
|
769
|
+
private func generateI1Permutations(_ mrzText: String, maxPermutations: Int = 50) -> [String] {
|
|
770
|
+
var permutations = Set<String>([mrzText])
|
|
771
|
+
|
|
772
|
+
// Find all positions with I or 1
|
|
773
|
+
let ambiguousPositions = mrzText.enumerated().compactMap { (index, char) in
|
|
774
|
+
(char == "I" || char == "i" || char == "l" || char == "1") ? index : nil
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Limit positions to avoid too many permutations
|
|
778
|
+
let positions = Array(ambiguousPositions.prefix(min(6, ambiguousPositions.count)))
|
|
779
|
+
|
|
780
|
+
let numCombinations = 1 << positions.count
|
|
781
|
+
let limit = min(numCombinations, maxPermutations)
|
|
782
|
+
|
|
783
|
+
for i in 0..<limit {
|
|
784
|
+
var variant = Array(mrzText)
|
|
785
|
+
|
|
786
|
+
for (j, pos) in positions.enumerated() {
|
|
787
|
+
let useOne = (i & (1 << j)) != 0
|
|
788
|
+
let currentChar = variant[pos]
|
|
789
|
+
|
|
790
|
+
if useOne && currentChar != "1" {
|
|
791
|
+
variant[pos] = "1"
|
|
792
|
+
} else if !useOne && (currentChar == "1" || currentChar == "l" || currentChar == "i") {
|
|
793
|
+
variant[pos] = "I"
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
permutations.insert(String(variant))
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return Array(permutations)
|
|
801
|
+
}
|
|
802
|
+
}
|