Hotfix: Belgium ID cards (#1061)

* feat: parse belgium TD1 mrz android

* feat: Parse Belgium TD1 MRZ IOS
This commit is contained in:
Seshanth.S🐺
2025-09-13 00:41:56 +05:30
committed by GitHub
parent 5b02868573
commit 85df67604a
4 changed files with 382 additions and 51 deletions

View File

@@ -26,9 +26,15 @@ object OcrUtils {
private val REGEX_ID_DOCUMENT_NUMBER = "(ID)(?<country>[A-Z<]{3})(?<documentNumber>[A-Z0-9<]{9})(?<checkDigitDocumentNumber>[0-9]{1})"
private val REGEX_ID_DATE_OF_BIRTH = "(?<dateOfBirth>[0-9]{6})(?<checkDigitDateOfBirth>[0-9]{1})(?<gender>[FM<]{1})"
// Belgium TD1 (ID Card) specific pattern
private val REGEX_BELGIUM_ID_DOCUMENT_NUMBER = "IDBEL(?<doc9>[A-Z0-9]{9})<(?<doc3>[A-Z0-9]{3})(?<checkDigit>\\d)"
private val REGEX_BELGIUM_ID_DATE_OF_BIRTH = "(?<dateOfBirth>[0-9]{6})(?<checkDigitDateOfBirth>[0-9]{1})(?<gender>[FM<]{1})(?<expirationDate>[0-9]{6})(?<checkDigitExpiration>[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 = "(?<expirationDate>[0-9]{6})(?<checkDigitExpiration>[0-9]{1})" + Pattern.quote(countryCode)
val expirationDate: String? = if (!finalCountryCode.isNullOrEmpty()) {
val expirationDateRegex = "(?<expirationDate>[0-9]{6})(?<checkDigitExpiration>[0-9]{1})" + Pattern.quote(finalCountryCode)
// val expirationDateRegex = "(?<expirationDate>[0-9]{6})(?<checkDigitExpiration>[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",

View File

@@ -80,6 +80,112 @@ struct LiveMRZScannerView: View {
]
}
private func correctBelgiumDocumentNumber(result: String) -> String? {
// Belgium TD1 format: IDBEL000001115<7027
let line1RegexPattern = "IDBEL(?<doc9>[A-Z0-9]{9})<(?<doc3>[A-Z0-9<]{3})(?<checkDigit>\\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)
}
}
}

View File

@@ -26,9 +26,15 @@ object OcrUtils {
private val REGEX_ID_DOCUMENT_NUMBER = "(ID)(?<country>[A-Z<]{3})(?<documentNumber>[A-Z0-9<]{9})(?<checkDigitDocumentNumber>[0-9]{1})"
private val REGEX_ID_DATE_OF_BIRTH = "(?<dateOfBirth>[0-9]{6})(?<checkDigitDateOfBirth>[0-9]{1})(?<gender>[FM<]{1})"
// Belgium TD1 (ID Card) specific pattern
private val REGEX_BELGIUM_ID_DOCUMENT_NUMBER = "IDBEL(?<doc9>[A-Z0-9]{9})<(?<doc3>[A-Z0-9]{3})(?<checkDigit>\\d)"
private val REGEX_BELGIUM_ID_DATE_OF_BIRTH = "(?<dateOfBirth>[0-9]{6})(?<checkDigitDateOfBirth>[0-9]{1})(?<gender>[FM<]{1})(?<expirationDate>[0-9]{6})(?<checkDigitExpiration>[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 = "(?<expirationDate>[0-9]{6})(?<checkDigitExpiration>[0-9]{1})" + Pattern.quote(countryCode)
val expirationDate: String? = if (!finalCountryCode.isNullOrEmpty()) {
val expirationDateRegex = "(?<expirationDate>[0-9]{6})(?<checkDigitExpiration>[0-9]{1})" + Pattern.quote(finalCountryCode)
// val expirationDateRegex = "(?<expirationDate>[0-9]{6})(?<checkDigitExpiration>[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",

View File

@@ -83,6 +83,112 @@ struct SelfLiveMRZScannerView: View {
]
}
private func correctBelgiumDocumentNumber(result: String) -> String? {
// Belgium TD1 format: IDBEL000001115<7027
let line1RegexPattern = "IDBEL(?<doc9>[A-Z0-9]{9})<(?<doc3>[A-Z0-9<]{3})(?<checkDigit>\\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)
}
}
}