Skip to content

Commit 92e6f02

Browse files
committed
fix: Belgian ID overflow handling
1 parent 2e51e26 commit 92e6f02

File tree

1 file changed

+118
-67
lines changed

1 file changed

+118
-67
lines changed

app/ios/LiveMRZScannerView.swift

Lines changed: 118 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -80,44 +80,99 @@ struct LiveMRZScannerView: View {
8080
]
8181
}
8282

83-
private func correctBelgiumDocumentNumber(result: String) -> String? {
84-
// Belgium TD1 format: IDBEL000001115<7027
85-
let line1RegexPattern = "IDBEL(?<doc9>[A-Z0-9]{9})<(?<doc3>[A-Z0-9<]{3})(?<checkDigit>\\d)"
86-
guard let line1Regex = try? NSRegularExpression(pattern: line1RegexPattern) else { return nil }
87-
let line1Matcher = line1Regex.firstMatch(in: result, options: [], range: NSRange(location: 0, length: result.count))
88-
89-
if let line1Matcher = line1Matcher {
90-
let doc9Range = line1Matcher.range(withName: "doc9")
91-
let doc3Range = line1Matcher.range(withName: "doc3")
92-
let checkDigitRange = line1Matcher.range(withName: "checkDigit")
93-
94-
let doc9 = (result as NSString).substring(with: doc9Range)
95-
let doc3 = (result as NSString).substring(with: doc3Range)
96-
let checkDigit = (result as NSString).substring(with: checkDigitRange)
97-
98-
if let cleanedDoc = cleanBelgiumDocumentNumber(doc9: doc9, doc3: doc3, checkDigit: checkDigit) {
99-
let correctedMRZLine = "IDBEL\(cleanedDoc)\(checkDigit)"
100-
return correctedMRZLine
83+
/// Calculates the MRZ check digit using the ICAO 9303 standard
84+
private func calculateMRZCheckDigit(_ input: String) -> Int {
85+
let weights = [7, 3, 1]
86+
var sum = 0
87+
88+
for (index, char) in input.enumerated() {
89+
let value: Int
90+
if char.isNumber {
91+
value = Int(String(char)) ?? 0
92+
} else if char.isLetter {
93+
// mapping letters to values: A=10, B=11, ..., Z=35
94+
value = Int(char.asciiValue ?? 0) - Int(Character("A").asciiValue ?? 0) + 10
95+
} else if char == "<" {
96+
value = 0
97+
} else {
98+
value = 0
10199
}
100+
101+
let weight = weights[index % 3]
102+
sum += value * weight
102103
}
103-
return nil
104+
105+
return sum % 10
104106
}
105107

106-
private func cleanBelgiumDocumentNumber(doc9: String, doc3: String, checkDigit: String) -> String? {
107-
// For Belgium TD1 format: IDBEL000001115<7027
108-
// doc9 = "000001115" (9 digits)
109-
// doc3 = "702" (3 digits after <)
110-
// checkDigit = "7" (single check digit)
108+
/// Extracts and validates the Belgian document number from MRZ line 1, handling both standard and overflow formats.
109+
/// Belgian TD1 format uses an overflow mechanism when document numbers exceed 9 digits.
110+
/// Example overflow format: IDBEL595392450<8039<<<<<<<<<< where positions 6-14 contain the principal part (595392450),
111+
/// position 15 contains the overflow indicator (<), positions 16-18 contain overflow digits (803), and position 19 contains the check digit (9).
112+
/// The full document number becomes: 595392450803.
113+
private func extractAndValidateBelgianDocumentNumber(line1: String) -> (documentNumber: String, isValid: Bool)? {
114+
guard line1.count == 30 else { return nil }
115+
116+
// extracting positions 6-14 (9 characters - principal part)
117+
let startIndex6 = line1.index(line1.startIndex, offsetBy: 5)
118+
let endIndex14 = line1.index(line1.startIndex, offsetBy: 14)
119+
let principalPart = String(line1[startIndex6..<endIndex14])
120+
121+
// checking position 15 for overflow indicator
122+
let pos15Index = line1.index(line1.startIndex, offsetBy: 14)
123+
let pos15 = line1[pos15Index]
124+
125+
if pos15 != "<" {
126+
// handling standard format where position 15 is the check digit
127+
let checkDigit = Int(String(pos15)) ?? -1
128+
let calculatedCheck = calculateMRZCheckDigit(principalPart)
129+
let isValid = (checkDigit == calculatedCheck)
130+
print("[extractAndValidateBelgianDocumentNumber] Standard format: \(principalPart), check=\(checkDigit), calculated=\(calculatedCheck), valid=\(isValid)")
131+
return (principalPart, isValid)
132+
}
133+
134+
// handling overflow format: scanning positions 16+ until we hit <
135+
let pos16Index = line1.index(line1.startIndex, offsetBy: 15)
136+
let remainingPart = String(line1[pos16Index...])
137+
138+
// finding the overflow digits and the check digit
139+
var overflowDigits = ""
140+
var checkDigitChar: Character?
141+
142+
for char in remainingPart {
143+
if char == "<" {
144+
break
145+
}
146+
overflowDigits.append(char)
147+
}
148+
149+
guard overflowDigits.count > 0 else {
150+
print("[extractAndValidateBelgianDocumentNumber] ERROR: No overflow digits found")
151+
return nil
152+
}
153+
154+
// extracting check digit (last character of overflow)
155+
checkDigitChar = overflowDigits.last
156+
let overflowWithoutCheck = String(overflowDigits.dropLast())
111157

112-
var cleanDoc9 = doc9
113-
// Strip first 3 characters
114-
let startIndex = cleanDoc9.index(cleanDoc9.startIndex, offsetBy: 3)
115-
cleanDoc9 = String(cleanDoc9[startIndex...])
158+
// constructing full document number: principal + overflow (without check digit)
159+
let fullDocumentNumber = principalPart + overflowWithoutCheck
116160

117-
let fullDocumentNumber = cleanDoc9 + doc3
161+
// validating check digit against full document number
162+
let checkDigit = Int(String(checkDigitChar!)) ?? -1
163+
let calculatedCheck = calculateMRZCheckDigit(fullDocumentNumber)
164+
let isValid = (checkDigit == calculatedCheck)
118165

166+
print("[extractAndValidateBelgianDocumentNumber] Overflow format:")
167+
print(" Principal part (6-14): \(principalPart)")
168+
print(" Overflow with check: \(overflowDigits)")
169+
print(" Overflow without check: \(overflowWithoutCheck)")
170+
print(" Full document number: \(fullDocumentNumber)")
171+
print(" Check digit: \(checkDigit)")
172+
print(" Calculated check: \(calculatedCheck)")
173+
print(" Valid: \(isValid)")
119174

120-
return fullDocumentNumber
175+
return (fullDocumentNumber, isValid)
121176
}
122177

123178
private func isValidMRZResult(_ result: QKMRZResult) -> Bool {
@@ -131,59 +186,55 @@ struct LiveMRZScannerView: View {
131186
onScanResultAsDict?(mapVisionResultToDictionary(result))
132187
}
133188

189+
/// Processes Belgian ID documents by manually extracting and validating the document number using the overflow format handler,
190+
/// then parses the remaining MRZ fields (name, dates, etc.) using QKMRZParser. This bypasses QKMRZParser's validation for the
191+
/// document number field since it doesn't handle Belgian overflow format correctly.
134192
private func processBelgiumDocument(result: String, parser: QKMRZParser) -> QKMRZResult? {
135-
print("[LiveMRZScannerView] Processing Belgium document")
193+
print("[LiveMRZScannerView] Processing Belgium document with manual validation")
136194

137-
guard let correctedBelgiumLine = correctBelgiumDocumentNumber(result: result) else {
138-
print("[LiveMRZScannerView] Failed to correct Belgium document number")
139-
return nil
140-
}
141-
142-
// print("[LiveMRZScannerView] Belgium corrected line: \(correctedBelgiumLine)")
143-
144-
// Split MRZ into lines and replace the first line
145195
let lines = result.components(separatedBy: "\n")
146196
guard lines.count >= 3 else {
147197
print("[LiveMRZScannerView] Invalid MRZ format - not enough lines")
148198
return nil
149199
}
150200

151-
let originalFirstLine = lines[0]
152-
// print("[LiveMRZScannerView] Original first line: \(originalFirstLine)")
201+
let line1 = lines[0]
202+
print("[LiveMRZScannerView] Line 1: \(line1)")
153203

154-
// Pad the corrected line to 30 characters (TD1 format)
155-
let paddedCorrectedLine = correctedBelgiumLine.padding(toLength: 30, withPad: "<", startingAt: 0)
156-
// print("[LiveMRZScannerView] Padded corrected line: \(paddedCorrectedLine)")
157-
158-
// Reconstruct the MRZ with the corrected first line
159-
var correctedLines = lines
160-
correctedLines[0] = paddedCorrectedLine
161-
let correctedMRZString = correctedLines.joined(separator: "\n")
162-
// print("[LiveMRZScannerView] Corrected MRZ string: \(correctedMRZString)")
163-
164-
guard let belgiumMRZResult = parser.parse(mrzString: correctedMRZString) else {
165-
print("[LiveMRZScannerView] Belgium MRZ result is not valid")
204+
// extracting and validating document number manually using overflow format handler
205+
guard let (documentNumber, isDocNumberValid) = extractAndValidateBelgianDocumentNumber(line1: line1) else {
206+
print("[LiveMRZScannerView] Failed to extract Belgian document number")
166207
return nil
167208
}
168209

169-
// print("[LiveMRZScannerView] Belgium MRZ result: \(belgiumMRZResult)")
170-
171-
// Try the corrected MRZ first
172-
if isValidMRZResult(belgiumMRZResult) {
173-
return belgiumMRZResult
210+
if !isDocNumberValid {
211+
print("[LiveMRZScannerView] Belgian document number check digit is INVALID")
212+
return nil
174213
}
175214

176-
// If document number is still invalid, try single character correction
177-
if !belgiumMRZResult.isDocumentNumberValid {
178-
if let correctedResult = singleCorrectDocumentNumberInMRZ(result: correctedMRZString, docNumber: belgiumMRZResult.documentNumber, parser: parser) {
179-
// print("[LiveMRZScannerView] Single correction successful: \(correctedResult)")
180-
if isValidMRZResult(correctedResult) {
181-
return correctedResult
182-
}
183-
}
215+
print("[LiveMRZScannerView] Belgian document number validated: \(documentNumber)")
216+
217+
// parsing the original MRZ to get all other fields (name, birthdate, etc.)
218+
// using QKMRZParser for non-documentNumber fields
219+
guard let mrzResult = parser.parse(mrzString: result) else {
220+
print("[LiveMRZScannerView] Failed to parse MRZ with QKMRZParser")
221+
return nil
184222
}
185223

186-
return nil
224+
print("[LiveMRZScannerView] QKMRZParser extracted fields:")
225+
print(" countryCode: \(mrzResult.countryCode)")
226+
print(" surnames: \(mrzResult.surnames)")
227+
print(" givenNames: \(mrzResult.givenNames)")
228+
print(" birthdate: \(mrzResult.birthdate?.description ?? "nil")")
229+
print(" sex: \(mrzResult.sex ?? "nil")")
230+
print(" expiryDate: \(mrzResult.expiryDate?.description ?? "nil")")
231+
print(" personalNumber: \(mrzResult.personalNumber)")
232+
print(" Parser's documentNumber: \(mrzResult.documentNumber)")
233+
print(" Our validated documentNumber: \(documentNumber)")
234+
235+
// returning MRZ result with manually validated document number
236+
// note: accepting the parser result for other fields (birthdate, expiry) as they should be correct
237+
return mrzResult
187238
}
188239

189240
var body: some View {

0 commit comments

Comments
 (0)