Files
self/app/ios/MrzScanEngine.swift
Justin Hernandez 88e28733b1 Phase 1 Native Consolidation: Extract MRZ OCR/Mapping Helpers and Thin Scanner Views (#1823)
* mrz consolidation

* consolidate mrz swift code

* update specs

* save changes

* fixes

* format

* update spec

* pr feedback

* add tests

* save prep work

* consolidate MRZ handling logic

* fixes

* format

* format

* remove giga

* pr feedback

* update spec

* update

* revert change

* pr feedback

* updates
2026-03-06 17:54:46 -08:00

100 lines
3.5 KiB
Swift

// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import Vision
import UIKit
struct MrzScanEngine {
static func scan(image: UIImage, roi: CGRect? = nil, completion: @escaping (String, [CGRect]) -> Void) {
guard let cgImage = image.cgImage else {
DispatchQueue.main.async {
completion("Image not valid", [])
}
return
}
let request = VNRecognizeTextRequest { (request, error) in
if let error = error {
print("Vision error: \(error)")
}
guard let observations = request.results as? [VNRecognizedTextObservation] else {
print("No text observations found")
DispatchQueue.main.async {
completion("No text found", [])
}
return
}
var mrzLines: [String] = []
var boxes: [CGRect] = []
let sortedObservations = observations.sorted { $0.boundingBox.minY > $1.boundingBox.minY }
for (_, obs) in sortedObservations.enumerated() {
if let candidate = obs.topCandidates(1).first {
let text = candidate.string
// TD1 format (ID cards): 30 chars, TD3 format (passports): 44 chars
if text.matches(pattern: "^[A-Z0-9<]{30}$") ||
text.matches(pattern: "^[A-Z0-9<]{44}$") {
if let currentWidth = mrzLines.first?.count, currentWidth != text.count {
mrzLines = []
boxes = []
}
mrzLines.append(text)
boxes.append(obs.boundingBox)
if (mrzLines.count == 2 && mrzLines.allSatisfy { $0.count == 44 }) ||
(mrzLines.count == 3 && mrzLines.allSatisfy { $0.count == 30 }) {
break
}
}
}
}
DispatchQueue.main.async {
if mrzLines.isEmpty {
print("No MRZ lines found")
completion("", [])
} else {
print("Found \(mrzLines.count) MRZ lines")
completion(mrzLines.joined(separator: "\n"), boxes)
}
}
}
request.recognitionLevel = .accurate
request.usesLanguageCorrection = false
request.recognitionLanguages = ["en"]
if let roi = roi {
request.regionOfInterest = roi
} else {
let imageHeight = CGFloat(cgImage.height)
let roiHeight = imageHeight * 0.2
let defaultRoi = CGRect(x: 0, y: 0, width: 1.0, height: roiHeight / imageHeight)
request.regionOfInterest = defaultRoi
}
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
DispatchQueue.global(qos: .userInitiated).async {
do {
try handler.perform([request])
} catch {
print("Failed to perform recognition: \(error)")
DispatchQueue.main.async {
completion("Failed to perform recognition: \(error)", [])
}
}
}
}
}
extension String {
func matches(pattern: String) -> Bool {
return range(of: pattern, options: .regularExpression) != nil
}
}