Files
self/app/ios/MRZScanner.swift
Eric Nakagawa 4d4efffe5a Apply BSL to app codebase (#639)
* Clean up root license wording

* Simplify SPDX header

* simplify license and rename BSL to BUSL

* fix merge issues

* fix missing method

---------

Co-authored-by: Justin Hernandez <transphorm@gmail.com>
2025-06-23 21:47:53 -07:00

102 lines
4.1 KiB
Swift

// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11
//
// MRZScanner.swift
import Vision
import UIKit
struct MRZScanner {
static func scan(image: UIImage, roi: CGRect? = nil, completion: @escaping (String, [CGRect]) -> Void) {
guard let cgImage = image.cgImage else {
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")
completion("No text found", [])
return
}
// print("Found \(observations.count) text observations")
var mrzLines: [String] = []
var boxes: [CGRect] = []
// Sort lines from top to bottom
let sortedObservations = observations.sorted { $0.boundingBox.minY > $1.boundingBox.minY }
for (index, obs) in sortedObservations.enumerated() {
if let candidate = obs.topCandidates(1).first {
let text = candidate.string
let confidence = candidate.confidence
// print("Line \(index): '\(text)' (confidence: \(confidence), position: \(obs.boundingBox))")
// Check if this looks like an MRZ line (either contains "<" or matches MRZ pattern)
// TD1 format (ID cards): 30 chars, TD3 format (passports): 44 chars
if text.contains("<") ||
text.matches(pattern: "^[A-Z0-9<]{30}$") || //TD1 //case where there's no '<' in MRZ
text.matches(pattern: "^[A-Z0-9<]{44}$") //TD3
{
// print("Matched MRZ pattern: \(text)")
mrzLines.append(text)
boxes.append(obs.boundingBox)
// Check if we have a complete MRZ
if (mrzLines.count == 2 && mrzLines.allSatisfy { $0.count == 44 }) || // TD3 - passport
(mrzLines.count == 3 && mrzLines.allSatisfy { $0.count == 30 }) { // TD1 - ID card
break
}
} else {
// print("Did not match MRZ pattern: \(text)")
}
}
}
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"]
// Use provided ROI. If not use as bottom 20%
if let roi = roi {
// print("[MRZScanner] Using provided ROI: \(roi) (image size: \(cgImage.width)x\(cgImage.height))")
request.regionOfInterest = roi
} else {
let imageHeight = CGFloat(cgImage.height)
let roiHeight = imageHeight * 0.2 // Bottom 20%
let defaultRoi = CGRect(x: 0, y: 0, width: 1.0, height: roiHeight / imageHeight)
// print("[MRZScanner] Using default ROI: \(defaultRoi) (image size: \(cgImage.width)x\(cgImage.height), roi height: \(roiHeight))")
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)")
}
}
}
}
extension String {
func matches(pattern: String) -> Bool {
return range(of: pattern, options: .regularExpression) != nil
}
}