mirror of
https://github.com/selfxyz/self.git
synced 2026-01-10 15:18:18 -05:00
Hotfix: Belgium ID cards (#1061)
* feat: parse belgium TD1 mrz android * feat: Parse Belgium TD1 MRZ IOS
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user