From 429bed68b31dc672667ada064ef5d811b86fea99 Mon Sep 17 00:00:00 2001 From: seshanthS Date: Fri, 12 Sep 2025 17:13:04 +0530 Subject: [PATCH 1/2] feat: parse belgium TD1 mrz android --- .../com/passportreader/utils/OcrUtils.kt | 74 ++++++++++++++++--- .../com/selfxyz/selfSDK/utils/OcrUtils.kt | 73 +++++++++++++++--- 2 files changed, 123 insertions(+), 24 deletions(-) diff --git a/app/android/android-passport-reader/app/src/main/java/example/jllarraz/com/passportreader/utils/OcrUtils.kt b/app/android/android-passport-reader/app/src/main/java/example/jllarraz/com/passportreader/utils/OcrUtils.kt index a7c3dc44a..a41e2061d 100644 --- a/app/android/android-passport-reader/app/src/main/java/example/jllarraz/com/passportreader/utils/OcrUtils.kt +++ b/app/android/android-passport-reader/app/src/main/java/example/jllarraz/com/passportreader/utils/OcrUtils.kt @@ -26,9 +26,15 @@ object OcrUtils { private val REGEX_ID_DOCUMENT_NUMBER = "(ID)(?[A-Z<]{3})(?[A-Z0-9<]{9})(?[0-9]{1})" private val REGEX_ID_DATE_OF_BIRTH = "(?[0-9]{6})(?[0-9]{1})(?[FM<]{1})" + // Belgium TD1 (ID Card) specific pattern + private val REGEX_BELGIUM_ID_DOCUMENT_NUMBER = "IDBEL(?[A-Z0-9]{9})<(?[A-Z0-9]{3})(?\\d)" + private val REGEX_BELGIUM_ID_DATE_OF_BIRTH = "(?[0-9]{6})(?[0-9]{1})(?[FM<]{1})(?[0-9]{6})(?[0-9]{1})" + private val patternDocumentNumber = Pattern.compile(REGEX_ID_DOCUMENT_NUMBER) private val patternDateOfBirth = Pattern.compile(REGEX_ID_DATE_OF_BIRTH) private val patternDocumentCode = Pattern.compile(REGEX_ID_DOCUMENT_CODE) + private val patternBelgiumDocumentNumber = Pattern.compile(REGEX_BELGIUM_ID_DOCUMENT_NUMBER) + private val patternBelgiumDateOfBirth = Pattern.compile(REGEX_BELGIUM_ID_DATE_OF_BIRTH) fun processOcr( @@ -50,7 +56,7 @@ object OcrUtils { fullRead += "$temp-" } fullRead = fullRead.uppercase() - Log.d(TAG, "Read: $fullRead") + // Log.d(TAG, "Read: $fullRead") // We try with TD1 format first (ID Card) val patternTD1Line1 = Pattern.compile(REGEX_TD1_LINE1) @@ -69,40 +75,63 @@ object OcrUtils { val matcherDocumentNumber = patternDocumentNumber.matcher(fullRead) val matcherDateOfBirth = patternDateOfBirth.matcher(fullRead) - val hasDocumentNumber = matcherDocumentNumber.find() val hasDateOfBirth = matcherDateOfBirth.find() + // Belgium specific matchers + val matcherBelgiumDocumentNumber = patternBelgiumDocumentNumber.matcher(fullRead) + val hasBelgiumDocumentNumber = matcherBelgiumDocumentNumber.find() + val documentNumber = if (hasDocumentNumber) matcherDocumentNumber.group("documentNumber") else null val checkDigitDocumentNumber = if (hasDocumentNumber) matcherDocumentNumber.group("checkDigitDocumentNumber")?.toIntOrNull() else null val countryCode = if (hasDocumentNumber) matcherDocumentNumber.group("country") else null val dateOfBirth = if (hasDateOfBirth) matcherDateOfBirth.group("dateOfBirth") else null + + // Belgium specific values + val belgiumCheckDigit = if (hasBelgiumDocumentNumber) matcherBelgiumDocumentNumber.group("checkDigit")?.toIntOrNull() else null + val belgiumDateOfBirth = if (hasBelgiumDocumentNumber) { + val dateOfBirthMatcher = patternBelgiumDateOfBirth.matcher(fullRead) + if (dateOfBirthMatcher.find()) dateOfBirthMatcher.group("dateOfBirth") else null + } else null + + // Final values + val finalDocumentNumber = if (hasBelgiumDocumentNumber) { + val doc9 = matcherBelgiumDocumentNumber.group("doc9") + val doc3 = matcherBelgiumDocumentNumber.group("doc3") + val checkDigit = matcherBelgiumDocumentNumber.group("checkDigit") + cleanBelgiumDocumentNumber(doc9, doc3, checkDigit) + } else documentNumber + val finalDateOfBirth = if (hasBelgiumDocumentNumber) belgiumDateOfBirth else dateOfBirth + val finalCountryCode = if (hasBelgiumDocumentNumber) "BEL" else countryCode + val finalCheckDigit = if (hasBelgiumDocumentNumber) belgiumCheckDigit else checkDigitDocumentNumber + val checkDigitDateOfBirth = if (hasDateOfBirth) matcherDateOfBirth.group("checkDigitDateOfBirth")?.toIntOrNull() else null val gender = if (hasDateOfBirth) matcherDateOfBirth.group("gender") else null - val expirationDate: String? = if (!countryCode.isNullOrEmpty()) { - val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})" + Pattern.quote(countryCode) + val expirationDate: String? = if (!finalCountryCode.isNullOrEmpty()) { + val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})" + Pattern.quote(finalCountryCode) + // val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})UTO" val patternExpirationDate = Pattern.compile(expirationDateRegex) val matcherExpirationDate = patternExpirationDate.matcher(fullRead) if (matcherExpirationDate.find()) matcherExpirationDate.group("expirationDate") else null } else null // Only proceed if all required fields are present and non-empty - if (!countryCode.isNullOrEmpty() && !documentNumber.isNullOrEmpty() && !dateOfBirth.isNullOrEmpty() && !expirationDate.isNullOrEmpty() && checkDigitDocumentNumber != null) { - val cleanDocumentNumber = cleanDocumentNumber(documentNumber, checkDigitDocumentNumber) - Log.d(TAG, "cleanDocumentNumber") + if (!finalCountryCode.isNullOrEmpty() && !finalDocumentNumber.isNullOrEmpty() && !finalDateOfBirth.isNullOrEmpty() && !expirationDate.isNullOrEmpty() && finalCheckDigit != null) { + val cleanDocumentNumber = cleanDocumentNumber(finalDocumentNumber, finalCheckDigit) + // Log.d(TAG, "cleanDocumentNumber") if (cleanDocumentNumber != null) { - val mrzInfo = createDummyMrz("ID", countryCode, cleanDocumentNumber, dateOfBirth, expirationDate) + val mrzInfo = createDummyMrz("ID", finalCountryCode, cleanDocumentNumber, finalDateOfBirth, expirationDate) // Log.d(TAG, "MRZ-TD1: $mrzInfo") callback.onMRZRead(mrzInfo, timeRequired) return } } else { - if (countryCode.isNullOrEmpty()) Log.d(TAG, "Missing or invalid countryCode") - if (documentNumber.isNullOrEmpty()) Log.d(TAG, "Missing or invalid documentNumber") - if (dateOfBirth.isNullOrEmpty()) Log.d(TAG, "Missing or invalid dateOfBirth") + if (finalCountryCode.isNullOrEmpty()) Log.d(TAG, "Missing or invalid finalCountryCode") + if (finalDocumentNumber.isNullOrEmpty()) Log.d(TAG, "Missing or invalid finalDocumentNumber") + if (finalDateOfBirth.isNullOrEmpty()) Log.d(TAG, "Missing or invalid dateOfBirth") if (expirationDate.isNullOrEmpty()) Log.d(TAG, "Missing or invalid expirationDate") - if (checkDigitDocumentNumber == null) Log.d(TAG, "Missing or invalid checkDigitDocumentNumber") + if (finalCheckDigit == null) Log.d(TAG, "Missing or invalid finalCheckDigit") } } @@ -194,6 +223,27 @@ object OcrUtils { return null } + private fun cleanBelgiumDocumentNumber(doc9: String, doc3: String, checkDigit: String): String? { + // For Belgium TD1 format: IDBEL000001115<7027 + // doc9 = "000001115" (9 digits) + // doc3 = "702" (3 digits after <) + // checkDigit = "7" (single check digit) + + var cleanDoc9 = doc9 + cleanDoc9 = cleanDoc9.substring(3) + + val fullDocumentNumber = cleanDoc9 + doc3 + + val checkDigitCalculated = MRZInfo.checkDigit(fullDocumentNumber).toString().toInt() + val expectedCheckDigit = checkDigit.toInt() + + if (checkDigitCalculated == expectedCheckDigit) { + return fullDocumentNumber + } + + return null + } + private fun createDummyMrz( documentType: String, issuingState: String = "ESP", diff --git a/packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/utils/OcrUtils.kt b/packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/utils/OcrUtils.kt index fcd9b2a5c..57cbd4571 100644 --- a/packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/utils/OcrUtils.kt +++ b/packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/utils/OcrUtils.kt @@ -26,9 +26,15 @@ object OcrUtils { private val REGEX_ID_DOCUMENT_NUMBER = "(ID)(?[A-Z<]{3})(?[A-Z0-9<]{9})(?[0-9]{1})" private val REGEX_ID_DATE_OF_BIRTH = "(?[0-9]{6})(?[0-9]{1})(?[FM<]{1})" + // Belgium TD1 (ID Card) specific pattern + private val REGEX_BELGIUM_ID_DOCUMENT_NUMBER = "IDBEL(?[A-Z0-9]{9})<(?[A-Z0-9]{3})(?\\d)" + private val REGEX_BELGIUM_ID_DATE_OF_BIRTH = "(?[0-9]{6})(?[0-9]{1})(?[FM<]{1})(?[0-9]{6})(?[0-9]{1})" + private val patternDocumentNumber = Pattern.compile(REGEX_ID_DOCUMENT_NUMBER) private val patternDateOfBirth = Pattern.compile(REGEX_ID_DATE_OF_BIRTH) private val patternDocumentCode = Pattern.compile(REGEX_ID_DOCUMENT_CODE) + private val patternBelgiumDocumentNumber = Pattern.compile(REGEX_BELGIUM_ID_DOCUMENT_NUMBER) + private val patternBelgiumDateOfBirth = Pattern.compile(REGEX_BELGIUM_ID_DATE_OF_BIRTH) fun processOcr( @@ -49,7 +55,6 @@ object OcrUtils { temp = temp.replace("\r".toRegex(), "").replace("\n".toRegex(), "").replace("\t".toRegex(), "").replace(" ", "") fullRead += "$temp-" } - // fullRead = fullRead.toUpperCase() fullRead = fullRead.uppercase() // Log.d(TAG, "Read: $fullRead") @@ -70,40 +75,63 @@ object OcrUtils { val matcherDocumentNumber = patternDocumentNumber.matcher(fullRead) val matcherDateOfBirth = patternDateOfBirth.matcher(fullRead) - val hasDocumentNumber = matcherDocumentNumber.find() val hasDateOfBirth = matcherDateOfBirth.find() + // Belgium specific matchers + val matcherBelgiumDocumentNumber = patternBelgiumDocumentNumber.matcher(fullRead) + val hasBelgiumDocumentNumber = matcherBelgiumDocumentNumber.find() + val documentNumber = if (hasDocumentNumber) matcherDocumentNumber.group("documentNumber") else null val checkDigitDocumentNumber = if (hasDocumentNumber) matcherDocumentNumber.group("checkDigitDocumentNumber")?.toIntOrNull() else null val countryCode = if (hasDocumentNumber) matcherDocumentNumber.group("country") else null val dateOfBirth = if (hasDateOfBirth) matcherDateOfBirth.group("dateOfBirth") else null + + // Belgium specific values + val belgiumCheckDigit = if (hasBelgiumDocumentNumber) matcherBelgiumDocumentNumber.group("checkDigit")?.toIntOrNull() else null + val belgiumDateOfBirth = if (hasBelgiumDocumentNumber) { + val dateOfBirthMatcher = patternBelgiumDateOfBirth.matcher(fullRead) + if (dateOfBirthMatcher.find()) dateOfBirthMatcher.group("dateOfBirth") else null + } else null + + // Final values + val finalDocumentNumber = if (hasBelgiumDocumentNumber) { + val doc9 = matcherBelgiumDocumentNumber.group("doc9") + val doc3 = matcherBelgiumDocumentNumber.group("doc3") + val checkDigit = matcherBelgiumDocumentNumber.group("checkDigit") + cleanBelgiumDocumentNumber(doc9, doc3, checkDigit) + } else documentNumber + val finalDateOfBirth = if (hasBelgiumDocumentNumber) belgiumDateOfBirth else dateOfBirth + val finalCountryCode = if (hasBelgiumDocumentNumber) "BEL" else countryCode + val finalCheckDigit = if (hasBelgiumDocumentNumber) belgiumCheckDigit else checkDigitDocumentNumber + val checkDigitDateOfBirth = if (hasDateOfBirth) matcherDateOfBirth.group("checkDigitDateOfBirth")?.toIntOrNull() else null val gender = if (hasDateOfBirth) matcherDateOfBirth.group("gender") else null - val expirationDate: String? = if (!countryCode.isNullOrEmpty()) { - val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})" + Pattern.quote(countryCode) + val expirationDate: String? = if (!finalCountryCode.isNullOrEmpty()) { + val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})" + Pattern.quote(finalCountryCode) + // val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})UTO" val patternExpirationDate = Pattern.compile(expirationDateRegex) val matcherExpirationDate = patternExpirationDate.matcher(fullRead) if (matcherExpirationDate.find()) matcherExpirationDate.group("expirationDate") else null } else null // Only proceed if all required fields are present and non-empty - if (!countryCode.isNullOrEmpty() && !documentNumber.isNullOrEmpty() && !dateOfBirth.isNullOrEmpty() && !expirationDate.isNullOrEmpty() && checkDigitDocumentNumber != null) { - val cleanDocumentNumber = cleanDocumentNumber(documentNumber, checkDigitDocumentNumber) - Log.d(TAG, "cleanDocumentNumber") + if (!finalCountryCode.isNullOrEmpty() && !finalDocumentNumber.isNullOrEmpty() && !finalDateOfBirth.isNullOrEmpty() && !expirationDate.isNullOrEmpty() && finalCheckDigit != null) { + val cleanDocumentNumber = cleanDocumentNumber(finalDocumentNumber, finalCheckDigit) + // Log.d(TAG, "cleanDocumentNumber") if (cleanDocumentNumber != null) { - val mrzInfo = createDummyMrz("ID", countryCode, cleanDocumentNumber, dateOfBirth, expirationDate) + val mrzInfo = createDummyMrz("ID", finalCountryCode, cleanDocumentNumber, finalDateOfBirth, expirationDate) // Log.d(TAG, "MRZ-TD1: $mrzInfo") callback.onMRZRead(mrzInfo, timeRequired) return } } else { - if (countryCode.isNullOrEmpty()) Log.d(TAG, "Missing or invalid countryCode") - if (documentNumber.isNullOrEmpty()) Log.d(TAG, "Missing or invalid documentNumber") - if (dateOfBirth.isNullOrEmpty()) Log.d(TAG, "Missing or invalid dateOfBirth") + if (finalCountryCode.isNullOrEmpty()) Log.d(TAG, "Missing or invalid finalCountryCode") + if (finalDocumentNumber.isNullOrEmpty()) Log.d(TAG, "Missing or invalid finalDocumentNumber") + if (finalDateOfBirth.isNullOrEmpty()) Log.d(TAG, "Missing or invalid dateOfBirth") if (expirationDate.isNullOrEmpty()) Log.d(TAG, "Missing or invalid expirationDate") - if (checkDigitDocumentNumber == null) Log.d(TAG, "Missing or invalid checkDigitDocumentNumber") + if (finalCheckDigit == null) Log.d(TAG, "Missing or invalid finalCheckDigit") } } @@ -197,6 +225,27 @@ object OcrUtils { return null } + private fun cleanBelgiumDocumentNumber(doc9: String, doc3: String, checkDigit: String): String? { + // For Belgium TD1 format: IDBEL000001115<7027 + // doc9 = "000001115" (9 digits) + // doc3 = "702" (3 digits after <) + // checkDigit = "7" (single check digit) + + var cleanDoc9 = doc9 + cleanDoc9 = cleanDoc9.substring(3) + + val fullDocumentNumber = cleanDoc9 + doc3 + + val checkDigitCalculated = MRZInfo.checkDigit(fullDocumentNumber).toString().toInt() + val expectedCheckDigit = checkDigit.toInt() + + if (checkDigitCalculated == expectedCheckDigit) { + return fullDocumentNumber + } + + return null + } + private fun createDummyMrz( documentType: String, issuingState: String = "ESP", From ee3a546469355cd12f30f48b103c157ff51d8f97 Mon Sep 17 00:00:00 2001 From: seshanthS Date: Fri, 12 Sep 2025 23:34:13 +0530 Subject: [PATCH 2/2] feat: Parse Belgium TD1 MRZ IOS --- app/ios/LiveMRZScannerView.swift | 143 ++++++++++++++++-- .../ios/SelfSDK/SelfLiveMRZScannerView.swift | 143 ++++++++++++++++-- 2 files changed, 259 insertions(+), 27 deletions(-) diff --git a/app/ios/LiveMRZScannerView.swift b/app/ios/LiveMRZScannerView.swift index ed6562bb1..7def9801d 100644 --- a/app/ios/LiveMRZScannerView.swift +++ b/app/ios/LiveMRZScannerView.swift @@ -80,6 +80,112 @@ struct LiveMRZScannerView: View { ] } + private func correctBelgiumDocumentNumber(result: String) -> String? { + // Belgium TD1 format: IDBEL000001115<7027 + let line1RegexPattern = "IDBEL(?[A-Z0-9]{9})<(?[A-Z0-9<]{3})(?\\d)" + guard let line1Regex = try? NSRegularExpression(pattern: line1RegexPattern) else { return nil } + let line1Matcher = line1Regex.firstMatch(in: result, options: [], range: NSRange(location: 0, length: result.count)) + + if let line1Matcher = line1Matcher { + let doc9Range = line1Matcher.range(withName: "doc9") + let doc3Range = line1Matcher.range(withName: "doc3") + let checkDigitRange = line1Matcher.range(withName: "checkDigit") + + let doc9 = (result as NSString).substring(with: doc9Range) + let doc3 = (result as NSString).substring(with: doc3Range) + let checkDigit = (result as NSString).substring(with: checkDigitRange) + + if let cleanedDoc = cleanBelgiumDocumentNumber(doc9: doc9, doc3: doc3, checkDigit: checkDigit) { + let correctedMRZLine = "IDBEL\(cleanedDoc)\(checkDigit)" + return correctedMRZLine + } + } + return nil + } + + private func cleanBelgiumDocumentNumber(doc9: String, doc3: String, checkDigit: String) -> String? { + // For Belgium TD1 format: IDBEL000001115<7027 + // doc9 = "000001115" (9 digits) + // doc3 = "702" (3 digits after <) + // checkDigit = "7" (single check digit) + + var cleanDoc9 = doc9 + // Strip first 3 characters + let startIndex = cleanDoc9.index(cleanDoc9.startIndex, offsetBy: 3) + cleanDoc9 = String(cleanDoc9[startIndex...]) + + let fullDocumentNumber = cleanDoc9 + doc3 + + + return fullDocumentNumber + } + + private func isValidMRZResult(_ result: QKMRZResult) -> Bool { + return result.isDocumentNumberValid && result.isExpiryDateValid && result.isBirthdateValid + } + + private func handleValidMRZResult(_ result: QKMRZResult) { + parsedMRZ = result + scanComplete = true + onScanComplete?(result) + onScanResultAsDict?(mapVisionResultToDictionary(result)) + } + + private func processBelgiumDocument(result: String, parser: QKMRZParser) -> QKMRZResult? { + print("[LiveMRZScannerView] Processing Belgium document") + + guard let correctedBelgiumLine = correctBelgiumDocumentNumber(result: result) else { + print("[LiveMRZScannerView] Failed to correct Belgium document number") + return nil + } + + // print("[LiveMRZScannerView] Belgium corrected line: \(correctedBelgiumLine)") + + // Split MRZ into lines and replace the first line + let lines = result.components(separatedBy: "\n") + guard lines.count >= 3 else { + print("[LiveMRZScannerView] Invalid MRZ format - not enough lines") + return nil + } + + let originalFirstLine = lines[0] + // print("[LiveMRZScannerView] Original first line: \(originalFirstLine)") + + // Pad the corrected line to 30 characters (TD1 format) + let paddedCorrectedLine = correctedBelgiumLine.padding(toLength: 30, withPad: "<", startingAt: 0) + // print("[LiveMRZScannerView] Padded corrected line: \(paddedCorrectedLine)") + + // Reconstruct the MRZ with the corrected first line + var correctedLines = lines + correctedLines[0] = paddedCorrectedLine + let correctedMRZString = correctedLines.joined(separator: "\n") + // print("[LiveMRZScannerView] Corrected MRZ string: \(correctedMRZString)") + + guard let belgiumMRZResult = parser.parse(mrzString: correctedMRZString) else { + print("[LiveMRZScannerView] Belgium MRZ result is not valid") + return nil + } + + // print("[LiveMRZScannerView] Belgium MRZ result: \(belgiumMRZResult)") + + // Try the corrected MRZ first + if isValidMRZResult(belgiumMRZResult) { + return belgiumMRZResult + } + + // If document number is still invalid, try single character correction + if !belgiumMRZResult.isDocumentNumberValid { + if let correctedResult = singleCorrectDocumentNumberInMRZ(result: correctedMRZString, docNumber: belgiumMRZResult.documentNumber, parser: parser) { + // print("[LiveMRZScannerView] Single correction successful: \(correctedResult)") + if isValidMRZResult(correctedResult) { + return correctedResult + } + } + } + + return nil + } + var body: some View { ZStack(alignment: .bottom) { CameraView( @@ -91,20 +197,31 @@ struct LiveMRZScannerView: View { // print("[LiveMRZScannerView] result: \(result)") let parser = QKMRZParser(ocrCorrection: false) if let mrzResult = parser.parse(mrzString: result) { - let doc = mrzResult; - if doc.allCheckDigitsValid == true && !scanComplete { - parsedMRZ = mrzResult - scanComplete = true - onScanComplete?(mrzResult) - onScanResultAsDict?(mapVisionResultToDictionary(mrzResult)) - } else if doc.isDocumentNumberValid == false && !scanComplete { + let doc = mrzResult + // print("[LiveMRZScannerView] doc: \(doc)") + + guard !scanComplete else { return } + + // Check if already valid + if doc.allCheckDigitsValid { + handleValidMRZResult(mrzResult) + return + } + + // Handle Belgium documents (only if not already valid) + if doc.countryCode == "BEL" { + if let belgiumResult = processBelgiumDocument(result: result, parser: parser) { + handleValidMRZResult(belgiumResult) + } + return + } + + // Handle other documents with invalid document numbers + if !doc.isDocumentNumberValid { if let correctedResult = singleCorrectDocumentNumberInMRZ(result: result, docNumber: doc.documentNumber, parser: parser) { - let correctedDoc = correctedResult - if correctedDoc.allCheckDigitsValid == true { - parsedMRZ = correctedResult - scanComplete = true - onScanComplete?(correctedResult) - onScanResultAsDict?(mapVisionResultToDictionary(correctedResult)) + // print("[LiveMRZScannerView] correctedDoc: \(correctedResult)") + if correctedResult.allCheckDigitsValid { + handleValidMRZResult(correctedResult) } } } diff --git a/packages/mobile-sdk-alpha/ios/SelfSDK/SelfLiveMRZScannerView.swift b/packages/mobile-sdk-alpha/ios/SelfSDK/SelfLiveMRZScannerView.swift index 13326e01e..f5b1c6298 100644 --- a/packages/mobile-sdk-alpha/ios/SelfSDK/SelfLiveMRZScannerView.swift +++ b/packages/mobile-sdk-alpha/ios/SelfSDK/SelfLiveMRZScannerView.swift @@ -83,6 +83,112 @@ struct SelfLiveMRZScannerView: View { ] } + private func correctBelgiumDocumentNumber(result: String) -> String? { + // Belgium TD1 format: IDBEL000001115<7027 + let line1RegexPattern = "IDBEL(?[A-Z0-9]{9})<(?[A-Z0-9<]{3})(?\\d)" + guard let line1Regex = try? NSRegularExpression(pattern: line1RegexPattern) else { return nil } + let line1Matcher = line1Regex.firstMatch(in: result, options: [], range: NSRange(location: 0, length: result.count)) + + if let line1Matcher = line1Matcher { + let doc9Range = line1Matcher.range(withName: "doc9") + let doc3Range = line1Matcher.range(withName: "doc3") + let checkDigitRange = line1Matcher.range(withName: "checkDigit") + + let doc9 = (result as NSString).substring(with: doc9Range) + let doc3 = (result as NSString).substring(with: doc3Range) + let checkDigit = (result as NSString).substring(with: checkDigitRange) + + if let cleanedDoc = cleanBelgiumDocumentNumber(doc9: doc9, doc3: doc3, checkDigit: checkDigit) { + let correctedMRZLine = "IDBEL\(cleanedDoc)\(checkDigit)" + return correctedMRZLine + } + } + return nil + } + + private func cleanBelgiumDocumentNumber(doc9: String, doc3: String, checkDigit: String) -> String? { + // For Belgium TD1 format: IDBEL000001115<7027 + // doc9 = "000001115" (9 digits) + // doc3 = "702" (3 digits after <) + // checkDigit = "7" (single check digit) + + var cleanDoc9 = doc9 + // Strip first 3 characters + let startIndex = cleanDoc9.index(cleanDoc9.startIndex, offsetBy: 3) + cleanDoc9 = String(cleanDoc9[startIndex...]) + + let fullDocumentNumber = cleanDoc9 + doc3 + + + return fullDocumentNumber + } + + private func isValidMRZResult(_ result: QKMRZResult) -> Bool { + return result.isDocumentNumberValid && result.isExpiryDateValid && result.isBirthdateValid + } + + private func handleValidMRZResult(_ result: QKMRZResult) { + parsedMRZ = result + scanComplete = true + onScanComplete?(result) + onScanResultAsDict?(mapVisionResultToDictionary(result)) + } + + private func processBelgiumDocument(result: String, parser: QKMRZParser) -> QKMRZResult? { + print("[LiveMRZScannerView] Processing Belgium document") + + guard let correctedBelgiumLine = correctBelgiumDocumentNumber(result: result) else { + print("[LiveMRZScannerView] Failed to correct Belgium document number") + return nil + } + + // print("[LiveMRZScannerView] Belgium corrected line: \(correctedBelgiumLine)") + + // Split MRZ into lines and replace the first line + let lines = result.components(separatedBy: "\n") + guard lines.count >= 3 else { + print("[LiveMRZScannerView] Invalid MRZ format - not enough lines") + return nil + } + + let originalFirstLine = lines[0] + // print("[LiveMRZScannerView] Original first line: \(originalFirstLine)") + + // Pad the corrected line to 30 characters (TD1 format) + let paddedCorrectedLine = correctedBelgiumLine.padding(toLength: 30, withPad: "<", startingAt: 0) + // print("[LiveMRZScannerView] Padded corrected line: \(paddedCorrectedLine)") + + // Reconstruct the MRZ with the corrected first line + var correctedLines = lines + correctedLines[0] = paddedCorrectedLine + let correctedMRZString = correctedLines.joined(separator: "\n") + // print("[LiveMRZScannerView] Corrected MRZ string: \(correctedMRZString)") + + guard let belgiumMRZResult = parser.parse(mrzString: correctedMRZString) else { + print("[LiveMRZScannerView] Belgium MRZ result is not valid") + return nil + } + + // print("[LiveMRZScannerView] Belgium MRZ result: \(belgiumMRZResult)") + + // Try the corrected MRZ first + if isValidMRZResult(belgiumMRZResult) { + return belgiumMRZResult + } + + // If document number is still invalid, try single character correction + if !belgiumMRZResult.isDocumentNumberValid { + if let correctedResult = singleCorrectDocumentNumberInMRZ(result: correctedMRZString, docNumber: belgiumMRZResult.documentNumber, parser: parser) { + // print("[LiveMRZScannerView] Single correction successful: \(correctedResult)") + if isValidMRZResult(correctedResult) { + return correctedResult + } + } + } + + return nil + } + var body: some View { ZStack(alignment: .bottom) { SelfCameraView( @@ -94,22 +200,31 @@ struct SelfLiveMRZScannerView: View { // print("[LiveMRZScannerView] result: \(result)") let parser = QKMRZParser(ocrCorrection: false) if let mrzResult = parser.parse(mrzString: result) { - let doc = mrzResult; + let doc = mrzResult // print("[LiveMRZScannerView] doc: \(doc)") - if doc.allCheckDigitsValid == true && !scanComplete { - parsedMRZ = mrzResult - scanComplete = true - onScanComplete?(mrzResult) - onScanResultAsDict?(mapVisionResultToDictionary(mrzResult)) - } else if doc.isDocumentNumberValid == false && !scanComplete { + + guard !scanComplete else { return } + + // Check if already valid + if doc.allCheckDigitsValid { + handleValidMRZResult(mrzResult) + return + } + + // Handle Belgium documents (only if not already valid) + if doc.countryCode == "BEL" { + if let belgiumResult = processBelgiumDocument(result: result, parser: parser) { + handleValidMRZResult(belgiumResult) + } + return + } + + // Handle other documents with invalid document numbers + if !doc.isDocumentNumberValid { if let correctedResult = singleCorrectDocumentNumberInMRZ(result: result, docNumber: doc.documentNumber, parser: parser) { - let correctedDoc = correctedResult - // print("[LiveMRZScannerView] correctedDoc: \(correctedDoc)") - if correctedDoc.allCheckDigitsValid == true { - parsedMRZ = correctedResult - scanComplete = true - onScanComplete?(correctedResult) - onScanResultAsDict?(mapVisionResultToDictionary(correctedResult)) + // print("[LiveMRZScannerView] correctedDoc: \(correctedResult)") + if correctedResult.allCheckDigitsValid { + handleValidMRZResult(correctedResult) } } }