diff --git a/.github/workflows/Build and test framework.yml b/.github/workflows/Build and test framework.yml new file mode 100644 index 0000000..fd98aaf --- /dev/null +++ b/.github/workflows/Build and test framework.yml @@ -0,0 +1,17 @@ +name: Build and test + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + - name: Build and test + run: swift test -v --enable-code-coverage diff --git a/.github/workflows/Build and test.yml b/.github/workflows/Build and test.yml deleted file mode 100644 index 35cb8e5..0000000 --- a/.github/workflows/Build and test.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Build and test - -on: - push: - branches: [ develop ] - pull_request: - branches: [ develop ] - -jobs: - build: - - runs-on: macos-latest - - steps: - - uses: actions/checkout@v2 - - name: Build and test - run: swift test -v --enable-code-coverage - - name: Test coverage - uses: maxep/spm-lcov-action@0.3.0 - with: - output-file: ./coverage/lcov.info - - name: Codecov - uses: codecov/codecov-action@v2.0.1 diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/MRZScanner.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/MRZScanner.xcscheme index 1d2b8e0..d4b3139 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/MRZScanner.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/MRZScanner.xcscheme @@ -1,6 +1,6 @@ + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/MRZScannerTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/MRZScannerTests.xcscheme index 36f7f17..6d77ca8 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/MRZScannerTests.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/MRZScannerTests.xcscheme @@ -1,6 +1,6 @@ Bool { true } -} diff --git a/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 9221b9b..0000000 --- a/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Example/Example/Base.lproj/LaunchScreen.storyboard b/Example/Example/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e932..0000000 --- a/Example/Example/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Example/Example/Base.lproj/Main.storyboard b/Example/Example/Base.lproj/Main.storyboard deleted file mode 100644 index 25a7638..0000000 --- a/Example/Example/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Example/Example/Info.plist b/Example/Example/Info.plist deleted file mode 100644 index 00d9d92..0000000 --- a/Example/Example/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - NSCameraUsageDescription - To work, you need to give access to the camera. - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/Example/Example/PreviewView.swift b/Example/Example/PreviewView.swift deleted file mode 100644 index 1d7b4f0..0000000 --- a/Example/Example/PreviewView.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// PreviewView.swift -// Example -// -// Created by Roman Mazeev on 28.06.2021. -// - -import UIKit -import AVFoundation - -class PreviewView: UIView { - var videoPreviewLayer: AVCaptureVideoPreviewLayer { - guard let layer = layer as? AVCaptureVideoPreviewLayer else { - fatalError(""" - Expected `AVCaptureVideoPreviewLayer` type for layer. - Check PreviewView.layerClass implementation. - """) - } - - return layer - } - - var session: AVCaptureSession? { - get { videoPreviewLayer.session } - set { videoPreviewLayer.session = newValue } - } - - // MARK: UIView - - override class var layerClass: AnyClass { - AVCaptureVideoPreviewLayer.self - } -} diff --git a/Example/Example/ViewController.swift b/Example/Example/ViewController.swift deleted file mode 100644 index 3435fd3..0000000 --- a/Example/Example/ViewController.swift +++ /dev/null @@ -1,441 +0,0 @@ -// -// ViewController.swift -// Example -// -// Created by Roman Mazeev on 28.06.2021. -// - -import UIKit -import AVFoundation -import MRZScanner - -class ViewController: UIViewController { - // MARK: UI objects - private let previewView = PreviewView() - private let cutoutView = UIView() - private let maskLayer = CAShapeLayer() - - // MARK: Scanning related - private let scanner = LiveMRZScanner() - private var scanningIsEnabled = true { - didSet { - scanningIsEnabled ? captureSession.startRunning() : captureSession.stopRunning() - - if !scanningIsEnabled { - removeBoxes() - } - } - } - - private let dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeStyle = .none - return formatter - }() - - // MARK: Capture related objects - private let captureSession = AVCaptureSession() - private let captureSessionQueue = DispatchQueue(label: "com.aita.mrzExample.captureSessionQueue") - - private var captureDevice: AVCaptureDevice? - - private let videoDataOutput = AVCaptureVideoDataOutput() - private let videoDataOutputQueue = DispatchQueue(label: "com.aita.mrzExample.videoDataOutputQueue") - - /// Device orientation. Updated whenever the orientation changes to a different supported orientation. - private var currentOrientation = UIDeviceOrientation.portrait - - private func checkCaptureDeviceAuthorization() { - switch AVCaptureDevice.authorizationStatus(for: .video) { - case .authorized: - return - default: - AVCaptureDevice.requestAccess(for: .video) { granted in - guard granted else { fatalError("To work, you need to give access to the camera.") } - return - } - } - } - - // MARK: Region of interest (ROI) and text orientation - // Region of video data output buffer that recognition should be run on. - // Gets recalculated once the bounds of the preview layer are known. - var regionOfInterest = CGRect(x: 0, y: 0, width: 1, height: 1) - // Orientation of text to search for in the region of interest. - var textOrientation = CGImagePropertyOrientation.up - - // MARK: Coordinate transforms - var bufferAspectRatio: Double! - // Transform from UI orientation to buffer orientation. - var uiRotationTransform = CGAffineTransform.identity - // Transform bottom-left coordinates to top-left. - var bottomToTopTransform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -1) - // Transform coordinates in ROI to global coordinates (still normalized). - var roiToGlobalTransform = CGAffineTransform.identity - - // Vision -> AVF coordinate transform. - var visionToAVFTransform = CGAffineTransform.identity - - // MARK: View controller methods - - override func viewDidLoad() { - super.viewDidLoad() - setup() - - checkCaptureDeviceAuthorization() - - // Set up preview view. - previewView.session = captureSession - previewView.videoPreviewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill - - // Set up cutout view. - cutoutView.backgroundColor = UIColor.gray.withAlphaComponent(0.5) - maskLayer.backgroundColor = UIColor.clear.cgColor - maskLayer.fillRule = .evenOdd - cutoutView.layer.mask = maskLayer - - // Starting the capture session is a blocking call. Perform setup using - // a dedicated serial dispatch queue to prevent blocking the main thread. - captureSessionQueue.async { - self.setupCamera() - - // Calculate region of interest now that the camera is setup. - DispatchQueue.main.async { - // Figure out initial ROI. - self.calculateRegionOfInterest() - } - } - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - - // Only change the current orientation if the new one is landscape or - // portrait. You can't really do anything about flat or unknown. - let deviceOrientation = UIDevice.current.orientation - if deviceOrientation.isPortrait || deviceOrientation.isLandscape { - currentOrientation = deviceOrientation - } - - // Handle device orientation in the preview layer. - if let videoPreviewLayerConnection = previewView.videoPreviewLayer.connection { - if let newVideoOrientation = AVCaptureVideoOrientation(deviceOrientation: deviceOrientation) { - videoPreviewLayerConnection.videoOrientation = newVideoOrientation - } - } - - // Orientation changed: figure out new region of interest (ROI). - calculateRegionOfInterest() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - updateCutout() - } - - // MARK: Setup - private func setup() { - view.insetsLayoutMarginsFromSafeArea = false - previewView.translatesAutoresizingMaskIntoConstraints = false - cutoutView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(previewView) - view.addSubview(cutoutView) - - NSLayoutConstraint.activate([ - previewView.topAnchor.constraint(equalTo: view.topAnchor), - previewView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - previewView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - previewView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - - cutoutView.topAnchor.constraint(equalTo: view.topAnchor), - cutoutView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - cutoutView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - cutoutView.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - } - - private func calculateRegionOfInterest() { - // In landscape orientation the desired ROI is specified as the ratio of - // buffer width to height. When the UI is rotated to portrait, keep the - // vertical size the same (in buffer pixels). Also try to keep the - // horizontal size the same up to a maximum ratio. - let desiredHeightRatio = 0.15 - let desiredWidthRatio = 0.6 - let maxPortraitWidth = 0.8 - - // Figure out size of ROI. - let size: CGSize - if currentOrientation.isPortrait || currentOrientation == .unknown { - size = CGSize(width: min(desiredWidthRatio * bufferAspectRatio, maxPortraitWidth), - height: desiredHeightRatio / bufferAspectRatio) - } else { - size = CGSize(width: desiredWidthRatio, height: desiredHeightRatio) - } - // Make it centered. - regionOfInterest.origin = CGPoint(x: (1 - size.width) / 2, y: (1 - size.height) / 2) - regionOfInterest.size = size - - // ROI changed, update transform. - setupOrientationAndTransform() - - // Update the cutout to match the new ROI. - DispatchQueue.main.async { - // Wait for the next run cycle before updating the cutout. This - // ensures that the preview layer already has its new orientation. - self.updateCutout() - } - } - - private func updateCutout() { - // Figure out where the cutout ends up in layer coordinates. - let roiRectTransform = bottomToTopTransform.concatenating(uiRotationTransform) - let cutout = previewView.videoPreviewLayer.layerRectConverted( - fromMetadataOutputRect: regionOfInterest - .applying(roiRectTransform) - ) - - // Create the mask. - let path = UIBezierPath(rect: cutoutView.frame) - path.append(UIBezierPath(rect: cutout)) - maskLayer.path = path.cgPath - } - - private func setupOrientationAndTransform() { - // Recalculate the affine transform between Vision coordinates and AVF coordinates. - - // Compensate for region of interest. - let roi = regionOfInterest - roiToGlobalTransform = CGAffineTransform(translationX: roi.origin.x, - y: roi.origin.y) - .scaledBy(x: roi.width, y: roi.height) - - // Compensate for orientation (buffers always come in the same orientation). - switch currentOrientation { - case .landscapeLeft: - textOrientation = CGImagePropertyOrientation.up - uiRotationTransform = CGAffineTransform.identity - case .landscapeRight: - textOrientation = CGImagePropertyOrientation.down - uiRotationTransform = CGAffineTransform(translationX: 1, y: 1).rotated(by: CGFloat.pi) - case .portraitUpsideDown: - textOrientation = CGImagePropertyOrientation.left - uiRotationTransform = CGAffineTransform(translationX: 1, y: 0).rotated(by: CGFloat.pi / 2) - default: // We default everything else to .portraitUp - textOrientation = CGImagePropertyOrientation.right - uiRotationTransform = CGAffineTransform(translationX: 0, y: 1).rotated(by: -CGFloat.pi / 2) - } - - // Full Vision ROI to AVF transform. - visionToAVFTransform = roiToGlobalTransform - .concatenating(bottomToTopTransform) - .concatenating(uiRotationTransform) - } - - private func setupCamera() { - guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, - for: AVMediaType.video, - position: .back) else { - fatalError("Could not create capture device.") - } - self.captureDevice = captureDevice - - // NOTE: - // Requesting 4k buffers allows recognition of smaller text but will - // consume more power. Use the smallest buffer size necessary to keep - // down battery usage. - if captureDevice.supportsSessionPreset(.hd4K3840x2160) { - captureSession.sessionPreset = AVCaptureSession.Preset.hd4K3840x2160 - bufferAspectRatio = 3840.0 / 2160.0 - } else { - captureSession.sessionPreset = AVCaptureSession.Preset.hd1920x1080 - bufferAspectRatio = 1920.0 / 1080.0 - } - - guard let deviceInput = try? AVCaptureDeviceInput(device: captureDevice) else { - fatalError("Could not create device input.") - } - - if captureSession.canAddInput(deviceInput) { - captureSession.addInput(deviceInput) - } - - // Configure video data output. - videoDataOutput.alwaysDiscardsLateVideoFrames = true - videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue) - videoDataOutput.videoSettings = - [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] - if captureSession.canAddOutput(videoDataOutput) { - captureSession.addOutput(videoDataOutput) - // NOTE: - // There is a trade-off to be made here. Enabling stabilization will - // give temporally more stable results and should help the recognizer - // converge. But if it's enabled the VideoDataOutput buffers don't - // match what's displayed on screen, which makes drawing bounding - // boxes very hard. Disable it in this app to allow drawing detected - // bounding boxes on screen. - videoDataOutput.connection(with: AVMediaType.video)?.preferredVideoStabilizationMode = .off - } else { - fatalError("Could not add VDO output") - } - - // Set zoom and autofocus to help focus on very small text. - do { - try captureDevice.lockForConfiguration() - captureDevice.videoZoomFactor = 1.5 - captureDevice.autoFocusRangeRestriction = .near - captureDevice.unlockForConfiguration() - } catch { - fatalError("Could not set zoom level due to error: \(error)") - } - - captureSession.startRunning() - } - - // MARK: Bounding box drawing - - private func showBoundingRects(valid validRects: [CGRect], invalid invalidRects: [CGRect]) { - let layer = self.previewView.videoPreviewLayer - self.removeBoxes() - - for rect in invalidRects { - let rect = layer.layerRectConverted(fromMetadataOutputRect: rect.applying(visionToAVFTransform)) - self.draw(rect: rect, color: UIColor.red.cgColor) - } - for rect in validRects { - let rect = layer.layerRectConverted(fromMetadataOutputRect: rect.applying(visionToAVFTransform)) - self.draw(rect: rect, color: UIColor.green.cgColor) - } - } - - // Draw a box on screen. Must be called from main queue. - private var boxLayer = [CAShapeLayer]() - private func draw(rect: CGRect, color: CGColor) { - let layer = CAShapeLayer() - layer.opacity = 0.5 - layer.borderColor = color - layer.borderWidth = 1 - layer.frame = rect - boxLayer.append(layer) - previewView.videoPreviewLayer.insertSublayer(layer, at: 1) - } - - // Remove all drawn boxes. Must be called on main queue. - private func removeBoxes() { - for layer in boxLayer { - layer.removeFromSuperlayer() - } - boxLayer.removeAll() - } - - // MARK: Alert displaying - - private func displayError(_ error: Error) { - let alertController = UIAlertController( - title: "Can't read MRZ code", - message: error.localizedDescription, - preferredStyle: .alert - ) - - addAlertActionAndPresent(alertController) - } - - private func displayScanningResult(_ result: ParsedResult) { - var birthdateString: String? - var expiryDateString: String? - - if let birthdate = result.birthdate { - birthdateString = dateFormatter.string(from: birthdate) - } - - if let expiryDate = result.expiryDate { - expiryDateString = dateFormatter.string(from: expiryDate) - } - - let alertText = """ - documentType: \(result.documentType) - countryCode: \(result.countryCode) - surnames: \(result.surnames) - givenNames: \(result.givenNames) - documentNumber: \(result.documentNumber ?? "-") - nationalityCountryCode: \(result.nationalityCountryCode) - birthdate: \(birthdateString ?? "-") - sex: \(result.sex) - expiryDate: \(expiryDateString ?? "-") - personalNumber: \(result.optionalData ?? "-") - personalNumber2: \(result.optionalData2 ?? "-") - """ - - let alertController = UIAlertController( - title: "MRZ scanned", - message: alertText, - preferredStyle: .alert - ) - - addAlertActionAndPresent(alertController) - } - - private func addAlertActionAndPresent(_ alertController: UIAlertController) { - alertController.addAction(.init(title: "OK", style: .cancel, handler: { [ unowned self ] _ in - self.scanningIsEnabled = true - })) - - if scanningIsEnabled { - present(alertController, animated: true) - scanningIsEnabled = false - } - } -} - -// MARK: - AVCaptureVideoDataOutputSampleBufferDelegate - -extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate { - func captureOutput( - _ output: AVCaptureOutput, - didOutput sampleBuffer: CMSampleBuffer, - from connection: AVCaptureConnection - ) { - if let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), scanningIsEnabled { - scanner.scanFrame( - scanningImage: .pixelBuffer(pixelBuffer), - orientation: textOrientation, - regionOfInterest: regionOfInterest, - minimumTextHeight: 0.1, - foundBoundingRectsHandler: { [weak self] in - self?.showBoundingRects(valid: [], invalid: $0) - }, - completionHandler: { [weak self] result in - switch result { - case .success(let scanningResult): - self?.displayScanningResult(scanningResult.result) - self?.showBoundingRects( - valid: scanningResult.boundingRects.valid, - invalid: scanningResult.boundingRects.invalid - ) - case .failure(let error): - self?.displayError(error) - } - } - ) - } - } -} - -// MARK: - Utility extensions - -extension AVCaptureVideoOrientation { - init?(deviceOrientation: UIDeviceOrientation) { - switch deviceOrientation { - case .portrait: - self = .portrait - case .portraitUpsideDown: - self = .portraitUpsideDown - case .landscapeLeft: - self = .landscapeRight - case .landscapeRight: - self = .landscapeLeft - default: - return nil - } - } -} diff --git a/Example/MRZScannerExample.xcodeproj/project.pbxproj b/Example/MRZScannerExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..654d061 --- /dev/null +++ b/Example/MRZScannerExample.xcodeproj/project.pbxproj @@ -0,0 +1,417 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 0E67B46229628DBA00ECFD84 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E67B45829628DBA00ECFD84 /* ExampleApp.swift */; }; + 0E67B46329628DBA00ECFD84 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E67B45929628DBA00ECFD84 /* Assets.xcassets */; }; + 0E67B46429628DBA00ECFD84 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E67B45A29628DBA00ECFD84 /* ViewModel.swift */; }; + 0E67B46529628DBA00ECFD84 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E67B45C29628DBA00ECFD84 /* Preview Assets.xcassets */; }; + 0E67B46629628DBA00ECFD84 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E67B45D29628DBA00ECFD84 /* CameraView.swift */; }; + 0E67B46729628DBA00ECFD84 /* Camera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E67B45E29628DBA00ECFD84 /* Camera.swift */; }; + 0E67B46829628DBA00ECFD84 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E67B45F29628DBA00ECFD84 /* ContentView.swift */; }; + 0E816B8D295DA114001DB8C5 /* MRZScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 0E816B8C295DA114001DB8C5 /* MRZScanner */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0E472C72295D23770033AA9E /* MRZScannerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MRZScannerExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0E472C87295D24320033AA9E /* MRZScanner */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MRZScanner; path = ..; sourceTree = ""; }; + 0E67B45829628DBA00ECFD84 /* ExampleApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; + 0E67B45929628DBA00ECFD84 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 0E67B45A29628DBA00ECFD84 /* ViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; + 0E67B45C29628DBA00ECFD84 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 0E67B45D29628DBA00ECFD84 /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; + 0E67B45E29628DBA00ECFD84 /* Camera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Camera.swift; sourceTree = ""; }; + 0E67B45F29628DBA00ECFD84 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 0E67B46029628DBA00ECFD84 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0E67B46129628DBA00ECFD84 /* Example.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0E472C6F295D23770033AA9E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0E816B8D295DA114001DB8C5 /* MRZScanner in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0E472C69295D23770033AA9E = { + isa = PBXGroup; + children = ( + 0E67B45729628DBA00ECFD84 /* MRZScannerExample */, + 0E472C86295D24320033AA9E /* Packages */, + 0E472C73295D23770033AA9E /* Products */, + 0E816B8B295DA109001DB8C5 /* Frameworks */, + ); + sourceTree = ""; + }; + 0E472C73295D23770033AA9E /* Products */ = { + isa = PBXGroup; + children = ( + 0E472C72295D23770033AA9E /* MRZScannerExample.app */, + ); + name = Products; + sourceTree = ""; + }; + 0E472C86295D24320033AA9E /* Packages */ = { + isa = PBXGroup; + children = ( + 0E472C87295D24320033AA9E /* MRZScanner */, + ); + name = Packages; + sourceTree = ""; + }; + 0E67B45729628DBA00ECFD84 /* MRZScannerExample */ = { + isa = PBXGroup; + children = ( + 0E67B45829628DBA00ECFD84 /* ExampleApp.swift */, + 0E67B45929628DBA00ECFD84 /* Assets.xcassets */, + 0E67B45A29628DBA00ECFD84 /* ViewModel.swift */, + 0E67B45B29628DBA00ECFD84 /* Preview Content */, + 0E67B45D29628DBA00ECFD84 /* CameraView.swift */, + 0E67B45E29628DBA00ECFD84 /* Camera.swift */, + 0E67B45F29628DBA00ECFD84 /* ContentView.swift */, + 0E67B46029628DBA00ECFD84 /* Info.plist */, + 0E67B46129628DBA00ECFD84 /* Example.entitlements */, + ); + path = MRZScannerExample; + sourceTree = ""; + }; + 0E67B45B29628DBA00ECFD84 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 0E67B45C29628DBA00ECFD84 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 0E816B8B295DA109001DB8C5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0E472C71295D23770033AA9E /* MRZScannerExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0E472C81295D23780033AA9E /* Build configuration list for PBXNativeTarget "MRZScannerExample" */; + buildPhases = ( + 0E472C6E295D23770033AA9E /* Sources */, + 0E472C6F295D23770033AA9E /* Frameworks */, + 0E472C70295D23770033AA9E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MRZScannerExample; + packageProductDependencies = ( + 0E816B8C295DA114001DB8C5 /* MRZScanner */, + ); + productName = Example; + productReference = 0E472C72295D23770033AA9E /* MRZScannerExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0E472C6A295D23770033AA9E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1420; + TargetAttributes = { + 0E472C71295D23770033AA9E = { + CreatedOnToolsVersion = 14.2; + }; + }; + }; + buildConfigurationList = 0E472C6D295D23770033AA9E /* Build configuration list for PBXProject "MRZScannerExample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 0E472C69295D23770033AA9E; + productRefGroup = 0E472C73295D23770033AA9E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0E472C71295D23770033AA9E /* MRZScannerExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0E472C70295D23770033AA9E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0E67B46529628DBA00ECFD84 /* Preview Assets.xcassets in Resources */, + 0E67B46329628DBA00ECFD84 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0E472C6E295D23770033AA9E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0E67B46729628DBA00ECFD84 /* Camera.swift in Sources */, + 0E67B46829628DBA00ECFD84 /* ContentView.swift in Sources */, + 0E67B46629628DBA00ECFD84 /* CameraView.swift in Sources */, + 0E67B46429628DBA00ECFD84 /* ViewModel.swift in Sources */, + 0E67B46229628DBA00ECFD84 /* ExampleApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 0E472C7F295D23780033AA9E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 0E472C80295D23780033AA9E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 0E472C82295D23780033AA9E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = MRZScannerExample/Example.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_ASSET_PATHS = "MRZScannerExample/Preview\\ Content/Preview\\ Assets.xcassets"; + DEVELOPMENT_TEAM = 2AB5X5S74A; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MRZScannerExample/Info.plist; + INFOPLIST_KEY_NSCameraUsageDescription = "Share camera to start scanning"; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 13.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = MRZScanner.Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 0E472C83295D23780033AA9E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = MRZScannerExample/Example.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_ASSET_PATHS = "MRZScannerExample/Preview\\ Content/Preview\\ Assets.xcassets"; + DEVELOPMENT_TEAM = 2AB5X5S74A; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MRZScannerExample/Info.plist; + INFOPLIST_KEY_NSCameraUsageDescription = "Share camera to start scanning"; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 13.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = MRZScanner.Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0E472C6D295D23770033AA9E /* Build configuration list for PBXProject "MRZScannerExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0E472C7F295D23780033AA9E /* Debug */, + 0E472C80295D23780033AA9E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0E472C81295D23780033AA9E /* Build configuration list for PBXNativeTarget "MRZScannerExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0E472C82295D23780033AA9E /* Debug */, + 0E472C83295D23780033AA9E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 0E816B8C295DA114001DB8C5 /* MRZScanner */ = { + isa = XCSwiftPackageProductDependency; + productName = MRZScanner; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 0E472C6A295D23770033AA9E /* Project object */; +} diff --git a/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/MRZScannerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Example/MRZScannerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/MRZScannerExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Example/MRZScannerExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Example/MRZScannerExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/MRZScannerExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..1d94759 --- /dev/null +++ b/Example/MRZScannerExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "mrzparser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/romanmazeev/MRZParser.git", + "state" : { + "revision" : "2c1c4809379f081c01297f0ac92df2e94c6db54a", + "version" : "1.1.3" + } + } + ], + "version" : 2 +} diff --git a/Example/MRZScannerExample.xcodeproj/xcshareddata/xcschemes/MRZScannerExample.xcscheme b/Example/MRZScannerExample.xcodeproj/xcshareddata/xcschemes/MRZScannerExample.xcscheme new file mode 100644 index 0000000..93ac729 --- /dev/null +++ b/Example/MRZScannerExample.xcodeproj/xcshareddata/xcschemes/MRZScannerExample.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/MRZScannerExample/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json rename to Example/MRZScannerExample/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Example/MRZScannerExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/MRZScannerExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..532cd72 --- /dev/null +++ b/Example/MRZScannerExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,63 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Assets.xcassets/Contents.json b/Example/MRZScannerExample/Assets.xcassets/Contents.json similarity index 100% rename from Example/Example/Assets.xcassets/Contents.json rename to Example/MRZScannerExample/Assets.xcassets/Contents.json diff --git a/Example/MRZScannerExample/Camera.swift b/Example/MRZScannerExample/Camera.swift new file mode 100644 index 0000000..0f27c7e --- /dev/null +++ b/Example/MRZScannerExample/Camera.swift @@ -0,0 +1,114 @@ +// +// Camera.swift +// Example +// +// Created by Roman Mazeev on 01/01/2023. +// + +import AVFoundation +import CoreImage + +final class Camera: NSObject { + let captureSession = AVCaptureSession() + + private(set) lazy var imageStream: AsyncStream = { + AsyncStream { continuation in + imageStreamCallback = { ciImage in + continuation.yield(ciImage) + } + } + }() + private var imageStreamCallback: ((CIImage) -> Void)? + + private let captureDevice = AVCaptureDevice.default(for: .video) + 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 { + 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() + } + } + } + + 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() + @unknown default: + return false + } + } + + private func configureCaptureSession(completionHandler: (_ success: Bool) -> Void) throws { + var success = false + + self.captureSession.beginConfiguration() + + defer { + self.captureSession.commitConfiguration() + completionHandler(success) + } + + guard let captureDevice else { fatalError("Unable to create capture device") } + let deviceInput = try AVCaptureDeviceInput(device: captureDevice) + + let videoOutput = AVCaptureVideoDataOutput() + videoOutput.setSampleBufferDelegate(self, queue: .init(label: "VideoDataOutput queue")) + + guard captureSession.canAddInput(deviceInput) else { + fatalError("Unable to add device input to capture session.") + } + + guard captureSession.canAddOutput(videoOutput) else { + fatalError("Unable to add video output to capture session.") + } + + captureSession.addInput(deviceInput) + captureSession.addOutput(videoOutput) + + self.deviceInput = deviceInput + self.videoOutput = videoOutput + + videoOutput.connection(with: .video)?.videoOrientation = .portrait + + isCaptureSessionConfigured = true + + success = true + } +} + +extension Camera: AVCaptureVideoDataOutputSampleBufferDelegate { + func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + guard let pixelBuffer = sampleBuffer.imageBuffer else { return } + + imageStreamCallback?(CIImage(cvPixelBuffer: pixelBuffer)) + } +} diff --git a/Example/MRZScannerExample/CameraView.swift b/Example/MRZScannerExample/CameraView.swift new file mode 100644 index 0000000..af3c562 --- /dev/null +++ b/Example/MRZScannerExample/CameraView.swift @@ -0,0 +1,46 @@ +// +// CameraView.swift +// Example +// +// Created by Roman Mazeev on 01/01/2023. +// + +import UIKit +import SwiftUI +import AVFoundation + +struct CameraView: UIViewControllerRepresentable { + let captureSession: AVCaptureSession + + func makeUIViewController(context: Context) -> UIViewController { + return CameraViewController(captureSession: captureSession) + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +final class CameraViewController: UIViewController { + var previewLayer: AVCaptureVideoPreviewLayer! + + init(captureSession: AVCaptureSession) { + previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + 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 + view.layer.addSublayer(previewLayer) + } +} + +struct CameraView_Previews: PreviewProvider { + static var previews: some View { + CameraView(captureSession: .init()) + } +} diff --git a/Example/MRZScannerExample/ContentView.swift b/Example/MRZScannerExample/ContentView.swift new file mode 100644 index 0000000..54b8079 --- /dev/null +++ b/Example/MRZScannerExample/ContentView.swift @@ -0,0 +1,132 @@ +// +// ContentView.swift +// Example +// +// Created by Roman Mazeev on 01/01/2023. +// + +import SwiftUI +import MRZParser + +struct ContentView: View { + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter + }() + + @StateObject private var viewModel = ViewModel() + + var body: some View { + GeometryReader { proxy in + Group { + if let cameraRect = viewModel.cameraRect { + CameraView(captureSession: viewModel.captureSession) + .frame(width: cameraRect.width, height: cameraRect.height) + } + + ZStack { + Color.black.opacity(0.5) + + if let mrzRect = viewModel.mrzRect { + Rectangle() + .blendMode(.destinationOut) + .frame(width: mrzRect.size.width, height: mrzRect.size.height) + .position(mrzRect.origin) + .task { + do { + try await viewModel.startMRZScanning() + } catch { + print(error.localizedDescription) + } + } + } + } + .compositingGroup() + + + if let boundingRects = viewModel.boundingRects { + ForEach(boundingRects.valid, id: \.self) { boundingRect in + createBoundingRect(boundingRect, color: .green) + } + + ForEach(boundingRects.invalid, id: \.self) { boundingRect in + createBoundingRect(boundingRect, color: .red) + } + } + } + .onAppear { + viewModel.cameraRect = proxy.frame(in: .global) + viewModel.mrzRect = .init(origin: .init(x: proxy.size.width / 2, y: proxy.size.height / 2), + 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( + title: Text("Important message"), + message: Text(createAlertMessage(mrzResult: viewModel.mrzResult!)), + dismissButton: .default(Text("Got it!")) { + Task { + try await viewModel.startMRZScanning() + } + } + ) + } + .task { + viewModel.startCamera() + } + .statusBarHidden() + .ignoresSafeArea() + } + + private func createBoundingRect(_ rect: CGRect, color: Color) -> some View { + Rectangle() + .stroke(color) + .frame(width: rect.width, height: rect.height) + .position(rect.origin) + } + + private func createAlertMessage(mrzResult: MRZResult) -> String { + var birthdateString: String? + var expiryDateString: String? + + 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 ?? "-") + """ + } +} + +extension CGRect: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(origin.x) + hasher.combine(origin.y) + hasher.combine(size.width) + hasher.combine(size.height) + } +} + + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Example/MRZScannerExample/Example.entitlements b/Example/MRZScannerExample/Example.entitlements new file mode 100644 index 0000000..f2ef3ae --- /dev/null +++ b/Example/MRZScannerExample/Example.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Example/MRZScannerExample/ExampleApp.swift b/Example/MRZScannerExample/ExampleApp.swift new file mode 100644 index 0000000..339a8b0 --- /dev/null +++ b/Example/MRZScannerExample/ExampleApp.swift @@ -0,0 +1,17 @@ +// +// ExampleApp.swift +// Example +// +// Created by Roman Mazeev on 29/12/2022. +// + +import SwiftUI + +@main +struct ExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Example/MRZScannerExample/Info.plist b/Example/MRZScannerExample/Info.plist new file mode 100644 index 0000000..816a149 --- /dev/null +++ b/Example/MRZScannerExample/Info.plist @@ -0,0 +1,13 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + + + diff --git a/Example/MRZScannerExample/Preview Content/Preview Assets.xcassets/Contents.json b/Example/MRZScannerExample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/MRZScannerExample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/MRZScannerExample/ViewModel.swift b/Example/MRZScannerExample/ViewModel.swift new file mode 100644 index 0000000..0870cc6 --- /dev/null +++ b/Example/MRZScannerExample/ViewModel.swift @@ -0,0 +1,89 @@ +// +// ViewModel.swift +// Example +// +// Created by Roman Mazeev on 01/01/2023. +// + +import AVFoundation +import SwiftUI +import MRZScanner +import MRZParser +import Vision + +final class ViewModel: ObservableObject { + + // MARK: Camera + private let camera = Camera() + @Published var cameraRect: CGRect? + var captureSession: AVCaptureSession { + camera.captureSession + } + + func startCamera() { + Task { + await camera.start() + } + } + + // MARK: Scanning + @Published var boundingRects: ScanedBoundingRects? + @Published var mrzRect: CGRect? + @Published var mrzResult: MRZResult? + + private var scanningTask: Task<(), Error>? + + func startMRZScanning() async throws { + guard let cameraRect, let mrzRect else { return } + + let correctedMRZRect = correctCoordinates(to: .leftTop, rect: mrzRect) + let roi = MRZScanner.convertRect(to: .normalizedRect, rect: correctedMRZRect, imageWidth: Int(cameraRect.width), imageHeight: Int(cameraRect.height)) + let scanningStream = MRZScanner.scanLive( + imageStream: camera.imageStream, + configuration: .init(orientation: .up, regionOfInterest: roi, minimumTextHeight: 0.1, recognitionLevel: .fast) + ) + + scanningTask = Task { + for try await liveScanningResult in scanningStream { + Task { @MainActor in + switch liveScanningResult { + case .found(let scanningResult): + boundingRects = correctBoundingRects(to: .center, rects: scanningResult.boundingRects) + mrzResult = scanningResult.result + scanningTask?.cancel() + case .notFound(let boundingRects): + self.boundingRects = correctBoundingRects(to: .center, rects: boundingRects) + } + } + } + } + } + + // MARK: - Correct CGRect origin from top left to center + + enum CorrectionType { + case center + case leftTop + } + + private func correctBoundingRects(to type: CorrectionType, rects: ScanedBoundingRects) -> ScanedBoundingRects { + guard let mrzRect else { fatalError("Camera rect must be set") } + + let convertedCoordinates = rects.convertedToImageRects(imageWidth: Int(mrzRect.width), imageHeight: Int(mrzRect.height)) + let correctedMRZRect = correctCoordinates(to: .leftTop, rect: mrzRect) + + func correctRects(_ rects: [CGRect]) -> [CGRect] { + rects + .map { correctCoordinates(to: type, rect: $0) } + .map { .init(origin: .init(x: $0.origin.x + correctedMRZRect.minX, y: $0.origin.y + correctedMRZRect.minY), size: $0.size) } + } + + return .init(valid: correctRects(convertedCoordinates.valid), invalid: correctRects(convertedCoordinates.invalid)) + } + + private func correctCoordinates(to type: CorrectionType, rect: CGRect) -> CGRect { + let x = type == .center ? rect.minX + rect.width / 2 : rect.minX - rect.width / 2 + let y = type == .center ? rect.minY + rect.height / 2 : rect.minY - rect.height / 2 + return CGRect(origin: .init(x: x, y: y), size: rect.size) + } +} diff --git a/LICENSE b/LICENSE index 9f90781..d7a80c5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 App in the Air +Copyright (c) 2021 Roman Mazeev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.resolved b/Package.resolved index 7f2162e..1d94759 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,16 +1,14 @@ { - "object": { - "pins": [ - { - "package": "MRZParser", - "repositoryURL": "https://github.com/appintheair/MRZParser.git", - "state": { - "branch": null, - "revision": "98ef7d43a98991b0296e993f76562ebd29038595", - "version": "1.1.2" - } + "pins" : [ + { + "identity" : "mrzparser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/romanmazeev/MRZParser.git", + "state" : { + "revision" : "2c1c4809379f081c01297f0ac92df2e94c6db54a", + "version" : "1.1.3" } - ] - }, - "version": 1 + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index 8ad9225..35c54c4 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -13,7 +13,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/appintheair/MRZParser.git", .upToNextMajor(from: "1.1.2")) + .package(url: "https://github.com/romanmazeev/MRZParser.git", .upToNextMajor(from: "1.1.3")) ], targets: [ .target( diff --git a/README.md b/README.md index 08ceb84..6f497d6 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,8 @@ -[![Build and test](https://github.com/appintheair/MRZScanner/actions/workflows/Build%20and%20test.yml/badge.svg?branch=develop)](https://github.com/appintheair/MRZScanner/actions/workflows/Build%20and%20test.yml) -[![SPM](https://img.shields.io/badge/SPM-compatible-brightgreen.svg)](https://github.com/appintheair/MRZParser/blob/develop/Package.swift) -[![codecov](https://codecov.io/gh/appintheair/MRZScanner/branch/develop/graph/badge.svg?token=BAvvoujCum)](https://codecov.io/gh/appintheair/MRZScanner) +[![SPM](https://img.shields.io/badge/SPM-compatible-brightgreen.svg)](https://github.com/romanmazeev/MRZParser/blob/master/Package.swift) # MRZScanner Library for scanning documents via [MRZ](https://en.wikipedia.org/wiki/Machine-readable_passport) using [ Vision API](https://developer.apple.com/documentation/vision/vnrecognizetextrequest). -## Example -The example project is located inside the [Example](https://github.com/appintheair/MRZScanner/tree/develop/Example) folder. - -![gif](https://github.com/appintheair/MRZScanner/blob/develop/docs/img/example.gif) - -*To run it, you need a device with the [minimum required OS version](https://github.com/appintheair/MRZScanner#requirements).* - ## Requirements * iOS 13.0+ * macOS 10.15+ @@ -22,16 +13,39 @@ The example project is located inside the [Example](https://github.com/appinthea ### Swift Package Manager ```swift dependencies: [ - .package(url: "https://github.com/appintheair/MRZScanner.git", .upToNextMajor(from: "0.0.1")) + .package(url: "https://github.com/romanmazeev/MRZScanner.git", .upToNextMajor(from: "1.0.0")) ] ``` -*The library has an SPM [dependency](https://github.com/appintheair/MRZParser) for MRZ code parsing.* +*The library has an SPM [dependency](https://github.com/romanmazeev/MRZParser) for MRZ code parsing.* ## Usage -Currently there are 2 scanners available, `LiveMRZScanner` and `ImageMRZScanner`. -The first is used to scan the MRZ code on a single image, and the second in real-time scanning. -To scan, you need to call the `scanFrame` / `scan` method of the scanner. +1. For both image scanning and live scanning, we need to create `ScanningConfiguration` +```swift +ScanningConfiguration(orientation: .up, regionOfInterest: roi, minimumTextHeight: 0.1, recognitionLevel: .fast) +``` + +2. After you need to start scanning +```swift +/// Live scanning +for try await scanningResult in MRZScanner.scanLive(imageStream: imageStream, configuration: configuration) { + // Handle `scanningResult` here +} + +/// Single scanning +let scanningResult = try await MRZScanner.scanSingle(image: image, configuration: configuration) +``` + +*Also, for the convenience of transforming coordinates into a normalized form and back, there is a static method `convertRect`* +```swift +MRZScanner.convertRect(to: .normalizedRect, rect: rect, imageWidth: imageWidth, imageHeight: imageHeight) +``` + +## Example +![gif](https://github.com/romanmazeev/MRZScanner/blob/master/Docs/MRZScannerExample.gif) + +The example project is located inside the [`Example` folder](https://github.com/romanmazeev/MRZScanner/tree/master/Example). +*To run it, you need a device with the [minimum required OS version](https://github.com/romanmazeev/MRZScanner#requirements).* ## License -The library is distributed under the MIT [LICENSE](https://opensource.org/licenses/MIT). +The library is distributed under the [MIT LICENSE](https://opensource.org/licenses/MIT). diff --git a/Sources/MRZScanner/AsyncStream+map.swift b/Sources/MRZScanner/AsyncStream+map.swift new file mode 100644 index 0000000..d7d2cd5 --- /dev/null +++ b/Sources/MRZScanner/AsyncStream+map.swift @@ -0,0 +1,23 @@ +// +// AsyncStream+map.swift +// +// +// Created by Roman Mazeev on 29/12/2022. +// + +extension AsyncStream { + public func map(_ transform: @escaping (Self.Element) async throws -> Transformed) -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + Task { + for await element in self { + do { + continuation.yield(try await transform(element)) + } catch { + continuation.finish(throwing: error) + } + } + continuation.finish() + } + } + } +} diff --git a/Sources/MRZScanner/Private/Trackers/FrequencyTracker.swift b/Sources/MRZScanner/MRZFrequencyTracker.swift similarity index 71% rename from Sources/MRZScanner/Private/Trackers/FrequencyTracker.swift rename to Sources/MRZScanner/MRZFrequencyTracker.swift index 01d6517..498f8d7 100644 --- a/Sources/MRZScanner/Private/Trackers/FrequencyTracker.swift +++ b/Sources/MRZScanner/MRZFrequencyTracker.swift @@ -1,19 +1,21 @@ // -// FrequencyTracker.swift +// MRZFrequencyTracker.swift // // // Created by Roman Mazeev on 13.07.2021. // -final class FrequencyTracker: Tracker { +import MRZParser + +final class MRZFrequencyTracker { private let frequency: Int - private var seenResults: [ParsedResult: Int] = [:] + private var seenResults: [MRZResult: Int] = [:] init(frequency: Int) { self.frequency = frequency } - func isResultStable(_ result: ParsedResult) -> Bool { + func isResultStable(_ result: MRZResult) -> Bool { guard let seenResultFrequency = seenResults[result] else { seenResults[result] = 1 return false diff --git a/Sources/MRZScanner/MRZScanner.swift b/Sources/MRZScanner/MRZScanner.swift new file mode 100644 index 0000000..b5be7f1 --- /dev/null +++ b/Sources/MRZScanner/MRZScanner.swift @@ -0,0 +1,96 @@ +// +// MRZScanner.swift +// +// +// Created by Roman Mazeev on 12.07.2021. +// + +import CoreImage +import Vision +import MRZParser + +public struct ScanningConfiguration { + let orientation: CGImagePropertyOrientation + let regionOfInterest: CGRect + let minimumTextHeight: Float + let recognitionLevel: VNRequestTextRecognitionLevel + + public init(orientation: CGImagePropertyOrientation, regionOfInterest: CGRect, minimumTextHeight: Float, recognitionLevel: VNRequestTextRecognitionLevel) { + self.orientation = orientation + self.regionOfInterest = regionOfInterest + self.minimumTextHeight = minimumTextHeight + self.recognitionLevel = recognitionLevel + } +} + +public struct MRZScanner { + public static func scanLive(imageStream: AsyncStream, configuration: ScanningConfiguration) -> AsyncThrowingStream, Error> { + let frequencyTracker = MRZFrequencyTracker(frequency: 2) + + return imageStream.map { image in + let recognizerResults = try await VisionTextRecognizer.recognize(scanningImage: image, configuration: configuration) + let validatedResults = MRZValidator.getValidatedResults(from: recognizerResults.map(\.results)) + + let boundingRects = getScannedBoundingRects(from: recognizerResults, validLines: validatedResults) + guard let parsedResult = MRZParser.init(isOCRCorrectionEnabled: true).parse(mrzLines: validatedResults.map(\.result)) else { + return .notFound(boundingRects) + } + + return frequencyTracker.isResultStable(parsedResult) + ? LiveScanningResult.found(.init(result: parsedResult, boundingRects: boundingRects)) + : .notFound(boundingRects) + } + } + + public static func scanSingle(image: CIImage, configuration: ScanningConfiguration) async throws -> ScanningResult { + let recognizerResults = try await VisionTextRecognizer.recognize(scanningImage: image, configuration: configuration) + let validatedResults = MRZValidator.getValidatedResults(from: recognizerResults.map(\.results)) + guard let parsedResult = MRZParser.init(isOCRCorrectionEnabled: true).parse(mrzLines: validatedResults.map(\.result)) else { + throw MRZScannerError.codeNotFound + } + + return .init(result: parsedResult, boundingRects: getScannedBoundingRects(from: recognizerResults, validLines: validatedResults)) + } + + private static func getScannedBoundingRects( + from results: [VisionTextRecognizer.Result], + validLines: [MRZValidator.Result] + ) -> ScanedBoundingRects { + let allBoundingRects = results.map(\.boundingRect) + let validRectIndexes = Set(validLines.map(\.index)) + + var validScannedBoundingRects: [CGRect] = [] + var invalidScannedBoundingRects: [CGRect] = [] + allBoundingRects.enumerated().forEach { + if validRectIndexes.contains($0.offset) { + validScannedBoundingRects.append($0.element) + } else { + invalidScannedBoundingRects.append($0.element) + } + } + + return .init(valid: validScannedBoundingRects, invalid: invalidScannedBoundingRects) + } + + private init() {} +} + +public extension MRZScanner { + enum RectConvertationType { + case imageRect + case normalizedRect + } + + static func convertRect(to type: RectConvertationType, rect: CGRect, imageWidth: Int, imageHeight: Int) -> CGRect { + switch type { + case .imageRect: + return VNImageRectForNormalizedRect(rect, imageWidth, imageHeight) + case .normalizedRect: + return VNNormalizedRectForImageRect(rect, imageWidth, imageHeight) + } + } +} + +enum MRZScannerError: Error { + case codeNotFound +} diff --git a/Sources/MRZScanner/Private/Validators/MRZValidator.swift b/Sources/MRZScanner/MRZValidator.swift similarity index 74% rename from Sources/MRZScanner/Private/Validators/MRZValidator.swift rename to Sources/MRZScanner/MRZValidator.swift index 43e9e9f..5a9307a 100644 --- a/Sources/MRZScanner/Private/Validators/MRZValidator.swift +++ b/Sources/MRZScanner/MRZValidator.swift @@ -7,9 +7,16 @@ import MRZParser -struct MRZValidator: Validator { - func getValidatedResults(from possibleLines: [[String]]) -> ValidatedResults { - var validLines = ValidatedResults() +struct MRZValidator { + struct Result { + /// MRZLine + let result: String + /// MRZLine boundingRect index + let index: Int + } + + static func getValidatedResults(from possibleLines: [[String]]) -> [Result] { + var validLines: [Result] = [] for validMRZCode in MRZFormat.allCases { guard validLines.count < validMRZCode.linesCount else { break } @@ -28,4 +35,6 @@ struct MRZValidator: Validator { } return validLines } + + private init() {} } diff --git a/Sources/MRZScanner/Private/LiveScanner.swift b/Sources/MRZScanner/Private/LiveScanner.swift deleted file mode 100644 index c8a6e55..0000000 --- a/Sources/MRZScanner/Private/LiveScanner.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// LiveScanner.swift -// -// -// Created by Roman Mazeev on 14.07.2021. -// - -protocol LiveScanner { - var tracker: Tracker { get } -} diff --git a/Sources/MRZScanner/Private/Parsers/MRZParser.swift b/Sources/MRZScanner/Private/Parsers/MRZParser.swift deleted file mode 100644 index 5e2d36c..0000000 --- a/Sources/MRZScanner/Private/Parsers/MRZParser.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// MRZLineParser.swift -// -// -// Created by Roman Mazeev on 13.07.2021. -// - -import MRZParser - -// TODO: Remove this line when `ParsedResult` struct will be implemented -public typealias ParsedResult = MRZResult - -struct MRZLineParser: Parser { - func parse(lines: [String]) -> ParsedResult? { - MRZParser(isOCRCorrectionEnabled: true).parse(mrzLines: lines) - } -} diff --git a/Sources/MRZScanner/Private/Parsers/Parser.swift b/Sources/MRZScanner/Private/Parsers/Parser.swift deleted file mode 100644 index 77cc82d..0000000 --- a/Sources/MRZScanner/Private/Parsers/Parser.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Parser.swift -// -// -// Created by Roman Mazeev on 13.07.2021. -// - -// TODO: Add `ParsedResult` to use not only MRZ - -protocol Parser { - func parse(lines: [String]) -> ParsedResult? -} diff --git a/Sources/MRZScanner/Private/Scanner/DefaultScanner.swift b/Sources/MRZScanner/Private/Scanner/DefaultScanner.swift deleted file mode 100644 index d856cf5..0000000 --- a/Sources/MRZScanner/Private/Scanner/DefaultScanner.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// DefaultScanner.swift -// -// -// Created by Roman Mazeev on 14.07.2021. -// - -import CoreImage - -struct DefaultScanner: Scanner { - enum ScanningError: Error { - case codeNotFound - } - - enum ScanningType { - case live - case single - } - - let textRecognizer: TextRecognizer - let validator: Validator - let parser: Parser - - init(textRecognizer: TextRecognizer, validator: Validator, parser: Parser) { - self.textRecognizer = textRecognizer - self.validator = validator - self.parser = parser - } - - func scan( - scanningType: ScanningType, - scanningImage: ScanningImage, - orientation: CGImagePropertyOrientation, - regionOfInterest: CGRect?, - minimumTextHeight: Float?, - recognitionLevel: RecognitionLevel, - foundBoundingRectsHandler: (([CGRect]) -> Void)? = nil, - completionHandler: @escaping (Result, Error>) -> Void - ) { - textRecognizer.recognize( - scanningImage: scanningImage, - orientation: orientation, - regionOfInterest: regionOfInterest, - minimumTextHeight: minimumTextHeight, - recognitionLevel: recognitionLevel - ) { - switch $0 { - case .success(let results): - DispatchQueue.main.async { - foundBoundingRectsHandler?(results.map { $0.boundingRect }) - } - - let validatedResult = validator.getValidatedResults(from: results.map { $0.results }) - guard let parsedResult = parser.parse(lines: validatedResult.map { $0.result }) else { - if scanningType == .single { - DispatchQueue.main.async { - completionHandler(.failure(ScanningError.codeNotFound)) - } - } - return - } - - DispatchQueue.main.async { - completionHandler(.success( - .init( - result: parsedResult, - boundingRects: getScannedBoundingRects( - from: results, - validLines: validatedResult - ) - ) - )) - } - case .failure(let error): - DispatchQueue.main.async { - completionHandler(.failure(error)) - } - } - } - } - - private func getScannedBoundingRects( - from results: [TextRecognizerResult], - validLines: ValidatedResults - ) -> ScannedBoundingRects { - let allBoundingRects = results.map(\.boundingRect) - let validRectIndexes = Set(validLines.map(\.index)) - - var scannedBoundingRects: ScannedBoundingRects = ([], []) - allBoundingRects.enumerated().forEach { - if validRectIndexes.contains($0.offset) { - scannedBoundingRects.valid.append($0.element) - } else { - scannedBoundingRects.invalid.append($0.element) - } - } - - return scannedBoundingRects - } -} - diff --git a/Sources/MRZScanner/Private/Scanner/Scanner.swift b/Sources/MRZScanner/Private/Scanner/Scanner.swift deleted file mode 100644 index 0aa8d5f..0000000 --- a/Sources/MRZScanner/Private/Scanner/Scanner.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Scanner.swift -// -// -// Created by Roman Mazeev on 13.07.2021. -// - -import CoreImage - -protocol Scanner { - var textRecognizer: TextRecognizer { get } - var validator: Validator { get } - var parser: Parser { get } -} diff --git a/Sources/MRZScanner/Private/ScannerService.swift b/Sources/MRZScanner/Private/ScannerService.swift deleted file mode 100644 index 11901a4..0000000 --- a/Sources/MRZScanner/Private/ScannerService.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// ScannerService.swift -// -// -// Created by Roman Mazeev on 14.07.2021. -// - -protocol ScannerService { - var scanner: DefaultScanner { get } -} diff --git a/Sources/MRZScanner/Private/TextRecognizers/TextRecognizer.swift b/Sources/MRZScanner/Private/TextRecognizers/TextRecognizer.swift deleted file mode 100644 index 11153f3..0000000 --- a/Sources/MRZScanner/Private/TextRecognizers/TextRecognizer.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Recognizer.swift -// -// -// Created by Roman Mazeev on 12.07.2021. -// - -import CoreImage - -struct TextRecognizerResult { - let results: [String] - let boundingRect: CGRect -} - -protocol TextRecognizer { - func recognize( - scanningImage: ScanningImage, - orientation: CGImagePropertyOrientation, - regionOfInterest: CGRect?, - minimumTextHeight: Float?, - recognitionLevel: RecognitionLevel, - completionHandler: @escaping (Result<[TextRecognizerResult], Error>) -> Void - ) -} diff --git a/Sources/MRZScanner/Private/TextRecognizers/VisionRecognizer.swift b/Sources/MRZScanner/Private/TextRecognizers/VisionRecognizer.swift deleted file mode 100644 index 99c37e7..0000000 --- a/Sources/MRZScanner/Private/TextRecognizers/VisionRecognizer.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// VisionRecognizer.swift -// -// -// Created by Roman Mazeev on 13.07.2021. -// - -import Vision - -struct VisionTextRecognizer: TextRecognizer { - func recognize( - scanningImage: ScanningImage, - orientation: CGImagePropertyOrientation, - regionOfInterest: CGRect?, - minimumTextHeight: Float?, - recognitionLevel: RecognitionLevel, - completionHandler: @escaping (Result<[TextRecognizerResult], Error>) -> Void - ) { - let request = VNRecognizeTextRequest { request, error in - guard error == nil else { - completionHandler(.failure(error!)) - return - } - - let visionResults = request.results as! [VNRecognizedTextObservation] - - let result: [TextRecognizerResult] = visionResults.map { - .init(results: $0.topCandidates(10).map { $0.string }, boundingRect: $0.boundingBox) - } - - completionHandler(.success(result)) - } - - if let regionOfInterest = regionOfInterest { - request.regionOfInterest = regionOfInterest - } - if let minimumTextHeight = minimumTextHeight { - request.minimumTextHeight = minimumTextHeight - } - request.recognitionLevel = recognitionLevel == .fast ? .fast : .accurate - request.usesLanguageCorrection = false - - let imageRequestHandler: VNImageRequestHandler - switch scanningImage { - case .cgImage(let image): - imageRequestHandler = VNImageRequestHandler(cgImage: image, orientation: orientation, options: [:]) - case .pixelBuffer(let pixelBuffer): - imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: orientation, options: [:]) - } - - DispatchQueue.global(qos: .userInitiated).async { - do { - try imageRequestHandler.perform([request]) - } catch { - completionHandler(.failure(error)) - } - } - } -} diff --git a/Sources/MRZScanner/Private/Trackers/Tracker.swift b/Sources/MRZScanner/Private/Trackers/Tracker.swift deleted file mode 100644 index 84c3a34..0000000 --- a/Sources/MRZScanner/Private/Trackers/Tracker.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Tracker.swift -// -// -// Created by Roman Mazeev on 16.06.2021. -// - -protocol Tracker { - func isResultStable(_ result: ParsedResult) -> Bool -} diff --git a/Sources/MRZScanner/Private/Validators/Validator.swift b/Sources/MRZScanner/Private/Validators/Validator.swift deleted file mode 100644 index 82bd6bc..0000000 --- a/Sources/MRZScanner/Private/Validators/Validator.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Validator.swift -// -// -// Created by Roman Mazeev on 12.07.2021. -// - -typealias ValidatedResults = [ValidatedResult] -struct ValidatedResult { - /// MRZLine - let result: String - /// MRZLine boundingRect index - let index: Int -} - -protocol Validator { - func getValidatedResults(from possibleLines: [[String]]) -> ValidatedResults -} diff --git a/Sources/MRZScanner/Public/DocumentScanningResult.swift b/Sources/MRZScanner/Public/DocumentScanningResult.swift deleted file mode 100644 index c923ae4..0000000 --- a/Sources/MRZScanner/Public/DocumentScanningResult.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// DocumentScanningResult.swift -// -// -// Created by Roman Mazeev on 12.07.2021. -// - -import CoreGraphics - -public typealias ScannedBoundingRects = (valid: [CGRect], invalid: [CGRect]) - -public struct DocumentScanningResult { - public let result: T - public let boundingRects: ScannedBoundingRects -} diff --git a/Sources/MRZScanner/Public/MRZScanner/ImageMRZScanner.swift b/Sources/MRZScanner/Public/MRZScanner/ImageMRZScanner.swift deleted file mode 100644 index 779cad6..0000000 --- a/Sources/MRZScanner/Public/MRZScanner/ImageMRZScanner.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ImageMRZScanner.swift -// -// -// Created by Roman Mazeev on 12.07.2021. -// - -import CoreImage - -public struct ImageMRZScanner: ScannerService { - let scanner: DefaultScanner - - public init() { - scanner = DefaultScanner( - textRecognizer: VisionTextRecognizer(), - validator: MRZValidator(), - parser: MRZLineParser() - ) - } - - init(textRecognizer: TextRecognizer, validator: Validator, parser: Parser) { - scanner = DefaultScanner( - textRecognizer: textRecognizer, - validator: validator, - parser: parser - ) - } - - public func scan( - scanningImage: ScanningImage, - orientation: CGImagePropertyOrientation, - regionOfInterest: CGRect? = nil, - minimumTextHeight: Float? = nil, - recognitionLevel: RecognitionLevel = .accurate, - foundBoundingRectsHandler: (([CGRect]) -> Void)? = nil, - completionHandler: @escaping (Result, Error>) -> Void - ) { - scanner.scan( - scanningType: .single, - scanningImage: scanningImage, - orientation: orientation, - regionOfInterest: regionOfInterest, - minimumTextHeight: minimumTextHeight, - recognitionLevel: recognitionLevel, - foundBoundingRectsHandler: foundBoundingRectsHandler, - completionHandler: completionHandler - ) - } -} - diff --git a/Sources/MRZScanner/Public/MRZScanner/LiveMRZScanner.swift b/Sources/MRZScanner/Public/MRZScanner/LiveMRZScanner.swift deleted file mode 100644 index 5f09f68..0000000 --- a/Sources/MRZScanner/Public/MRZScanner/LiveMRZScanner.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// LiveMRZScanner.swift -// -// -// Created by Roman Mazeev on 12.07.2021. -// - -import CoreImage - -public final class LiveMRZScanner: ScannerService, LiveScanner { - let scanner: DefaultScanner - let tracker: Tracker - - /// - Parameter frequency: Number of times the result was encountered - public init(frequency: Int = 2) { - scanner = DefaultScanner( - textRecognizer: VisionTextRecognizer(), - validator: MRZValidator(), - parser: MRZLineParser() - ) - - tracker = FrequencyTracker(frequency: frequency) - } - - init(textRecognizer: TextRecognizer, validator: Validator, parser: Parser, tracker: Tracker) { - scanner = DefaultScanner( - textRecognizer: textRecognizer, - validator: validator, - parser: parser - ) - - self.tracker = tracker - } - - public func scanFrame( - scanningImage: ScanningImage, - orientation: CGImagePropertyOrientation, - regionOfInterest: CGRect? = nil, - minimumTextHeight: Float? = nil, - foundBoundingRectsHandler: (([CGRect]) -> Void)? = nil, - completionHandler: @escaping (Result, Error>) -> Void - ) { - scanner.scan( - scanningType: .live, - scanningImage: scanningImage, - orientation: orientation, - regionOfInterest: regionOfInterest, - minimumTextHeight: minimumTextHeight, - recognitionLevel: .fast, - foundBoundingRectsHandler: foundBoundingRectsHandler, - completionHandler: { result in - switch result { - case .success(let scanningResult): - guard self.tracker.isResultStable(scanningResult.result) else { return } - - completionHandler( - .success(.init( - result: scanningResult.result, - boundingRects: scanningResult.boundingRects - )) - ) - case .failure(let error): - completionHandler(.failure(error)) - } - } - ) - } -} diff --git a/Sources/MRZScanner/Public/RecognitionLevel.swift b/Sources/MRZScanner/Public/RecognitionLevel.swift deleted file mode 100644 index f407da0..0000000 --- a/Sources/MRZScanner/Public/RecognitionLevel.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// RecognitionLevel.swift -// -// -// Created by Roman Mazeev on 13.07.2021. -// - -public enum RecognitionLevel { - case accurate - case fast -} diff --git a/Sources/MRZScanner/Public/ScanningImage.swift b/Sources/MRZScanner/Public/ScanningImage.swift deleted file mode 100644 index 3e1d187..0000000 --- a/Sources/MRZScanner/Public/ScanningImage.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// ScanningImage.swift -// ScanningImage -// -// Created by Roman Mazeev on 28.07.2021. -// - -import CoreImage - -public enum ScanningImage { - case cgImage(CGImage) - case pixelBuffer(CVPixelBuffer) -} diff --git a/Sources/MRZScanner/ScanningResult.swift b/Sources/MRZScanner/ScanningResult.swift new file mode 100644 index 0000000..42a242a --- /dev/null +++ b/Sources/MRZScanner/ScanningResult.swift @@ -0,0 +1,34 @@ +// +// ScanningResult.swift +// +// +// Created by Roman Mazeev on 29/12/2022. +// + +import CoreGraphics + +public struct ScanedBoundingRects { + public let valid: [CGRect], invalid: [CGRect] + + public func convertedToImageRects(imageWidth: Int, imageHeight: Int) -> Self { + .init( + valid: valid.map { MRZScanner.convertRect(to: .imageRect, rect: $0, imageWidth: imageWidth, imageHeight: imageHeight) }, + invalid: invalid.map { MRZScanner.convertRect(to: .imageRect, rect: $0, imageWidth: imageWidth, imageHeight: imageHeight) } + ) + } + + public init(valid: [CGRect], invalid: [CGRect]) { + self.valid = valid + self.invalid = invalid + } +} + +public struct ScanningResult { + public let result: T + public let boundingRects: ScanedBoundingRects +} + +public enum LiveScanningResult { + case notFound(ScanedBoundingRects) + case found(ScanningResult) +} diff --git a/Sources/MRZScanner/VisionTextRecognizer.swift b/Sources/MRZScanner/VisionTextRecognizer.swift new file mode 100644 index 0000000..ae206a7 --- /dev/null +++ b/Sources/MRZScanner/VisionTextRecognizer.swift @@ -0,0 +1,40 @@ +// +// TextRecognizer.swift +// +// +// Created by Roman Mazeev on 13.07.2021. +// + +import Vision +import CoreImage + +struct VisionTextRecognizer { + struct Result { + let results: [String] + let boundingRect: CGRect + } + + static func recognize(scanningImage: CIImage, configuration: ScanningConfiguration) async throws -> [Result] { + try await withCheckedThrowingContinuation { continuation in + let request = VNRecognizeTextRequest { response, _ in + guard let visionResults = response.results as? [VNRecognizedTextObservation] else { return } + continuation.resume(returning: visionResults.map { + Result(results: $0.topCandidates(10).map(\.string), boundingRect: $0.boundingBox) + }) + } + + request.regionOfInterest = configuration.regionOfInterest + request.minimumTextHeight = configuration.minimumTextHeight + request.recognitionLevel = configuration.recognitionLevel + request.usesLanguageCorrection = false + + do { + try VNImageRequestHandler(ciImage: scanningImage, orientation: configuration.orientation).perform([request]) + } catch { + continuation.resume(throwing: error) + } + } + } + + private init() {} +} diff --git a/Tests/MRZScannerTests/DefaultScannerTests.swift b/Tests/MRZScannerTests/DefaultScannerTests.swift deleted file mode 100644 index f74b05c..0000000 --- a/Tests/MRZScannerTests/DefaultScannerTests.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// DefaultScannerTests.swift -// -// -// Created by Roman Mazeev on 13.07.2021. -// - -import XCTest -@testable import MRZScanner - -final class DefaultScannerTests: XCTestCase { - private var scanner: DefaultScanner { - .init(textRecognizer: textRecognizer, validator: validator, parser: parser) - } - private var textRecognizer: StubTextRecognizer! - private var parser: StubParser! - private var tracker: StubTracker! - private var validator: StubValidator! - - override func setUp() { - super.setUp() - textRecognizer = StubTextRecognizer() - parser = StubParser() - tracker = StubTracker() - validator = StubValidator() - } - - // MARK: Single - - func testaSingleComplete() { - let expectation = XCTestExpectation() - textRecognizer.recognizeResult = .success(StubModels.textRecognizerResults) - validator.validatedResults = StubModels.validatedResults - parser.parsedResult = StubModels.firstParsedResult - scan(scanningType: .single) { result in - switch result { - case .success(let scanningResult): - XCTAssertEqual(StubModels.firstParsedResult, scanningResult.result) - expectation.fulfill() - case .failure: - XCTFail() - } - } - wait(for: [expectation], timeout: 10.0) - } - - func testaSingleRecognizeError() { - let expectation = XCTestExpectation() - textRecognizer.recognizeResult = .failure(StubError.stub) - scan(scanningType: .single) { result in - switch result { - case .success: - XCTFail() - case .failure(let error): - XCTAssertTrue(error is StubError) - expectation.fulfill() - } - } - wait(for: [expectation], timeout: 10.0) - } - - func testSingleParserError() { - let expectation = XCTestExpectation() - textRecognizer.recognizeResult = .success(StubModels.textRecognizerResults) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - expectation.fulfill() - } - - scan(scanningType: .single) { rects in - XCTFail() - } completion: { result in - switch result { - case .success: - XCTFail() - case .failure: - expectation.fulfill() - } - } - - wait(for: [expectation], timeout: 10.0) - } - - // MARK: Live - - func testLiveComplete() { - let expectation = XCTestExpectation() - textRecognizer.recognizeResult = .success(StubModels.textRecognizerResults) - parser.parsedResult = StubModels.firstParsedResult - scan(scanningType: .live) { result in - switch result { - case .success(let scanningResult): - XCTAssertEqual(StubModels.firstParsedResult, scanningResult.result) - expectation.fulfill() - case .failure: - XCTFail() - } - } - wait(for: [expectation], timeout: 10.0) - } - - func testLiveRecognizeError() { - let expectation = XCTestExpectation() - textRecognizer.recognizeResult = .failure(StubError.stub) - scan(scanningType: .live) { rects in - XCTFail() - } completion: { result in - switch result { - case .success: - XCTFail() - case .failure(let error): - XCTAssertTrue(error is StubError) - expectation.fulfill() - } - } - wait(for: [expectation], timeout: 10.0) - } - - func testLiveParserError() { - let expectation = XCTestExpectation() - textRecognizer.recognizeResult = .success(StubModels.textRecognizerResults) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - expectation.fulfill() - } - - scan(scanningType: .live) { rects in - XCTFail() - } completion: { result in - switch result { - case .success: - XCTFail() - case .failure: - XCTFail() - } - } - - wait(for: [expectation], timeout: 10.0) - } - - private func scan( - scanningType: DefaultScanner.ScanningType, - rectsHandler: (([CGRect]) -> Void)? = nil, - completion: @escaping (Result, Error> - ) -> Void) { - scanner.scan( - scanningType: scanningType, - scanningImage: .pixelBuffer(StubModels.sampleBufferStub), - orientation: .up, - regionOfInterest: nil, - minimumTextHeight: nil, - recognitionLevel: scanningType == .live ? .fast : .accurate, - completionHandler: completion - ) - } -} diff --git a/Tests/MRZScannerTests/ImageMRZScannerTests.swift b/Tests/MRZScannerTests/ImageMRZScannerTests.swift deleted file mode 100644 index 8e87441..0000000 --- a/Tests/MRZScannerTests/ImageMRZScannerTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ImageMRZScannerTests.swift -// -// -// Created by Roman Mazeev on 14.07.2021. -// - -import XCTest -@testable import MRZScanner - -final class ImageMRZScannerTests: XCTestCase { - private var imageMRZScanner: ImageMRZScanner { - .init(textRecognizer: textRecognizer, validator: StubValidator(), parser: parser) - } - - private var textRecognizer: StubTextRecognizer! - private var parser: StubParser! - - override func setUp() { - super.setUp() - - textRecognizer = StubTextRecognizer() - parser = StubParser() - } - - func testSuccess() { - let expectation = XCTestExpectation() - textRecognizer.recognizeResult = .success(StubModels.textRecognizerResults) - parser.parsedResult = StubModels.firstParsedResult - imageMRZScanner.scan(scanningImage: .pixelBuffer(StubModels.sampleBufferStub), orientation: .up) { result in - switch result { - case .success: - expectation.fulfill() - case .failure: - XCTFail() - } - } - wait(for: [expectation], timeout: 10.0) - } -} diff --git a/Tests/MRZScannerTests/LiveMRZScannerTests.swift b/Tests/MRZScannerTests/LiveMRZScannerTests.swift deleted file mode 100644 index 31d065b..0000000 --- a/Tests/MRZScannerTests/LiveMRZScannerTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// LiveMRZScannerTests.swift -// -// -// Created by Roman Mazeev on 14.07.2021. -// - -import XCTest -@testable import MRZScanner - -final class LiveMRZScannerTests: XCTestCase { - private var liveMRZScanner: LiveMRZScanner { - .init(textRecognizer: textRecognizer, validator: StubValidator(), parser: parser, tracker: tracker) - } - - private var textRecognizer: StubTextRecognizer! - private var parser: StubParser! - private var tracker: StubTracker! - - override func setUp() { - super.setUp() - - textRecognizer = StubTextRecognizer() - parser = StubParser() - tracker = StubTracker() - } - - func testSuccess() { - textRecognizer.recognizeResult = .success(StubModels.textRecognizerResults) - parser.parsedResult = StubModels.firstParsedResult - tracker.isResultStable = true - let expectation = XCTestExpectation() - liveMRZScanner.scanFrame(scanningImage: .pixelBuffer(StubModels.sampleBufferStub), orientation: .up) { result in - switch result { - case .success(let scanningResult): - XCTAssertEqual(scanningResult.result, StubModels.firstParsedResult) - expectation.fulfill() - case .failure: - XCTFail() - } - } - wait(for: [expectation], timeout: 10.0) - } - - func testFailure() { - textRecognizer.recognizeResult = .failure(StubError.stub) - let expectation = XCTestExpectation() - liveMRZScanner.scanFrame(scanningImage: .pixelBuffer(StubModels.sampleBufferStub), orientation: .up) { result in - switch result { - case .success: - XCTFail() - case .failure(let error): - XCTAssertTrue(error is StubError) - expectation.fulfill() - } - } - wait(for: [expectation], timeout: 10.0) - } - - func testTrackerFailure() { - textRecognizer.recognizeResult = .success(StubModels.textRecognizerResults) - parser.parsedResult = StubModels.firstParsedResult - tracker.isResultStable = false - let expectation = XCTestExpectation() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - expectation.fulfill() - } - liveMRZScanner.scanFrame(scanningImage: .pixelBuffer(StubModels.sampleBufferStub), orientation: .up) { result in - switch result { - case .success: - XCTFail() - case .failure: - XCTFail() - } - } - wait(for: [expectation], timeout: 10.0) - } -} diff --git a/Tests/MRZScannerTests/FrequencyTrackerTests.swift b/Tests/MRZScannerTests/MRZFrequencyTrackerTests.swift similarity index 87% rename from Tests/MRZScannerTests/FrequencyTrackerTests.swift rename to Tests/MRZScannerTests/MRZFrequencyTrackerTests.swift index ee0798d..17e54aa 100644 --- a/Tests/MRZScannerTests/FrequencyTrackerTests.swift +++ b/Tests/MRZScannerTests/MRZFrequencyTrackerTests.swift @@ -1,5 +1,5 @@ // -// FrequencyTrackerTests.swift +// MRZFrequencyTrackerTests.swift // // // Created by Roman Mazeev on 13.07.2021. @@ -8,14 +8,14 @@ import XCTest @testable import MRZScanner -final class FrequencyTrackerTests: XCTestCase { - private var tracker: Tracker! +final class MRZFrequencyTrackerTests: XCTestCase { + private var tracker: MRZFrequencyTracker! private let frequency = 8 override func setUp() { super.setUp() - tracker = FrequencyTracker(frequency: frequency) + tracker = MRZFrequencyTracker(frequency: frequency) } func testOneResultFrequencyTimes() { diff --git a/Tests/MRZScannerTests/MRZParserTests.swift b/Tests/MRZScannerTests/MRZParserTests.swift deleted file mode 100644 index 509e7d0..0000000 --- a/Tests/MRZScannerTests/MRZParserTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// MRZParserTests.swift -// -// -// Created by Roman Mazeev on 14.07.2021. -// - -import XCTest -@testable import MRZScanner - -final class MRZParserTests: XCTestCase { - private var parser: Parser! - - override func setUp() { - super.setUp() - - parser = MRZLineParser() - } - - func testSuccess() { - let parsedResult = parser.parse( - lines: [ - "P Bool { +extension MRZValidator.Result: Equatable { + public static func == (lhs: MRZValidator.Result, rhs: MRZValidator.Result) -> Bool { lhs.result == rhs.result && lhs.index == rhs.index } } diff --git a/Tests/MRZScannerTests/Stubs/StubModels.swift b/Tests/MRZScannerTests/StubModels.swift similarity index 69% rename from Tests/MRZScannerTests/Stubs/StubModels.swift rename to Tests/MRZScannerTests/StubModels.swift index 700cd35..25abdd6 100644 --- a/Tests/MRZScannerTests/Stubs/StubModels.swift +++ b/Tests/MRZScannerTests/StubModels.swift @@ -6,6 +6,7 @@ // @testable import MRZScanner +import MRZParser import CoreImage struct StubModels { @@ -17,9 +18,10 @@ struct StubModels { return formatter }() - static let firstParsedResult = ParsedResult( + static let firstParsedResult = MRZResult( format: .td3, documentType: .passport, + documentTypeAdditional: nil, countryCode: "UTO", surnames: "ERIKSSON", givenNames: "ANNA MARIA", @@ -32,9 +34,10 @@ struct StubModels { optionalData2: nil ) - static let secondParsedResult = ParsedResult( + static let secondParsedResult = MRZResult( format: .td2, documentType: .id, + documentTypeAdditional: "A", countryCode: "", surnames: "", givenNames: "", @@ -46,13 +49,4 @@ struct StubModels { optionalData: nil, optionalData2: nil ) - - static let textRecognizerResults: [TextRecognizerResult] = [.init(results: [], boundingRect: .zero)] - static let validatedResults: [ValidatedResult] = [.init(result: "", index: 0)] - - static var sampleBufferStub: CVPixelBuffer { - var pixelBuffer : CVPixelBuffer? = nil - CVPixelBufferCreate(kCFAllocatorDefault, 100, 100, kCVPixelFormatType_32BGRA, nil, &pixelBuffer) - return pixelBuffer! - } } diff --git a/Tests/MRZScannerTests/Stubs/StubError.swift b/Tests/MRZScannerTests/Stubs/StubError.swift deleted file mode 100644 index 42ba9f5..0000000 --- a/Tests/MRZScannerTests/Stubs/StubError.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// StubError.swift -// -// -// Created by Roman Mazeev on 14.07.2021. -// - -import Foundation - -enum StubError: Error { - case stub -} diff --git a/Tests/MRZScannerTests/Stubs/StubParser.swift b/Tests/MRZScannerTests/Stubs/StubParser.swift deleted file mode 100644 index 0ae6154..0000000 --- a/Tests/MRZScannerTests/Stubs/StubParser.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// StubParser.swift -// -// -// Created by Roman Mazeev on 13.07.2021. -// - -@testable import MRZScanner - -struct StubParser: Parser { - var parsedResult: ParsedResult? - func parse(lines: [String]) -> ParsedResult? { - parsedResult - } -} diff --git a/Tests/MRZScannerTests/Stubs/StubTextRecognizer.swift b/Tests/MRZScannerTests/Stubs/StubTextRecognizer.swift deleted file mode 100644 index e016b76..0000000 --- a/Tests/MRZScannerTests/Stubs/StubTextRecognizer.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// StubTextRecognizer.swift -// -// -// Created by Roman Mazeev on 13.07.2021. -// - -@testable import MRZScanner -import CoreImage - -struct StubTextRecognizer: TextRecognizer { - var recognizeResult: Result<[TextRecognizerResult], Error>? - func recognize( - scanningImage: ScanningImage, - orientation: CGImagePropertyOrientation, - regionOfInterest: CGRect?, - minimumTextHeight: Float?, - recognitionLevel: RecognitionLevel, - completionHandler: @escaping (Result<[TextRecognizerResult], Error>) -> Void - ) { - if let recognizeResult = recognizeResult { - completionHandler(recognizeResult) - } - } -} diff --git a/Tests/MRZScannerTests/Stubs/StubTracker.swift b/Tests/MRZScannerTests/Stubs/StubTracker.swift deleted file mode 100644 index 5658bb3..0000000 --- a/Tests/MRZScannerTests/Stubs/StubTracker.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// StubTracker.swift -// -// -// Created by Roman Mazeev on 13.07.2021. -// - -@testable import MRZScanner - -struct StubTracker: Tracker { - var isResultStable = true - func isResultStable(_ result: ParsedResult) -> Bool { isResultStable } - func reset() {} -} diff --git a/Tests/MRZScannerTests/Stubs/StubValidator.swift b/Tests/MRZScannerTests/Stubs/StubValidator.swift deleted file mode 100644 index 6b93f34..0000000 --- a/Tests/MRZScannerTests/Stubs/StubValidator.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// StubValidator.swift -// -// -// Created by Roman Mazeev on 13.07.2021. -// - -@testable import MRZScanner - -struct StubValidator: Validator { - var validatedResults: ValidatedResults = [] - func getValidatedResults(from possibleLines: [[String]]) -> ValidatedResults { - validatedResults - } -} diff --git a/docs/img/example.gif b/docs/img/example.gif deleted file mode 100644 index f2026fa..0000000 Binary files a/docs/img/example.gif and /dev/null differ