mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
* 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>
102 lines
4.1 KiB
Swift
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
|
|
}
|
|
}
|