Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand All @@ -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")
}
}

Expand Down Expand Up @@ -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",
Expand Down
143 changes: 130 additions & 13 deletions app/ios/LiveMRZScannerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +106 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Belgium document validation missing - inconsistent with Android

This iOS implementation doesn't validate the check digit, while the Android version does. This platform inconsistency could lead to different validation results.

Implement check digit validation to ensure platform consistency:

 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

+    // Validate check digit to match Android implementation
+    guard let parser = try? QKMRZParser() else { return nil }
+    let calculatedCheckDigit = parser.checkDigit(for: fullDocumentNumber)
+    guard String(calculatedCheckDigit) == checkDigit else { return nil }

     return fullDocumentNumber
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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
// Validate check digit to match Android implementation
guard let parser = try? QKMRZParser() else { return nil }
let calculatedCheckDigit = parser.checkDigit(for: fullDocumentNumber)
guard String(calculatedCheckDigit) == checkDigit else { return nil }
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(
Expand All @@ -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)
}
}
}
Expand Down
Loading
Loading