Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor MRZScanner & Example app #5

Merged
merged 4 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/Build and test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ on:
jobs:
build:

runs-on: macos-13
runs-on: macos-14

steps:
- uses: actions/checkout@v3
- name: Select Xcode version
run: sudo xcode-select -s '/Applications/Xcode_16.0.app/Contents/Developer'
- name: Run tests
run: swift test --enable-code-coverage
2 changes: 1 addition & 1 deletion .swiftpm/xcode/xcshareddata/xcschemes/MRZScanner.xcscheme
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
10 changes: 7 additions & 3 deletions Example/MRZScannerExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1420;
LastUpgradeCheck = 1600;
TargetAttributes = {
0E472C71295D23770033AA9E = {
CreatedOnToolsVersion = 14.2;
Expand Down Expand Up @@ -188,6 +188,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
Expand Down Expand Up @@ -221,6 +222,7 @@
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
Expand All @@ -247,6 +249,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
Expand Down Expand Up @@ -280,6 +283,7 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
Expand Down Expand Up @@ -321,7 +325,7 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.1;
Expand Down Expand Up @@ -365,7 +369,7 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/romanmazeev/MRZParser.git",
"state" : {
"branch" : "master",
"revision" : "a39f93e35e4d8de2dceeb6103ca4089856e3c566"
"revision" : "f6a0b10395e7cac783189ef2a0c8e1a55296ff37",
"version" : "1.1.4"
}
},
{
Expand All @@ -36,13 +36,22 @@
"version" : "1.1.0"
}
},
{
"identity" : "swift-custom-dump",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-custom-dump.git",
"state" : {
"revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c",
"version" : "1.3.0"
}
},
{
"identity" : "swift-dependencies",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies",
"state" : {
"revision" : "9783b58167f7618cb86011156e741cbc6f4cc864",
"version" : "1.1.2"
"revision" : "00bc30ca03f98881329fab7f1bebef8eba472596",
"version" : "1.3.1"
}
},
{
Expand All @@ -59,8 +68,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
"state" : {
"revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631",
"version" : "1.0.2"
"revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2",
"version" : "1.1.2"
}
}
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
82 changes: 22 additions & 60 deletions Example/MRZScannerExample/Camera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,86 +5,53 @@
// Created by Roman Mazeev on 01/01/2023.
//

import AVFoundation
@preconcurrency import AVFoundation
import CoreImage

final class Camera: NSObject {
let captureSession = AVCaptureSession()
actor Camera {
nonisolated let captureSession = AVCaptureSession()
private let outputSampleBufferDelegate = OutputSampleBufferDelegate()

var imageStream: AsyncStream<CIImage> {
AsyncStream { continuation in
imageStreamCallback = { ciImage in
continuation.yield(ciImage)
}
var imageStream: AsyncStream<CIImage>? {
.init { continuation in
outputSampleBufferDelegate.continuation = continuation
}
}

private var imageStreamCallback: ((CIImage) -> Void)?
private let captureDevice = AVCaptureDevice.default(for: .video)

// TODO: Refactor to use Swift Concurrency
private let sessionQueue = DispatchQueue(label: "Session queue")

private var isCaptureSessionConfigured = false
private var deviceInput: AVCaptureDeviceInput?
private var videoOutput: AVCaptureVideoDataOutput?

func start() async {
let authorized = await checkAuthorization()
guard authorized else {
func startCamera() async throws {
guard await checkAuthorization() else {
fatalError("You need to give access")
}

if isCaptureSessionConfigured {
if !captureSession.isRunning {
sessionQueue.async { [self] in
self.captureSession.startRunning()
}
}
return
}

sessionQueue.async { [self] in
try? self.configureCaptureSession { success in
guard success else { return }
self.captureSession.startRunning()
}
}
try await configureCaptureSession()
}

private func checkAuthorization() async -> Bool {
func requestCameraAccess() async -> Bool {
sessionQueue.suspend()
let status = await AVCaptureDevice.requestAccess(for: .video)
sessionQueue.resume()
return status
}

switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
return true
case .notDetermined, .restricted, .denied:
return await requestCameraAccess()
return await AVCaptureDevice.requestAccess(for: .video)
@unknown default:
return false
}
}

private func configureCaptureSession(completionHandler: (_ success: Bool) -> Void) throws {
var success = false

private func configureCaptureSession() async throws {
self.captureSession.beginConfiguration()

defer {
self.captureSession.commitConfiguration()
completionHandler(success)
if !captureSession.isRunning {
captureSession.startRunning()
}
}

guard let captureDevice else { fatalError("Unable to create capture device") }
guard let captureDevice = AVCaptureDevice.default(for: .video) else { fatalError("Unable to create capture device") }
let deviceInput = try AVCaptureDeviceInput(device: captureDevice)

let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: .init(label: "VideoDataOutput queue"))
videoOutput.setSampleBufferDelegate(outputSampleBufferDelegate, queue: .init(label: "com.MRZScannerExample.OutputSampleBufferDelegate"))

guard captureSession.canAddInput(deviceInput) else {
fatalError("Unable to add device input to capture session.")
Expand All @@ -97,21 +64,16 @@ final class Camera: NSObject {
captureSession.addInput(deviceInput)
captureSession.addOutput(videoOutput)

self.deviceInput = deviceInput
self.videoOutput = videoOutput

videoOutput.connection(with: .video)?.videoOrientation = .portrait

isCaptureSessionConfigured = true

success = true
videoOutput.connection(with: .video)?.videoRotationAngle = 90
}
}

extension Camera: AVCaptureVideoDataOutputSampleBufferDelegate {
final private class OutputSampleBufferDelegate: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
var continuation: AsyncStream<CIImage>.Continuation?

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let pixelBuffer = sampleBuffer.imageBuffer else { return }

imageStreamCallback?(CIImage(cvPixelBuffer: pixelBuffer))
continuation?.yield(CIImage(cvPixelBuffer: pixelBuffer))
}
}
2 changes: 1 addition & 1 deletion Example/MRZScannerExample/CameraView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ final class CameraViewController: UIViewController {
override func viewDidLoad() {
previewLayer.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.connection?.videoOrientation = .portrait
previewLayer.connection?.videoRotationAngle = 90
view.layer.addSublayer(previewLayer)
}
}
Expand Down
74 changes: 40 additions & 34 deletions Example/MRZScannerExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,10 @@ struct ContentView: View {
size: .init(width: proxy.size.width - 40, height: proxy.size.width / 5))
}
}
.alert(isPresented: .init(get: { viewModel.mrzResult != nil }, set: { _ in viewModel.mrzResult = nil })) {
.alert(isPresented: .init(get: { viewModel.result != nil }, set: { _ in viewModel.result = nil })) {
Alert(
title: Text("Important message"),
message: Text(createAlertMessage(mrzResult: viewModel.mrzResult!)),
dismissButton: .default(Text("Got it!")) {
Task {
guard let cameraRect, let mrzRect else { return }

await viewModel.startMRZScanning(cameraRect: cameraRect, mrzRect: mrzRect)
}
}
title: Text(createAlertTitle(result: viewModel.result!)),
message: Text(createAlertMessage(result: viewModel.result!))
)
}
.task {
Expand All @@ -88,35 +81,49 @@ struct ContentView: View {
.position(rect.origin)
}

private func createAlertMessage(mrzResult: ParserResult) -> String {
var birthdateString: String?
var expiryDateString: String?

if let birthdate = mrzResult.birthdate {
birthdateString = dateFormatter.string(from: birthdate)
private func createAlertTitle(result: Result<ParserResult, Error>) -> String {
switch result {
case .success:
return "Scanned successfully"
case .failure:
return "Error"
}
}

if let expiryDate = mrzResult.expiryDate {
expiryDateString = dateFormatter.string(from: expiryDate)
}
private func createAlertMessage(result: Result<ParserResult, Error>) -> String {
switch result {
case .success(let mrzResult):
var birthdateString: String?
var expiryDateString: String?

return """
Document type: \(mrzResult.documentType)
Country code: \(mrzResult.countryCode)
Surnames: \(mrzResult.surnames)
Given names: \(mrzResult.givenNames)
Document number: \(mrzResult.documentNumber ?? "-")
nationalityCountryCode: \(mrzResult.nationalityCountryCode)
birthdate: \(birthdateString ?? "-")
sex: \(mrzResult.sex)
expiryDate: \(expiryDateString ?? "-")
personalNumber: \(mrzResult.optionalData ?? "-")
personalNumber2: \(mrzResult.optionalData2 ?? "-")
"""
if let birthdate = mrzResult.birthdate {
birthdateString = dateFormatter.string(from: birthdate)
}

if let expiryDate = mrzResult.expiryDate {
expiryDateString = dateFormatter.string(from: expiryDate)
}

return """
Document type: \(mrzResult.documentType)
Country code: \(mrzResult.countryCode)
Surnames: \(mrzResult.surnames)
Given names: \(mrzResult.givenNames)
Document number: \(mrzResult.documentNumber ?? "-")
nationalityCountryCode: \(mrzResult.nationalityCountryCode)
birthdate: \(birthdateString ?? "-")
sex: \(mrzResult.sex)
expiryDate: \(expiryDateString ?? "-")
personalNumber: \(mrzResult.optionalData ?? "-")
personalNumber2: \(mrzResult.optionalData2 ?? "-")
"""
case .failure(let error):
return error.localizedDescription
}
}
}

extension CGRect: Hashable {
extension CGRect: @retroactive Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(origin.x)
hasher.combine(origin.y)
Expand All @@ -125,7 +132,6 @@ extension CGRect: Hashable {
}
}


struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
Expand Down
Loading
Loading