@@ -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